浅谈React钩子函数 useMemo 和 useCallback
前言
React中有很多官方提供的hooks,例如 useEffect,useState,useMemo,useCallback,有部分初学者分不清 useMemo和useCallback究竟适用于什么场景。今天我们就来聊聊这两个钩子函数。
useMemo
它用于优化渲染性能。useMemo
会接收一个箭头函数包裹的回调函数和依赖项数组,然后返回回调函数的计算结果。当依赖项数组中的某个值发生变化时,useMemo
会重新计算回调函数。如果依赖项没有发生变化,useMemo
会返回上一次计算的结果,这样可以避免不必要的计算。如下,只有在a或者b发生改变的时候,value的值才会重新计算。
const value = useMemo(() => {
return caculateFunction (a, b)
}, [a, b])
使用场景:当存在一个昂贵的计算操作,且该操作的输入值在多次渲染之间不会发生变化时,应当使用useMemo 。
实现原理:在react hooks的体系中,钩子函数都有自己每个阶段的执行逻辑,并且保存在Dispatcher中,看一下挂载时的调度器如下:
const HooksDispatcherOnMount: Dispatcher = {
...
useMemo: mountMemo,
...
};
更新时的调度器如下:
const HooksDispatcherOnUpdate: Dispatcher = {
...
useMemo: updateMemo,
...
}
很明显,关键点在updateMemo这个方法里面。我们看一下他的实现原理
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
这里面有一个关键函数就是areHookInputsEqual,用来对比前后两个依赖项是否发生改变
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (__DEV__) {
if (ignorePreviousDependencies) {
// Only true when this component is being hot reloaded.
return false;
}
}
if (prevDeps === null) {
if (__DEV__) {
console.error(
'%s received a final argument during this render, but not during ' +
'the previous render. Even though the final argument is optional, ' +
'its type cannot change between renders.',
currentHookNameInDev,
);
}
return false;
}
if (__DEV__) {
// Don't bother comparing lengths in prod because these arrays should be
// passed inline.
if (nextDeps.length !== prevDeps.length) {
console.error(
'The final argument passed to %s changed size between renders. The ' +
'order and size of this array must remain constant.\n\n' +
'Previous: %s\n' +
'Incoming: %s',
currentHookNameInDev,
`[${prevDeps.join(', ')}]`,
`[${nextDeps.join(', ')}]`,
);
}
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
上述代码可以看出areHookInputsEqual 函数接受两个依赖项数组 nextDeps
和 prevDeps
。它会先检查两个数组的长度是否相等,如果不相等,将在开发模式下发出警告。然后,它遍历数组并使用 is
函数(类似于 Object.is
)逐个比较元素。如果发现任何不相等的元素,函数将返回 false
。否则,返回 true
这样react就知道是否需要进行重新计算操作。
useCallback
useCallback
是一个允许你在多次渲染中缓存函数的 React Hook。
同样它也会接受两个参数,callback和依赖项, 当依赖数组中的值发生变化时,useCallback
会返回一个新的函数实例。否则,它将返回上一次创建的函数实例。useCallback 结合React.Memo进行使用。
const childFucntion = useCallback(() => {
action()
}, [a, b])
使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。例如: 子组件如下
const child = memo(() => {
return <div>
我是子组件
</div>
})
父组建如下:
const parent = props => {
const [num, setNum] = useState(0);
const getValue = value => {
console.log(value);
};
const changeState = () => {
setNum(pre => {
return pre + 1;
});
};
return (
<div>
我是父组件
<Button onClick={changeState}>点我改变state</Button>
{num}
<child getValue={getValue} />
</div>
);
};
当点击去改变number的值时,虽然num和child没有任何关系,但是child依然会重新渲染,这很明显造成了性能浪费。更新的原因就是 React.memo 检测的是props中数据的栈地址是否改变。当你去改变父组件中的state,就会导致父组件重新构建,而父组件重新构建的时候,会重新构建父组件中的所有函数(旧函数销毁,新函数创建,等于更新了函数地址),新的函数地址传入到子组件中被props检测到栈地址更新。也就引发了子组件的重新渲染。
解决办法就是用useCallback包裹一下要传入child组件的函数。
const parent = props => {
const [num, setNum] = useState(0);
const getValue = value => {
console.log(value);
};
const changeState = useCallback(() => {
setNum(val => val + 1);
}, []);
return (
<div>
我是父组件
<Button onClick={changeState}>点我改变state</Button>
{num}
<child getValue={getValue} />
</div>
);
};
useCallback 源码
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
其中areHookInputsEqual跟 useMemo一样。
总结
useCallBack不要随意使用,不假思索的对每个方法增加useCallBack不要随意使用会造成不必要的性能浪费,useCallBack本身就是需要一定性能的
useCallBack并不能阻止函数重新创建,它只能通过依赖决定返回新的函数还是旧的函数,从而在依赖不变的情况下保证函数地址不变
useCallBack需要配合React.memo使用
`useMemo会执行回调函数并且返回结果,但是useCallback不会执行回调函数。
转载自:https://juejin.cn/post/7253980320357269561