本文将简要介绍useCallback的基础用法,源代码实现。
1. 函数用途
const cachedFn = useCallback(fn, dependencies)
- fn:要缓存的函数值。React 将在初始渲染期间将您的函数返回(不是调用)。在下一次渲染时,如果自上次渲染以来依赖项没有更改,React 将提供相同的函数。否则提供当前渲染期间传递的函数,并将其存储起来以备以后重用。
- 依赖项:fn 代码内部引用的所有响应值的数组。
- return: 在初始渲染中,
useCallback返回fn 函数,在后续渲染期间,返回上次渲染中已存储的 fn 函数(如果依赖项未更改),或者返回本次渲染期间传递的 fn 函数。
2. 源代码位置
该函数的源代码位于react包下的ReactHooks.js文件。代码如下:
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, deps);
}
代码讲解:
- 参数:该函数接受两个参数:回调函数
callback和一个数组。 - 然后调用
resolveDispatcher函数获得dispatcher。
resolveDispatcher函数如下:
function resolveDispatcher() {
const dispatcher = ReactSharedInternals.H;
if (__DEV__) {
if (dispatcher === null) {
console.error(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
}
// Will result in a null access error if accessed outside render phase. We
// intentionally don't throw our own error because this is in a hot path.
// Also helps ensure this is inlined.
return ((dispatcher: any): Dispatcher);
}
注意到 const dispatcher = ReactSharedInternals.H;说明dispatcher的值是从ReactSharedInternals中取得的。ReactSharedInternals是React内部定义的一个对象,其结构如下:
const ReactSharedInternals: SharedStateServer = ({
H: null,
A: null,
}: any);
3. renderWithHooks文件
- 那么
ReactSharedInternals.H的值是多少呢。根据追踪ReactSharedInternals.H的值发现,最终useCallback的值有两个函数:分别是updateCallback和mountCallback。 - 在
ReactFiberHooks.js文件的renderWithHooks函数中,对useCallback的两种情况进行了如下赋值,分为mountCallback和updateCallback。
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// [Not Native Code]
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// [Not Native Code]
}
3.1 mountCallback函数实现
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数据结构。
3.1.1 mountWorkInProgressHook函数实现
mountWorkInProgressHook函数如下:
function mountWorkInProgressHook(): Hook {
//首先创建了一个hook对象
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
解析:
workInProgressHook是指向当前正在渲染的 hook 列表中某个 hook 的指针.if (workInProgressHook === null)说明是第一个要渲染的hookcurrentlyRenderingFiber.memoizedState = workInProgressHook = hook;让currentlyRenderingFiber.memoizedState指向第一个hookworkInProgressHook = workInProgressHook.next = hook;如果不是第一个hook,就将其连接到hookList末尾- 最后返回
workInProgressHook指针,可以通过该指针接触并调用hooks。
3.1.2 hook.memoizedState作用解释
返回到mountCallback的实现,发现有一句hook.memoizedState = [callback, nextDeps];。hook.memoizedState 在 React 的 hooks 实现中起着重要的作用,它用于存储当前 hook 的状态值。具体来说,它的用途包括:
- 状态存储: memoizedState 存储 hook 的最新状态值,确保在组件重新渲染时能够保持和访问这个状态。
- 状态持久化: 当组件重新渲染时,React 会根据 memoizedState 的值来决定如何更新组件的状态,而不必重新计算状态或重新初始化。
这也是
useCallback避免重复渲染的关键
3.2 updateCallback的实现
更新时,目标是仅当一个依赖项发生更改时才为提供新的函数引用。
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// step 1
const hook = updateWorkInProgressHook();
// step 2
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
// step 3
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// step 4
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
// step 5
hook.memoizedState = [callback, nextDeps];
return callback;
}
解析:
const hook = updateWorkInProgressHook();创建或重用钩子数据结构对象。nextDeps = deps === undefined ? null : deps;推断依赖项数组,如果未提供任何内容,则返回 null。- 当依赖项不为 null 时,这意味着正在记忆,将继续将它们与之前的依赖项进行比较
areHookInputsEqual(nextDeps, prevDeps)。 - 比较前一个和后一个依赖项,如果相同,则返回前一个值
return prevState[0];(memoizedState 数组中的第一个元素)。 - 当依赖项发生变化时,与 mountCallback 类似,将
[callback, nextDeps]存储到钩子对象的 memoizedState 属性中。
areHookInputsEqual 函数用于所有使用依赖项数组的hooks。
当没有先前的依赖项时,总是返回 false,并且每次渲染都会刷新。如果有依赖项,循环两个数组并使用 Object.is 比较各个值。