React Hooks 源码解读之 useMemo
react 版本:v17.0.3
1、Hook 入口
在 React Hooks 源码解读之Hook入口 一文中,我们介绍了 Hooks 的入口及hook处理函数的挂载,从 hook 处理函数的挂载关系我们可以得到这样的等式:
挂载阶段:
useMemo = ReactCurrentDispatcher.current.useMemo = HooksDispatcherOnMount.useMemo = mountMemo;
更新阶段:
useMemo = ReactCurrentDispatcher.current.useMemo = HooksDispatcherOnUpdate.useMemo = updateMemo;
因此,组件在挂载阶段,执行 useMemo,其实执行的是 mountMemo,而更新阶段,则执行的是updateMemo 。
2、挂载阶段
组件在挂载阶段,执行 useMemo,实际上执行的是 mountMemo,下面我们来看看 mountMemo 的实现:
2.1 mountMemo
// packages/react-reconciler/src/ReactFiberHooks.new.js function mountMemo<T>( nextCreate: () => T, // useMemo 的第一个参数:callback deps: Array<mixed> | void | null, // useMemo 的第二个参数:依赖项数组 ): T { // 创建 hook 对象,将 hook 对象添加到 workInProgressHook 单向链表中,返回最新的 hook 链表 const hook = mountWorkInProgressHook(); // 根据 useMemo 的第二个参数初始化 依赖项数组 const nextDeps = deps === undefined ? null : deps; // 执行 useMemo 的第一个参数 callback,计算需要缓存的值 const nextValue = nextCreate(); // 将计算后的值和相应的依赖项保存到 hook 对象的 memoizedState 属性上 hook.memoizedState = [nextValue, nextDeps]; // 返回缓存后的值 return nextValue; }复制代码
组件在挂载时,执行 useMemo,首先会创建一个 hook 对象,该 hook 对象将会被添加到 workInProgressHook 单向链表中:
const hook = mountWorkInProgressHook();复制代码
接着根据 useMemo 的第二个参数 deps 来初始化需要保存起来的 依赖项数组:
const nextDeps = deps === undefined ? null : deps;复制代码
然后执行 useMemo 的第一个参数 callback,计算需要缓存起来的值:
const nextValue = nextCreate();复制代码
最后将计算的值和相应的依赖项保存到 hook 对象的 memoizedState 属性上,并返回缓存后的值:
// 将计算后的值和相应的依赖项保存到 hook 对象的 memoizedState 属性上 hook.memoizedState = [nextValue, nextDeps]; // 返回缓存后的值 return nextValue;复制代码
由此可知道,useMemo 的 memoizedState 存储的并不是一个具体的值,而是一个包含有缓存值和依赖项的数组。
2.2 mountWorkInProgressHook
在 mountMemo() 函数中,使用 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 不为 null,则将 hook 对象接在 workInProgressHook 的尾部,从而形成一个单向链表。
3、更新阶段
组件在更新阶段,执行 useMemo,实际上执行的是 updateMemo,下面我们来看看 updateMemo 的实现。
3.1 updateMemo
// packages/react-reconciler/src/ReactFiberHooks.new.js function updateMemo<T>( nextCreate: () => T, // useMemo 的第一个参数:callback deps: Array<mixed> | void | null, // useMemo 的第二个参数:依赖项数组 ): T { // 找到该useMemo对应的hook 对象 const hook = updateWorkInProgressHook(); // 根据 useMemo 的第二个参数计算新的 依赖 const nextDeps = deps === undefined ? null : deps; // 之前的 缓存值和依赖项 [nextValue, nextDeps] 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)) { // 前后的依赖相同,说明不需要重新执行 nextCreate 计算新值,而是直接返回上一次计算的值 return prevState[0]; } } } // 前后的依赖数组不同或者没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值 // 执行 useMemo 的第一个参数 callback,计算新的值 const nextValue = nextCreate(); // 将计算后的新值和相应的依赖项保存到 hook 对象的 memoizedState 属性上 hook.memoizedState = [nextValue, nextDeps]; // // 返回计算后的新值 return nextValue; }复制代码
在更新阶段,执行 useMemo,首先会调用 updateWorkInProgressHook() ,获取当前正在执行 update 任务的fiber 节点上的hook 对象:
const hook = updateWorkInProgressHook();复制代码
接着获取新的依赖,方便与旧的依赖做比较,从而决定是否需要重新执行 useMemo 的第一个参数,计算新的值:
const nextDeps = deps === undefined ? null : deps;复制代码
然后从当前的 hook 对象中获取之前的依赖项数组,调用 areHookInputsEqual() 来比较前后的依赖项数组是否相同:
// 之前的 缓存值和依赖项 [nextValue, nextDeps] 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)) { // 前后的依赖相同,说明不需要重新执行 nextCreate 计算新值,而是直接返回上一次计算的值 return prevState[0]; } } }复制代码
如果 areHookInputsEqual() 返回的结果为 true,说明前后的 依赖项数组 是一样的,说明不需要重新执行 nextCreate 计算新值,而是直接返回上一次计算的值。
如果 areHookInputsEqual() 返回的结果为 false,则会执行下面的语句:
// 前后的依赖数组不同或者没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值 // 执行 useMemo 的第一个参数 callback,计算新的值 const nextValue = nextCreate(); // 将计算后的新值和相应的依赖项保存到 hook 对象的 memoizedState 属性上 hook.memoizedState = [nextValue, nextDeps]; // // 返回计算后的新值 return nextValue;复制代码
只要是前后的依赖项数组不同或者没有提供依赖项数组,useMemo 在每次渲染时都会执行上面的代码计算新值。因此,我们在使用 useMemo 的时候,应该传入依赖项数组,有助于避免每次渲染时都进行高开销的计算。
3.2 updateWorkInProgressHook
在 updateMemo() 函数中,通过 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、useMemo 流程图
5、总结
我们知道 useMemo 将会返回计算后的 memoized 值,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。虽然 useMemo 可以对数据进行缓存,但是也不能因此而滥用,我们应该考虑哪些数据值得去缓存,因为对于 useMemo 来说,除了计算值耗时以外,对比依赖项的比较也是需要时间的,我们应该对此进行衡量,才能够更好的去用 hook,而不是为了优化而优化。
作者:紫圣
链接:https://juejin.cn/post/7028282456956469279