question
为什么使用useCallback 和useMemo返回的memoized值可以起到性能优化的作用。
源码分析
- 我们在packages\react\src\ReactHooks.js可以找到对应的hook
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, deps);
}
export function useMemo<T>(
create: () => T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useMemo(create, deps);
}
- 在这里我们可以看到其useCallback,useMemo是调用dispatcher对应的方法, 而dispatcher是由resolveDispatcher()生成。而resolveDispatcher()中的dispatcher 是由ReactCurrentDispatcher.current生成。因此我们需要查看ReactCurrentDispatcher.current的数据是在哪里挂载的。
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
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://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
}
//如果在渲染阶段之外访问,将导致空访问错误。我们
//故意不抛出我们自己的错误,因为这是在热路径中。
//也有助于确保这是内联的。
return ((dispatcher: any): Dispatcher);
}
- 在packages\react-reconciler\src\ReactFiberHooks.js中的renderWithHooks对ReactCurrentDispatcher.current进行数据挂载这里只截取对应代码
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
//TODO 如果挂载期间根本没有使用挂钩,则发出警告,然后在更新期间使用一些挂钩。
//目前我们将更新渲染识别为一个装载,因为 memoizedState === null。
//这很棘手,因为它对某些类型的组件有效(例如 React.lazy)
//使用 memoizedState 来区分挂载/更新只有在至少使用一个有状态钩子的情况下才有效。
//非状态挂钩(例如上下文)不会添加到 memoizedState,
//所以 memoizedState 在更新和挂载期间将为 null。
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
}
- 因此当dispatcher.useCallback和dispatcher.useMemo实际调用的是会根据是否是初次渲染调用对应的HooksDispatcherOnMount和HooksDispatcherOnUpdate
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useId: updateId,
};
- HooksDispatcherOnMount和HooksDispatcherOnUpdate调用对应的方法,这里可以看到useCallback和useMemo会都会被hook.memoizedState缓存,区别就一个缓存的是函数,一个缓存的是值。对 hook.memoizedState查看我们可以了解到mountWorkInProgressHook和updateWorkInProgressHook生成,
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;
}
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 (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate();
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
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)) {
return prevState[0];
}
}
}
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate();
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
- mountWorkInProgressHook workInProgressHook = workInProgressHook.next = hook 根据这段代码我们可以了解到一个函数组件的所有hook函数会形成一个hooks链表,而workInProgressHook 指针会指向最后一个节点。这个hooks链表是存放在currentlyRenderingFiber.memoizedState
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
//这是列表中的第一个钩子
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
//追加到列表的末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
- updateWorkInProgressHook workInProgress 树中的 fiber 节点的下一个 hook 存在,链表直接指向节点,否则就从对应的 current 的 fiber 节点克隆过来,然后把这些 hook 构建出新的链表放到 currentlyRenderingFiber.memoizedState 上,方便下次更新时使用;
function updateWorkInProgressHook(): Hook {
//此函数用于更新和由 a 触发的重新渲染
//渲染阶段更新。它假设有一个当前的钩子我们可以
//克隆,或者我们可以从之前的渲染过程中进行的工作钩子
//用作基础。当我们到达基本列表的末尾时,我们必须切换到
//用于挂载的调度程序。
let nextCurrentHook: null | Hook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
//已经有一个正在进行的工作。重复使用它。
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
//从当前钩子克隆。
if (nextCurrentHook === null) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
//这是初始渲染。当组件到达此分支
//挂起,恢复,然后呈现一个额外的钩子。
const newHook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
nextCurrentHook = newHook;
} else {
// This is an update. We should always have a current hook.
throw new Error('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) {
//这是列表中的第一个钩子。
// 若这是链表的第一个hook节点,则使用 currentlyRenderingFiber.memoizedState 指针指向到该hook
// currentlyRenderingFiber 是在 renderWithHooks() 中赋值的,是当前函数组件对应的fiber节点
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
//附加到列表的末尾。
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
总结
之所以useCallback和useMemo可以达到性能优化的效果,是因为我们在初始节点已经把所有的 hooks 都挂载在链表中了,在更新阶段,所有的 hooks 都会进入到 update 阶段,比如 useState()内部会执行 updateState(),useEffect()内部会执行 updateEffect()等。二这些 hooks 的 update 阶段执行的函数里,都会执行函数 updateWorkInProgressHook()。
updateWorkInProgressHook()函数的作用,就是从 hooks 的链表中获取到当前位置,上次渲染后和本次将要渲染的两个 hook 节点:
currentHook: current 树中的那个 hook;即当前正在使用的那个 hook;
workInProgressHook: workInProgress 树中的那个 hook,即将要执行的 hook;
当通过对比两个hook,当依赖项没有发生改变时,workInProgressHook链表的指针会直接指向之前的fiber节点的地址。而不会重新创建。因此达到性能优化的效果
Top comments (0)