「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」
react 版本:v17.0.3
1、Hook 入口
在 React Hooks 源码解读之Hook入口 一文中,我们介绍了 Hooks 的入口及hook处理函数的挂载,从 hook 处理函数的挂载关系我们可以得到这样的等式:
-
挂载阶段:
useCallback = ReactCurrentDispatcher.current.useCallback = HooksDispatcherOnMount.useCallback = mountCallback;
-
更新阶段:
useCallback = ReactCurrentDispatcher.current.useCallback = HooksDispatcherOnUpdate.useCallback = updateCallback;
因此,组件在挂载阶段,执行 useCallback,其实执行的是 mountCallback,而在更新阶段,则执行的是 updateCallback 。
2、挂载阶段
组件在挂载阶段,执行 useCallback,实际上执行的是 mountCallback,下面我们来看看 mountCallback 的实现。
2.1 mountCallback
// packages/react-reconciler/src/ReactFiberHooks.new.js
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表
const hook = mountWorkInProgressHook();
// 根据 useCallback 的第二个参数初始化 依赖项数组
const nextDeps = deps === undefined ? null : deps;
// 将 useCallback 的第一个参数 和相应的依赖项保存到 hook 对象的 memoizedState 属性上
hook.memoizedState = [callback, nextDeps];
// 返回的是useCallback 的参数 callback 本身
return callback;
}
组件在挂载时,执行 useCallback,首先会创建一个 hook 对象,该 hook 对象将会被添加到 workInProgressHook 单向链表中:
const hook = mountWorkInProgressHook();
接着根据 useCallback 的第二个参数 deps 来初始化需要保存起来的 依赖项数组:
const nextDeps = deps === undefined ? null : deps;
然后将 useCallback 的参数 callback 和相应的依赖项保存到 hook 对象的 memoizedState 属性上,并返回useCallback 的参数 callback:
hook.memoizedState = [callback, nextDeps];
// 返回的是useCallback 的参数 callback 本身
return callback;
mountCallback 和 mountMemo 的实现虽然几乎是一样的,但区别在于 useMemo 返回的是计算后的 memoized 值,而 useCallback 直接返回的是它的参数 callback 本身。
2.2 mountWorkInProgressHook
在 mountCallback 函数中,使用 mountWorkInProgressHook() 函数创建了一个新的 hook 对象,我们来看看它是如何被创建的:
// packages/react-reconciler/src/ReactFiberHooks.new.js
// 创建一个新的 hook 对象,并返回当前的 workInProgressHook 对象
// workInProgressHook 对象是全局对象,在 mountWorkInProgressHook 中首次初始化
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
// Hooks are stored as a linked list on the fiber's memoizedState field
// 将 新建的 hook 对象以链表的形式存储在当前的 fiber 节点memoizedState属性上
// 只有在第一次打开页面的时候,workInProgressHook 为空
if (workInProgressHook === null) {
// This is the first hook in the list
// 链表上的第一个 hook
// currentlyRenderingFiber: The work-in-progress fiber. I've named it differently to distinguish it fromthe work-in-progress hook.
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
// 已经存在 workInProgressHook 对象,则将新创建的这个 Hook 接在 workInProgressHook 的尾部,形成链表
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
可以看到,在新建一个 hook 对象时,如果全局的 workInProgressHook 对象不存在 (值为 null),即组件在首次渲染时,将新建的 hook 对象赋值给 workInProgressHook 对象,也同时将 hook 对象赋值给 currentlyRenderingFiber 的 memoizedState 属性;如果 workInProgressHook 对象存在,则将 hook 对象接在 workInProgressHook 的尾部,从而形成一个单向链表。
3、更新阶段
组件在更新阶段,执行 useCallback,实际上执行的是 updateCallback,下面我们来看看 updateCallback 的实现。
3.1 updateCallback
// packages/react-reconciler/src/ReactFiberHooks.new.js
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 获取该 useCallback 对应的hook 对象
const hook = updateWorkInProgressHook();
// 根据 useCallback 的第二个参数计算新的 依赖
const nextDeps = deps === undefined ? null : deps;
// 之前的 回调函数和依赖项 [callback, nextDeps]
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
// 比较前后的依赖是否相同
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 前后的依赖相同,说明不需要重新保存 callback 和 deps,而是直接返回上一次的 callback
return prevState[0];
}
}
}
// 前后的依赖数组不同或者没有提供依赖项数组,useCallback 在每次渲染时都会重新保存组件更新时重新创建的 callback 和 deps
hook.memoizedState = [callback, nextDeps];
// 直接useCallback 的参数 callback 本身
return callback;
}
在更新阶段,执行 useCallback,首先会调用 updateWorkInProgressHook() ,获取当前正在执行 update 任务的fiber 节点上的hook对象:
const hook = updateWorkInProgressHook();
接着获取新的依赖,方便与旧的依赖作比较,从而决定是否需要重新保存 useCallback的两个参数 callback 和 deps:
const nextDeps = deps === undefined ? null : deps;
然后从当前的 hook 对象中获取之前的依赖项数组,调用 areHookInputsEqual() 函数来比较前后的依赖项数组是否相同:
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
// 比较前后的依赖是否相同
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 前后的依赖相同,说明不需要重新保存 callback 和 deps,而是直接返回上一次的 callback
return prevState[0];
}
}
}
如果 areHookInputsEqual() 返回的结果为 true,说明前后的 依赖项数组 是一样的,说明不需要重新保存 useCallback的两个参数,而是直接返回上一次保存的 callback。
如果 areHookInputsEqual() 返回的结果为 false,则会执行下面的语句:
// 前后的依赖数组不同或者没有提供依赖项数组,useCallback 在每次渲染时都会重新保存组件更新时重新创建的 callback 和 deps
hook.memoizedState = [callback, nextDeps];
// 直接useCallback 的参数 callback 本身
return callback;
只要是前后的依赖项数组不同或者没有提供依赖项数组,useCallback 在每次渲染时都会执行上面的代码。因此,我们在使用 useCallback 的时候,应该传入依赖项数组,有助于避免非必要的渲染 。
3.2 updateWorkInProgressHook
在 updateCallback 函数中,通过 updateWorkInProgressHook() 函数获取到了当前正在工作中的 Hook,即 workInProgressHook,我们来看看 updateWorkInProgressHook 的实现:
// packages/react-reconciler/src/ReactFiberHooks.new.js
function updateWorkInProgressHook(): Hook {
// This function is used both for updates and for re-renders triggered by a
// render phase update. It assumes there is either a current hook we can
// clone, or a work-in-progress hook from a previous render pass that we can
// use as a base. When we reach the end of the base list, we must switch to
// the dispatcher used for mounts.
// 获取 当前 hook 的下一个 hook
let nextCurrentHook: null | Hook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
// 取下一个 hook 为当前的hook
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
// 拷贝当前的 hook,作为当前正在工作中的 workInProgressHook
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
这里分两种情况:
- 如果是在 render 阶段,则会取下一个 hook 作为当前的hook,并返回 workInProgressHook;
- 如果是在 re-render 阶段,则在当前处理周期中,继续取当前的 workInProgressHook 做更新处理,最后再返回 workInProgressHook。
前后的 依赖项数组 是否一样,是通过 areHookInputsEqual() 来判断的,我们来看看它的实现。
3.3 areHookInputsEqual
// packages/react-reconciler/src/ReactFiberHooks.new.js
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
// 删除了 dev 代码
if (prevDeps === null) {
// 删除了 dev 代码
return false;
}
// 删除了 dev 代码
// deps 是一个 Array,循环遍历去比较 array 中的每个 item
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// is比较函数是浅比较
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
在 areHookInputsEqual 函数中,循环遍历 deps ,调用 is 方法去比较依赖项数组中的每个依赖,值得注意的是,is 方法是浅比较,也就是说如果是深比较那一定会更新的。下面是 is 方法的源码:
// packages/shared/objectIs.js
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
const objectIs: (x: any, y: any) => boolean =
typeof Object.is === 'function' ? Object.is : is;
export default objectIs;
Object.is() 方法判断两个值是否为同一个值,若当前浏览器支持该方法,则调用该方法来判断前后两个依赖项是否相同,若不支持,则调用React自己实现 is 方法来比较。
4、useCallback 流程图
5、总结
我们知道 useCallback 将会返回其参数 callback 的 memoized 版本,该 callback 仅在某个依赖项改变时才会更新。虽然 useCallback 可以对数据进行缓存,但是也不能因此而滥用,我们应该考虑哪些数据值得去缓存,因为对于 useCallback 来说,对比依赖变化是需要时间的,我们应该对此进行衡量,才能够更好的去用 hook,而不是为了优化而优化。