简介
前几天有人问我在useCallback函数如果第二个参数为空数组, 为什么拿不到最新的state值。正好自己也想多了解一下react底层实现。那么这一章就来分析一下useCallback内部是如何实现的。
示例demo与debug
新建了一个react项目,将APP.tsx改写成如下代码
import { useCallback, useState } from 'react'; function App() { const [num, updateNum] = useState(0); const TestCallback = useCallback(() =>{ console.log('num: ', num); },[]); return ( <div className="App"> <p onClick={() => { updateNum(num => num + 1); updateNum(num => num + 1); updateNum(num => num + 1); }}>{num}</p> <p onClick={TestCallback}>打印</p> </div> ); } export default App;
在浏览器的source设置断点,熟悉一遍useCallback的调用流程。(由于.gif过大,这里就不上git了,自行调试)
源码解析
useCallback的整体流程框架
在react中mount阶段和update阶段进入到同一个useCallback方法里。但resolveDispatcher找到的dispatch对象mount和update会不同,最终导致在mount阶段调用mountCallback而update阶段调用的是updateCallback。
下面为调用useCallback方法触发的行为
function useCallback(callback, deps) { var dispatcher = resolveDispatcher(); return dispatcher.useCallback(callback, deps); }
下面来看看resolveDispatcher是如何获取到dispatch的
function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current; ... return ((dispatcher: any): Dispatcher); }
ReactCurrentDispatcher.current会在renderWithHooks方法中进行所处阶段判断并且赋值。如果current === null || current.memoizedState === null为true表示在mount阶段反正为update阶段
function renderWithHooks<Props, SecondArg>(...) { ... ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; } // mount阶段调用的dispatch const HooksDispatcherOnMount: Dispatcher = { ... useCallback: mountCallback, }; // update阶段调用的dispatch const HooksDispatcherOnUpdate: Dispatcher = { ... useCallback: updateCallback, };
从上面的代码分析可以知道在mounted阶段调用的是mountCallback在update阶段调用updateCallback
Hook
一个函数式组件链路: fiber(FunctionComponent) => Hook(保存数据状态) => Queue(更新的队列结构) => update(更新的数据)
在后续需要使用到Hook这个结构,那么先来看一下Hook是数据结构是怎么样的,以及属性的作用是什么?
memoizedState 存放的是Hook对应的state
next链接到下一个Hook,从而形成一个无环单向链表
queue存储同一个hook更新的多个update对象,数据结构为环状单向链表
// 组件对应的fiber对象 const fiber = { // 保存该FunctionComponent对应的Hooks链表 memoizedState: hook, ... }; const hook: Hook = { // 1. memoizedState 存放的是Hook对应的state memoizedState: null, // 2. next链接到下一个Hook,从而形成一个`无环单向链表` queue: null, // 3. queue存储同一个hook更新的多个update对象,数据结构为`环状单向链表` next: null, ... };
fiber与Hooks的关系(懒得画图了,引用了Understanding the Closure Trap of React Hooks)
mount阶段
分析mountCallback的实现
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; }
mountWorkInProgressHook的实现,创建初始化Hook对象,并且将该Hook对象保存在workInProgressHook链路中. workInProgressHook表示正在执行的hook
function mountWorkInProgressHook(): Hook { const hook: Hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { // Append to the end of the list workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }
在组件render时,每当遇到下一个Hook,通过移动workInProgressHook的指针来获取到对应的Hook
PS: 只要每次组件render时useState的调用顺序及数量保持一致,那么始终可以通过workInProgressHook找到当前useState对应的hook对象
// fiber.memoizedState标识第一个Hook workInProgressHook = fiber.memoizedState; // 在组件`render`时,遇到下一个hook时 workInProgressHook = workInProgressHook.next; ....
update阶段
分析updateCallback的实现
通过updateWorkInProgressHook获取到当前的Hook对象
hook.memoizedState获取到上一次缓存的state。假设这是第一次update那么其值就是mount阶段保存的[callback, nextDeps]数据
如果依赖条件不为空,使用areHookInputsEqual判断依赖项是否更改。只会遍历数组第一层数据比较不会做深层比较。如果依赖项没变化,返回原本缓存的callback。
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; }
依赖比较areHookInputsEqual的方法实现
function areHookInputsEqual( nextDeps: Array<mixed>, prevDeps: Array<mixed> | null, ): boolean { ... // $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; }
总结
在React中会使用闭包机制来处理上文的callback回调函数。当包含useCallback组件被渲染时,React 会为该特定渲染周期创建一个闭包。闭包是一个封装的作用域,其中包含渲染时位于作用域内的变量、函数和其他引用。
因此deps我们传入的是空数组,其回调函数callback一直引用的状态始终是初始状态,无法获取最新状态。缓存的回调函数可以访问最初调用时范围内的状态和道具
插件推荐
阅读源码可以通过使用Bookmarks快速标记代码位置,实现快速条件
发表评论 取消回复