react中的useCallback内部实现
简介
前几天有人问我在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
的实现
- 通过
mountWorkInProgressHook
获取到对应的Hook对象 - 判断条件deps是否为undefined
- 将
回调函数
和判断条件
存入到hook.memoizedState
- 返回传入的回调函数
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
快速标记代码位置,实现快速条件
参考文献:
Understanding the Closure Trap of React Hooks React技术揭秘 build-your-own-react
转载自:https://juejin.cn/post/7251501699120922661