详解react中useCallback内部是如何实现的
作者:AKclown
示例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的指针来获取到对应的HookPS: 只要每次组件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快速标记代码位置,实现快速条件

到此这篇关于详解react中useCallback内部是如何实现的的文章就介绍到这了,更多相关react useCallback内部实现内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
