背景
初入 React Hooks 的小伙伴可能比较疑惑,为什么 useCallback
这个 Hook
每次写一个都要传入相应的 deeps 呢?,简直不要太麻烦了。
源码阅读
跟前面一篇文章里提到的类似,useCallback 也是用链表来进行存储和和初始化的。
mountCallback
mount
阶段,会执行 mountCallback
,它本身需要传入两个参数:
callback
:实际需要执行的函数deps
: 函数执行时需要传入的依赖
typescript
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
可以看到,mountCallback
内还会执行 mountWorkInProgressHook
来生成链表,拿到的 hook 其实就是链表中的某个节点。 最终,会把我们传入的 callback
和 deps
都绑定在 hook.memoizedState
上。
updateCallback
update
阶段,会执行 updateWorkInprogressHook
来更新链表,之后,会拿 nextDeps
和 prevDeps
来进行对比:
typescript
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
): boolean {
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(', ')}]`,
);
}
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
代码很简单,其实就是对 prevDeps
和 nextDeps
做了一层浅比较:
- 相等,则返回之前缓存的
callback
- 不相等,则返回当前定义的
callback
完整代码:
typescript
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 (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
所以可以看出, useCallback
就是对函数做了一层缓存,deps 在比较时是做的浅比较。
总结
useCallback 为啥要传 deps
React Hook 的设计如此,为了减少 re-render 下 callback
的创建次数。
- 优点:传入
deps
后,如果render
前后两次的deps
没有变化,那么会复用上一次创建的callback
,节省函数创建的开销,也是典型的空间换时间的做法。 - 缺点:
deps
有心智负担,容易造成死循环。遇到deps
死循环时,可以考虑用useRef
来解决。- 所有
callback
都使用useCallback
包一层,也会有额外的内存开销,毕竟callback
会一直存在链表上的某个节点里
最佳实践
不要直接使用 useCallback
,需要的时候再用它:
- 不使用的场景:比如直接在
render
内使用的函数,其实就没太大必要包一层useCallback
- 使用的场景:比如在
useEffect
或者其他hook
中用到了这个函数,就可以考虑用useCallback
套一层。否则会因为每次渲染时callback
都是一个新引用而导致重复执行 hook。