学习指南:
为什么需要Hooks?
Hook是React16.8的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(如生命周期)。
类组件
顾名思义,也就是通过使用ES6类的编写形式去编写组件,
该类必须继承React.Component
如果想要访问父组件传递过来的参数,可通过this.props的方式去访问
在组件中必须实现render方法,在return中返回React对象,如下:
函数组件
函数组件,顾名思义,就是通过函数编写的形式去实现一个React组件,是React中定义组件最简单的方式
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
函数第一个参数为props用于接收父组件传递过来的参数
区别
针对两种React组件,其区别主要分成以下几大方向:
- 编写形式
- 状态管理
- 生命周期
- 调用方式
- 获取渲染的值
- 编写形式
- 函数组件:function组件是以函数的形式编写,它只需要返回JSX即可。
- 类组件:class组件是以ES6的class语法编写的,它需要继承React.Component,并且有自己的render()方法来返回组件的结构
- 状态管理
- 函数组件:在Hook出现之前那没有自己的状态和状态的变更方法,只能通过props从外部传递数据,但是Hook出现后,可以通过使用 useState来方便的管理状态。
- 类组件:可以使用state来定义自己的内部状态,同时可以在内部使用setState()方法来修改这些状态。
- 生命周期
- 函数组件:没有常规的生命周期方法,如componentDidMount、componentDidUpdate等。
但是函数组件使用useEffect也能够完成替代生命周期的作用
- 类组件:可以使用所有的生命周期方法,如componentDidMount、componentDidUpdate等,同时可以使用ShouldComponentUpdate来控制是否需要更新组件。
- 调用方式
- 函数组件:调用则是执行函数即可,可以直接渲染。
- 类组件:需要将组件进行实例化,然后调用实例对象的render方法
- 获取渲染的值
- 而函数组件,本身就不存在this,props并不发生改变,
- 类组件中,输出this.props,Props在 React中是不可变的所以它永远不会改变,但是 this 总是可变的,以便您可以在 render 和生命周期函数中读取新版本
useState
useState 是 React 中一个用于管理组件状态的 Hook,它可以让你在函数式组件中使用状态(state)。
useState 接收一个初始状态值,返回一个包含当前状态和状态更新函数的数组,你可以使用数组解构来获取这两个值。
使用 useState 可以让你在函数式组件中使用状态,这使得函数式组件可以管理自己的状态,避免了使用类组件的繁琐,同时也使得代码更加简洁易读。
使用规则
- 只能在函数最外层调用Hook,不要在循环、条件判断或者子函数中调用。
- 只能在React的函数组件中调用Hook。不要在其他JavaScript函数中调用。
const [couter,setCouter] = useState(0)
- state 可以是 对象
当 useState 中的 state 是对象时,调用相应的setState ,需要注意,useState 不会自动合并更新对象,可以使用函数式的 setState 结合展开运算符来达到合并更新对象的效果。
export default function App() {
const initValue = { n: 0, m: 0 };
const [state, setState] = useState(initValue);
const addN = () => {
setState((state) => {
+ return { ...state, n: state.n + 1 };
});
};
return (
<div className="App">
<button onClick={addN}>+1,此时n:{state.n}</button>
</div>
);
}
useState 和useReduce 作为能够触发组件重新渲染的hooks,
我们在使用useState的时候要特别注意的是,useState派发更新函数的执行,
就会让整个function组件从头到尾执行一次。
- 通过回调函数更新状态
setCount((prevCount) => prevCount + 1);
这里用到了 setCount 的另外一种写法,即使用一个回调函数来更新状态。
在 React 中,状态的更新是异步的。
当我们调用 setCount(count + 1) 时,并不能保证count 立即更新,而是会在后续某个时间点更新。
如果我们想根据当前 count 的值更新状态,我们就不能直接使用 setCount,而是应该使用回调函数。
回调函数的第一个参数则是当前状态的值,为了避免出现异步更新所带来的问题,我们需要在回调函数中显式地使用当前状态值,并返回一个新的状态。
这样我们就能够保证状态的更新是可靠的,而且顺序也正确。
因此,setCount((prevCount) => prevCount + 1) 相当于是将当前的 count 值作为第一个参数传入一个回调函数,这个回调函数会将当前值加 1,返回一个新的状态,
然后将这个新状态设置为 count 的新值。这样我们就能够正确地更新状态,而不用担心有什么异步更新的问题。
实现原理
useState 的原理其实很简单:它是通过闭包和对象的引用来实现的。
因为 useState 在不断地记录着下一次组件渲染所需要用到的状态,并在该组件再次渲染时,重新取出之前
保存的状态值。这是通过闭包实现的。
实际上,每一次在 useState 中使用的状态值都是通过一个类似于对象的数组进行保存的。这个对象的引用在每一次函数执行结束后,被保存到了闭包中,以便在下一次函数执行时使用它,这就是通过闭包实现的。
当我们调用 useState 函数时,它会返回一个数组,其中第一个元素是组件中保存的状态值,第二个元素是更新状态值的函数。
在使用这个方法时,我们需要注意到的是,每次更新状态值时,都必须传入新的状态值,而不能改变已有的状态值。
因此,为了能够正常使用 useState,我们需要通过这个对象的引用来保存和更新状态值。
当调用更新函数时,React 将会重新渲染组件,并且使用新的状态值替换原来的状态值。
在这个过程中,React 会比较新的状态和之前的状态,以确定是否需要更新组件。
如果新状态和之前状态相同,则不会执行渲染操作。
需要注意的是,useState 创建的状态变量是可变的,即其值的状态是可以修改的。
但是,我们不应该直接修改状态变量的值,而是应该通过调用更新函数来修改值。
这是因为 React 在更新状态变量时可能会对多个调用进行批处理,提高性能和效率。
源码角度分析
事实上,React 中的 useState Hook 只是 useReducer 的一个简化版本。
useState 函数可以接受一个初始状态值,并返回一个数组,其中包含了状态值以及更新状态值的函数。
这个流程其实就是 useReducer 的包装过程,只是 useState 的更新函数内部已经实现好了对状态的更新行为,这样可以减少开发者的代码量,并且提高代码的可读性和可维护性。
FiberNode 是 React 内部实现协调的基本数据结构,可以理解为描述组件生成、更新、卸载过程的对象。
FiberNode 是 React Fiber 架构中的一个关键概念,在实现 React 的 Hooks 功能中也扮演着重要的角色
- 在组件内部声明 useState 变量
在组件内部通过 useState 函数声明一个变量,React 内部会根据这个变量创建并返回一个 Hook 对象,其中包含状态值以及更新状态值的函数。
useState 函数的返回值和保存当前状态值的变量的赋值操作都在该组件中执行,因此组件自身就存储了这个状态变量。
export function useState<S>(
initialState: (() => S) | S
): [S, Dispatch<SetStateAction<S>>] {
return useReducer(
basicStateReducer,
// useReducer has a special case to support lazy useState initialization
(initialState as any) as S,
(initialState: any) => {
return initialState
}
)
}
- 将 Hook 对象压入 FiberNode 的链表中
组件渲染时,React 首先会创建对应的 FiberNode,并将其加入 Fiber 树中。
接着,在 FiberNode 中创建一个 HookNode,
HookNode 中包含 index 和 memoizedState 两个字段,
分别表示 Hook 对象在 Hook 链表中的索引位置和当前状态的值。
React 在组件渲染时会扫描 FiberNode 上已经使用的 Hooks 链表,并根据每个 Hook 对象来获取和更新组件的状态。
- 利用 Hook 链表中的 index 获取当前 Hook 对象
React 通过调用 Hooks 根据 index 获取当前组件的 Hooks 链表中的 Hook 对象,并将其保存在变量 currentState 中。
function readContext(context, observedBits) {
const dispatcher = resolveDispatcher()
return dispatcher.readContext(context, observedBits)
}
function resolveCurrentlyRenderingFiber() {
const fiber = workInProgress ?? currentlyRenderingFiber
return fiber
}
function resolveCurrentlyRenderingFiberWithHooks() {
const currentlyRenderingFiber = resolveCurrentlyRenderingFiber()
invariant(
currentlyRenderingFiber !== null && currentlyRenderingFiber.memoizedState !== null,
'Hooks can only be called inside the body of a function component.'
)
return currentlyRenderingFiber
}
function getCurrentHookValue() {
const hook = resolveCurrentlyRenderingFiberWithHooks().memoizedState as Hook
return hook.memoizedState
}
- 执行 useState 的更新操作
在组件渲染完成后,React 代码便可执行 Hook 中定义的函数或代码片段,以根据 Hook 返回的状态值动态更新视图。
function useState<S>(
initialState: (() => S) | S
): [S, Dispatch<SetStateAction<S>>] {
const dispatcher = resolveDispatcher()
return dispatcher.useState(initialState)
}
useEffect
useEffect 是 React 提供的一个 Hook,用于在函数组件中执行副作用操作(side effects),
- 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响, 比如修改了全局变量,修改参数或者改变外部的存储;
如访问 DOM、调用 API、设置定时器等。它可以看作是类组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合体。
useEffect 接收两个参数,第一个参数是副作用函数,第二个参数是依赖项数组(可选),用于指定副作用函数所依赖的状态变量,只有当这些状态变量发生变化时,副作用函数才会被调用。
如果没有指定依赖项数组,则副作用函数在每次组件渲染时都会被调用。
副作用函数中可以返回一个清除函数,用于清除副作用,例如清除定时器、取消订阅等。
useEffect 有以下几个特点:
- useEffect 中定义的副作用函数是异步执行的,它不会阻塞浏览器的渲染。
- 副作用函数中可以返回一个清除函数,用于清除副作用,例如清除定时器、取消订阅等。
- 如果组件卸载了或依赖项数组发生了变化,React 就会自动执行清除函数,以避免出现内存泄漏等问题。
- 如果不指定依赖项数组,副作用函数在每次组件渲染时都会被调用,这可能会导致不必要的性能开销,因此需要谨慎使用。
- 在某些情况下,可以通过在依赖项数组中传入空数组来实现只在组件挂载和卸载时执行副作用函数的效果。
React何时清除effect?
- React 会在组件更新和卸载的时候执行清除操作。
- 但我们发现 effect 会在每次更新的时候都被调用。
useEffect(()=>{
console.log("监听redux中的数据")
return ()=>{
console.log("取消监听redux中的数据变化")
}
})
const [count,setCount] = useState(0);
const [message,setMessage] = useState("Hello World ")
useEffect(()=>{
console.log("被count与message同时影响")
})
useEffect(()=>{
console.log("只受count影响")
},[count])// 控制回调函数只受count影响
如果一个函数我们不希望依赖任何的内容时,也可以传入一个新的空数组[]
const [count,setCount] = useState(0);
const [message,setMessage] = useState("Hello World ")
useEffect(()=>{
console.log("发送网络请求,从服务器获取数据")
return ()=>{
console.log("只有在组件被卸载的时候,才会执行一次")
}
},[])// 只在函数初始化的时候执行一次
使用Hook的其中一个目的就是解决class中生命周期经常将很多的逻辑放在一起的问题
- 比如网络请求、事件监听、手动修改DOM、这些往往都会放在componentDidMount中。
但使用Effect Hook,可以很好的解决这一问题,我们可以将他们分离到不同的useEffect中。
- hook允许我们按照代码的用途分离他们,而不是像生命周期那样。
- React 将按照 effect 声明的顺序依次调用组件中的每一个effect 函数。
useEffect(()=>{
console.log("修改title")
})
useEffect(()=>{
console.log("监听redux中的数据")
})
useEffect(()=>{
console.log("发送网络请求,从服务器获取数据")
})
使用坑点
async await
不能直接用 async await 语法糖
/* 错误用法 ,effect不支持直接 async await 装饰的 */
useEffect(async ()=>{
/* 请求数据 */
const res = await getUserInfo(payload)
},[ a ,number ])
如果想要用 async 可以对 async 函数进行进行一层包装
useEffect(()=>{
async ()=>{
/* 请求数据 */
const res = await getUserInfo(payload)
}
},[ a ,number ])
useEffect 执行顺序 组件更新挂载完成 -> 浏览器dom 绘制完成 -> 执行useEffect回调 。
闭包问题
在 useEffect 中,闭包陷阱是指在 useEffect 中使引用了该 组件 中声明的某些状态,因为useEffect 只有
在第一次渲染的时候才会触发, 状态 渲染更新时, useEffect里面的回调函数并没有触发。
因此useEffect里面的 状态变量 还是初始化时的值,并没有获取到最新的. 这就是闭包陷阱
- 例如,在以下代码中,如果 data 的值在组件更新后发生了改变,但是 fetchData 函数中的闭包仍然引用了更新前的值,导致 fetchData 中使用的 data 值不是最新的值。
function MyComponent() {
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const result = await fetch('https://example.com/data');
setData(result);
};
fetchData();
}, []);
// ...
}
- 为了避免闭包陷阱,可以使用 **useRef 来存储需要在 useEffect 中使用的变量,将要访问的变量或函数存储在 ref 中,**然后在 useEffect 中使用 ref.current 访问存储的变量或函数,以避免闭包陷阱。
- 因为 ref 返回的对象始终是相同的,因此无论何时使用 ref.current,都可以获得到最新的值。
- 例如,在以下代码中,我们使用 dataRef.current 来存储 data 的值,并在 fetchData 函数中使用 dataRef.current,而不是直接使用 data。
function MyComponent() {
const [data, setData] = useState([]);
const dataRef = useRef([]);
useEffect(() => {
const fetchData = async () => {
const result = await fetch('https://example.com/data');
dataRef.current = result;
setData(result);
};
fetchData();
}, []);
// ...
}
应用场景
useEffect 应用场景
- 数据获取和订阅管理:useEffect 可以用于发送网络请求、订阅事件等异步操作,它可以在组件渲染完成后执行这些操作,并在组件卸载时清除它们。
useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
setData(data);
})
.catch(error => {
setError(error);
});
}, []);
- 状态更新后的操作:useEffect 可以监听一个或多个状态变化,当状态更新时执行相应的操作。例如,可以在 useEffect 中监听页面滚动条的变化并执行相应的操作,。
- 我们在 useEffect 的回调函数中添加了一个事件监听器,当滚动条发生变化时,会执行 handleScroll 函数中定义的操作。
- 注意,为了避免每次渲染都添加事件监听,我们将空数组作为 useEffect 的第二个参数,表示只在组件挂载和卸载时执行一次。
- 最后,为了避免内存泄漏,我们在 useEffect 的返回函数中移除事件监听器。
import React, { useEffect } from 'react';
function ScrollComponent() {
useEffect(() => {
function handleScroll() {
// 执行滚动条变化时的操作
console.log('scrolling');
}
// 在组件挂载时添加事件监听
window.addEventListener('scroll', handleScroll);
// 在组件卸载时移除事件监听
return () => window.removeEventListener('scroll', handleScroll);
}, []); // 空数组表示只在组件挂载和卸载时执行一次
return <div>ScrollComponent</div>;
}
- 处理副作用:useEffect 还可以用于处理一些副作用,例如在组件渲染完成后自动将焦点聚焦到某个输入框、在组件卸载时自动清除定时器等。
useEffect(() => {
const interval = setInterval(() => {
setCount(count => count + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
useRef
const dom = useRef(0);
console.log(`r.current:${dom.current}`);
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。
返回的 ref 对象在组件的整个生命周期内保持不变。
这个ref对象只有一个current属性,将一个值保存在内,它的地址一直不会变。
- 在 React 中,可以通过 ref 属性引用 DOM 元素或组件实例,以便在组件内部直接访问这些元素或实例,而无需使用选择器或其他方式来查找它们。
- 对于 input 标签,通过在组件中使用 ref 属性引用该元素,可以获取该元素的值或操作该元素的属性和方法。
- 例如,可以使用 ref.current.value 获取 input 元素中当前的文本值,并使用 ref.current.focus() 方法将焦点设置到该元素上。
import React, { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
const handleClick = () => {
console.log(inputRef.current.value);
};
return (
<>
<input type="text" ref={inputRef} />
<button onClick={handleClick}>Get Input Value</button>
</>
);
}
在上面的示例中,使用 useRef 钩子创建了一个名为 inputRef 的变量,该变量被传递给 input 元素的 ref 属性中。
在 handleClick 函数中,可以使用 inputRef.current.value 获取 input 元素中当前的文本值,并将其输出到控制台中。
- useRef 可以存储和更新组件内部的变量,因为 current 属性的值发生变化时,不会跟触发组件重新渲染,可以起到缓存数据的作用。
- useState或者useReducer虽然也可以保存当前数据源,但是会触发组件重新渲染,如果在函数组件内部声明变量则下一次更新也会重置。
- 因此可以用它来存储那些不需要触发重新渲染的状态,例如计时器的 ID:
- 在上面的例子中,intervalIdRef.current 可以持久存储计时器的 ID,并在组件卸载时清除该计时器。
import { useRef, useEffect } from 'react';
function Example() {
const intervalIdRef = useRef(null);
useEffect(() => {
intervalIdRef.current = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(intervalIdRef.current);
}, []);
// ...
}
小结一下:
- 我们使用useCurrent传入initialValue,可以得出其变成了一个内含current属性的对象,current属性对应的值为initialValue。
- 根据react文档,可以知道这个对象的地址从头到位都不会变化。
- useRef变化不会主动使页面渲染。
应用
- react-redux源码
react-redux 在react-hooks发布后,用react-hooks重新重构了其中的Provide,connectAdvanced)核心模块。
react-hooks在限制数据更新,高阶组件上有这一定的优势,其源码大量运用useMemo来做数据判定
/* 这里用到的useRef没有一个是绑定在dom元素上的,都是做数据缓存用的 */
/* react-redux 用userRef 来缓存 merge之后的 props */
const lastChildProps = useRef()
// lastWrapperProps 用 useRef 来存放组件真正的 props信息
const lastWrapperProps = useRef(wrapperProps)
//是否储存props是否处于正在更新状态
const renderIsScheduled = useRef(false)
这是react-redux中用useRef 对数据做的缓存,那么怎么做更新的呢 ,我们接下来看
//获取包装的props
function captureWrapperProps(
lastWrapperProps,
lastChildProps,
renderIsScheduled,
wrapperProps,
actualChildProps,
childPropsFromStoreUpdate,
notifyNestedSubs
) {
//我们要捕获包装props和子props,以便稍后进行比较
lastWrapperProps.current = wrapperProps //子props
lastChildProps.current = actualChildProps //经过 merge props 之后形成的 prop
renderIsScheduled.current = false
}
react-redux 用重新赋值的方法,改变缓存的数据源,避免不必要的数据更新。
如果选用useState储存数据,必然促使组件重新渲染,所以采用了useRef解决了这个问题。
- 在子组件中使用ref
不能直接在子组件上使用 ref
<Child ref={textInput} /> // 错误写法
但是这样还拿不到子组件的DOM,我们需要使用 forwardRef 配合使用,上面的例子可以写成这样
function CustomTextInput(props) {
// 这里必须声明 textInput,这样 ref 才可以引用它
const textInput = useRef(null);
// 打印 绑定到子组件的 ref
console.log(textInput);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<Child ref={textInput} /> //**依然使用ref传递**
<input type="button" value="Focus the text input" onClick={handleClick} />
</div>
);
}
const Child = forwardRef((props, ref) => { //** 看我 **
return <input type="text" ref={ref} />;//** 看我挂到对应的dom上 **
});
上面是通过forwardRef 把 Child 函数包起来,然后传入第二个参数 ref
最后挂载 ref={ref} 这样就可以拿到对应的DOM了,控制台打印一下看看
current: <input type="text"></input>
实现原理
function useRef(initialValue) {
return createRef().current;
}
该函数接收一个初始值initialValue,然后调用createRef()函数返回一个对象,最后返回该对象的current属性。
之所以经过两次函数调用,而不是直接返回一个对象的current属性,是由于createRef()返回的对象只有在组件渲染时才会被创建,在每次重新渲染时都会创建一个新的对象。
因此,需要在函数内部使用useRef时,每次都要调用createRef()函数来创建一个新的ref对象,从而实现正确的数据引用。
在React 源码中,useRef函数的内部实现基于useMemo函数和函数组件的执行顺序。
useMemo 函数的作用是在渲染中缓存值,避免每次渲染都重新计算,从而提高性能。
useRef 函数的内部实现就是创建一个useMemo函数,该函数利用了函数组件的执行顺序,在每次组件重
新渲染时,始终返回同一个ref对象,从而实现了对相同的引用的保留。
总的来说,useRef的实现原理是基于函数式编程思想和React的内部机制,通过createRef()函数创建一个
ref对象,利用useMemo函数缓存引用,从而实现对DOM元素引用、临时状态、数据缓存等的管理。
useMemo
useMemo 是 React 中的一个 Hook,用于在函数组件中进行性能优化。
当一个函数在不同的场景下可能被多次调用时,如果该函数的计算开销比较大,会导致不必要的性能开
销,这时候可以通过useMemo来对该函数进行封装,避免重复计算,提高性能。
它的作用就是将计算结果缓存起来,可以让组件只在必要的情况下进行重新计算,从而提高应用的性能,
避免在组件多次渲染的过程中进行重复计算。
useMemo 接收两个参数:
第一个参数是一个函数,这个函数的返回值是需要缓存的变量;
第二个参数是一个数组,用于指定依赖项,当依赖项中的任意一个发生变化时,就会重新计算缓存的值。如果依赖项未发生变化,则直接返回上一次缓存的值。
应用场景
1. 缓存 useEffect 的引用类型依赖
import { useEffect } from 'react'
export default () => {
const msg = {
info: 'hello world',
}
useEffect(() => {
console.log('msg:', msg.info)
}, [msg])
}
此时 msg 是一个对象该对象作为了 useEffect 的依赖,这里本意是 msg 变化的时候打印 msg 的信息。
但是实际上每次组件在render 的时候 msg 都会被重新创建,
因为msg是引用类型,所以 msg 的引用在每次 render 时都是不一样的,就会出现Effect依赖无限循环。
即 useEffect 在每次render 的时候都会重新执行,和我们预期的不一样。
此时 useMemo 就可以派上用场了:
import { useEffect, useMemo } from "react";
const App = () => {
const msg = useMemo(() => {
return {
info: "hello world",
};
}, []);
useEffect(() => {
console.log("msg:", msg.info);
}, [msg]);
};
export default App;
同理对于函数作为依赖的情况,我们可以使用 useCallback:
import { useEffect, useCallback } from "react";
const App = (props) => {
const print = useCallback(() => {
console.log("msg", props.msg);
}, [props.msg]);
useEffect(() => {
print();
}, [print]);
};
export default App;
2. 缓存子组件 props 中的引用类型。
做这一步的目的是为了防止组件非必要的重新渲染造成的性能消耗,所以首先要明确组件在什么情况下会重新渲染。
- 组件的 props 或 state 变化会导致组件重新渲染
- 父组件的重新渲染会导致其子组件的重新渲染
这一步优化的目的是:
在父组件中跟子组件没有关系的状态变更导致的重新渲染可以不渲染子组件,造成不必要的浪费。
import { useCallback, useState, memo, useMemo } from "react";
const Child = memo((props) => {});
const App = () => {
const [count, setCount] = useState(0);
const handleChange = useCallback(() => {}, []);
const list = useMemo(() => {
return [];
}, []);
return (
<>
<div
onPress={() => {
setCount(count + 1);
}}
>
increase
</div>
<Child handleChange={handleChange} list={list} />
</>
);
};
export default App;
对于复杂的组件项目中会使用 memo 进行包裹,目的是为了对组件接受的 props 属性进行浅比较来判断组件要不要进行重新渲染,但是如果 props 属性里面有引用类型的情况,比如 handleChange 在 App 组件每次重新渲染的时候都会重新创建生成,引用当然也是不一样的,那么势必会造成 Child 组件重新渲染。
引用会改变,所以我们需要缓存这些值保证引用不变,避免不必要的重复渲染。
- 当需要将大量数据传递给子组件时,使用useMemo缓存数据,避免在每次渲染时都重新计算数据。
- 当需要将函数作为props传递给子组件时,使用useMemo缓存函数的引用,以避免在每次渲染时都创建新的函数对象。
实现原理
function useMemo(factory, deps) {
const patch = useContext(Context);
if (patch) patch.deps(deps);
const inputs = Array.isArray(deps) ? deps : [factory];
return patch ? patch.memo(inputs, factory) : factory();
}
该函数接收两个参数:factory和deps。
其中factory是一个函数,用于创建要缓存的值;
deps是一个数组,包含了所有要监测的值,当deps中的值发生变化时,useMemo才重新计算缓存的值。
在React源码中,useMemo内部实现主要基于Context和Fiber节点。
当调用useMemo时,会通过useContext函数获取到当前Context对象,并将deps作为一个参数传递给
Context的deps方法,该方法负责向Fiber节点添加监控依赖。
当组件重新渲染时,React会检查deps数组中的所有依赖项,如果发现有变化,就重新计算缓存的值;
如果没有变化,就用之前的缓存值,从而避免了不必要的计算。
如果存在相同的deps和factory,useMemo会返回之前缓存的值,否则会重新计算缓存的值并存储起来。
为了实现数据的缓存和共享,依赖项内部存储的值将被封装成一个Memo对象,并绑定到Fiber节点中。
当组件重新渲染时,可以通过Memo对象直接获取上一次缓存的结果。
useCallback
简单来说就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。
useCallback 的作用是缓存函数,避免重复地创建函数实例,减小子组件的没必要要重复渲染,从而提高组件的性能。
常见问题
useCallback 和 useMemo 区别
- useMemo用于缓存计算结果,避免无用的计算。它需要传递两个参数:第一个参数是一个函数,它会在组件渲染时调用,并返回计算结果;第二个参数是一个数组,用于控制 useMemo 何时需要重新计算结果。
- useCallback本质上是 useMemo 的特定用例,它会缓存函数本身。它接受两个参数:第一个参数是一个函数,第二个参数是一个数组,用于控制何时需要重新创建缓存函数。当数组中的任何一个值发生变化时,useCallback 将会重新创建缓存函数。缓存函数的目的是避免在每次渲染时都创建新的函数引用。
- 因此,useMemo 和 useCallback 的主要区别在于它们缓存的是什么。useMemo 缓存计算结果,而 useCallback 则缓存函数本身。
useEffect和 useMemo useCallback的区别
- useEffect可以帮助我们在DOM更新完成后执行某些副作用操作,如数据获取,设置订阅以及手动更改react 组件中的 DOM 等。
有了useEffect,我们可以在函数组件中实现 像类组件中的生命周期那样某个阶段做某件事情,具有:
- componentDidMount
- componentDidUpdate
- componentWillUnmount
- useCallback 和 useMemo 都是性能优化的手段。
类似于类组件中的 shouldComponentUpdate,在子组件中使用 shouldComponentUpdate,
判定该组件的 props 和 state 是否有变化,从而避免每次父组件render时都去重新渲染子组件
- useEffect是在DOM改变之后触发,useMemo在DOM重新渲染之前就触发
- useEffect设置值会再次重新渲染,但useMemo不会
使用坑点
无限渲染
当在父组件中传递一个回调函数给子组件时,使用useCallback可以避免子组件由于父组件的重新渲染而重
新渲染自身,从而提高性能。
但是如果在useCallback中依赖的props或state发生变化,则会触发useCallback中的函数引用发生变化,
从而引起父组件的重渲染,进而又触发子组件自身的重新渲染,这就导致了无限循环。
import { useState, useCallback } from "react";
function ParentComponent() {
const [count, setCount] = useState(0);
// useCallback函数依赖了count
const memoizedCallback = useCallback(() => {
console.log('memoizedCallback');
setCount((prevCount) => prevCount + 1);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<ChildComponent callback={memoizedCallback} />
</div>
);
}
function ChildComponent({ callback }) {
const [childCount, setChildCount] = useState(0);
const handleClick = () => {
console.log('handleClick');
callback();
setChildCount((prevCount) => prevCount + 1);
};
return (
<div>
<p>Child count: {childCount}</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
在上面的例子中,当父组件的count发生变化时,会导致memoizedCallback函数发生变化,从而引发了父
组件的重新渲染,进而也会导致子组件重新渲染。
但是子组件的重新渲染又会导致父组件中的memoizedCallback再次发生变化,这就引发了无限循环。
useContext
useContext函数是React Hooks 中的一个hook 函数,它可以让你方便地使用 React Context 来在组件之间共享数据。
使用 useContext 函数,可以摆脱繁琐的 context.Consumer 和 context.provider 包裹,使代码更加简洁易读。
使用 useContext 函数,可以很方便的从 Provider 组件中取出对应的值,而且只需要一行代码就可以实现。
- 在需要共享数据的组件中创建 Context 对象,在 Provider 组件中传入需要共享的数据。
import React, {createContext} from "react";
export const UserContext = createContext
function Chat()
return (
<UserContext.Provider value={{currentChat,currentUser,socket,handleChatChange,handleWelcome}}>
</UserContext.Provider>
)
}
- 在需要使用共享组件中使用 useContext函数传入Context 对象,然后直接导出共享数据即可。
import React , { useContext }from 'react'
import { UserContext } from '../page/Chat';
const { currentUser,currentChat,socket } = useContext(UserContext)
实现原理
它主要是通过React.createContext()函数创建一个上下文对象,用于在组件之间传递数据。
这个上下文对象提供了两个组件:Provider和Consumer,可以通过 Provider 组件来在整个应用中共享数
据,并且Consumer组件或useContext可以在应用中的任何地方访问到这些数据。
自定义Hook
封装一个 Hook 的时候需要注意以下几点:
- 命名规范:Hook 的名称必须以 use 开头,这是 React 官方对 Hook 命名的规范。
- 状态隔离:Hook 必须保证状态是相互独立的,不同的组件使用同一个 Hook 时不会相互影响。
- 参数传递:Hook 可以接受参数,但是要避免在 Hook 内部使用 props 或者 context,因为这会使 Hook 的复用性受到限制。
- 状态更新:使用 Hook 的组件需要保证在组件树内只有唯一的 Hook 实例,避免在多个组件中使用同一个 Hook 导致状态更新异常。
- 异步操作:当 Hook 中包含异步操作时,需要使用 useEffect 来控制异步操作的执行时机,并根据需要返回取消异步操作的函数。
- 兼容性:如果需要兼容旧版本的 React,需要使用 useEffect、useState 等 Hook 的 polyfill 库或者手动实现 Hook。
- 文档说明:封装的 Hook 需要提供详细的文档说明,包括参数说明、返回值说明、使用注意事项等等,方便其他开发人员使用。