您的位置:首页 > 财经 > 金融 > 【React Hooks原理 - useCallback、useMemo】

【React Hooks原理 - useCallback、useMemo】

2025/4/19 7:54:19 来源:https://blog.csdn.net/weixin_44273311/article/details/140211937  浏览:    关键词:【React Hooks原理 - useCallback、useMemo】

介绍

在实际项目中,useCallback、useMemo这两个Hooks想必会很常见,可能我们会处于性能考虑避免组件重复刷新而使用类似useCallback、useMemo来进行缓存。接下来我们会从源码和使用的角度来聊聊这两个hooks。【源码地址】

为什么要有这两个Hooks

在开始介绍之前我们先来了解下为什么有这两个hooks,其解决了什么问题?借用官网案例:

function ProductPage({ productId, referrer, theme }) {// 每当 theme 改变时,都会生成一个不同的函数function handleSubmit(orderDetails) {post('/product/' + productId + '/buy', {referrer,orderDetails,});}return (<div className={theme}>{/* 这将导致 ShippingForm props 永远都不会是相同的,并且每次它都会重新渲染 */}<ShippingForm onSubmit={handleSubmit} /></div>);
}

每当切换主题theme,ProductPage就会重新渲染,而即使ShippingForm使用memo包裹并且没有做任何更改也会重新渲染,这就是常说的父组件渲染导致子组件跟着渲染。
再看另一种情况:

function createOptions() {return {serverUrl: 'https://localhost:1234',roomId: roomId};}useEffect(() => {const options = createOptions();const connection = createConnection();connection.connect();}, [createOptions])

在useEffect中添加了createOptions作为依赖,但是createOptions函数每次执行都返回的不同函数导致useEffect会重新执行

所以为了解决类似上面两种问题,利用缓存封装了useCallback、useMemo等hooks。

useCallback

function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;

让我们带着上面这两个问题来了解useCallback。用白话来说useCallback就是接收一个callback和依赖deps,只要依赖的deps没有改变,通过useCallback返回的函数就是同一个,以此来避免重复刷新。如果deps改变则useCallback会返回新的callback并将其缓存,以便下次对比。

从源码来看几乎所有的Hooks都被拆分为了mount、upadte两种(useContext除外),React内部会根据当前渲染阶段来判断调用那个来处理callback

// 首次挂载时
const HooksDispatcherOnMount: Dispatcher = {readContext,use,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,useSyncExternalStore: mountSyncExternalStore,useId: mountId,
};// 渲染更新时
const HooksDispatcherOnUpdate: Dispatcher = {readContext,use,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,useSyncExternalStore: updateSyncExternalStore,useId: updateId,
};

以下会以useCallback为例,从源码上一步一步了解。

调用流程

从上面流程图能看出,当我们在组件内使用useCallback的时候,React会通过dispatcher根据渲染状态来进行不同的处理。

export function useCallback<T>(callback: T,deps: Array<mixed> | void | null,
): T {return useCallbackImpl(callback, deps);
}function useCallbackImpl<T>(callback: T,deps: Array<mixed> | void | null,
): T {const dispatcher = resolveDispatcher();return dispatcher.useCallback(callback, deps);
}

这里的dispatcher 是一个对象,它会在不同的渲染阶段指向不同的实现。在初次渲染时,它会指向 HooksDispatcherOnMount,在更新时,它会指向 HooksDispatcherOnUpdate。

mountCallback

当首次渲染时,会执行mountCallbac返回新的callback并将其和所依赖的deps缓存到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并绑定到当前渲染的fiber中
  • 获取依赖deps,并将callback和deps缓存到当前fiber的hook中

在Function Component中,每个fiber节点都有一个自己的副作用hook list,在协调器(Reconciler)的fiber构造的beginWork阶段会将当然fiber节点的hook保存在hook list中,详情可查看这篇文章:【React架构 - Fiber构造循环】

updateCallback

更新渲染时,会执行updateCallback函数,会根据依赖是否变化来判断是否使用缓存

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;
}

updateCallback主要做了下列事情:

  • 通过updateWorkInProgressHook获取当前fiber节点对应的hook,并通过hook.memoizedState获取缓存的callback和deps
  • 当依赖存在时,通过areHookInputsEqual判断deps是否变化,如果没变则返回缓存中的callback,即prevState[0],否则缓存新的callback和deps,然后返回新的callback

在areHookInputsEqual中主要是通过Object.is来判断deps是否变化

function areHookInputsEqual(nextDeps, prevDeps) {if (prevDeps === null) {return false;}// 简单的长度检查if (nextDeps.length !== prevDeps.length) {return false;}// 逐一比较每一个依赖项for (let i = 0; i < nextDeps.length; i++) {if (Object.is(nextDeps[i], prevDeps[i])) {continue;}return false;}return true;
}

Object.is() 与 == 运算符并不等价。== 运算符在测试相等性之前,会对两个操作数进行类型转换(如果它们不是相同的类型),这可能会导致一些非预期的行为,例如 “” == false 的结果是 true,但是 Object.is() 不会对其操作数进行类型转换。
Object.is() 也不等价于 === 运算符。Object.is() 和 === 之间的唯一区别在于它们处理带符号的 0 和 NaN 值的时候。=== 运算符(和 == 运算符)将数值 -0 和 +0 视为相等,但是会将 NaN 视为彼此不相等。详细查看MDN

useMemo

useCallback、useMemo都是处于性能考虑通过缓存来避免重复执行的hook,同useCallback一样,useMemo也接收两个参数callback、deps。其区别主要是:useCallback是缓存以及返回函数,并不会调用函数,而useMemo会执行函数,缓存并换回函数的执行结果
同其他hooks一样,useMemo也分为了mount和update两个,下面一一介绍。

mountMemo

function mountMemo<T>(nextCreate: () => T,deps: Array<mixed> | void | null,
): T {// 创建一个添加到Fiber节点上的Hooks链表const hook = mountWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;// 计算需要memo的值const nextValue = nextCreate();// hook数据对象上存的值hook.memoizedState = [nextValue, nextDeps];return nextValue;
}

初次渲染:

  • mountWorkInProgressHook: 会创建一个hook链表并绑定到当前渲染的fiber中
  • 执行传入的callback,并将其保存到memoizedState中

updateMemo

function updateMemo<T>(nextCreate: () => T,deps: Array<mixed> | void | null,
): T {// 找到该useMemo对应的hook数据对象const hook = updateWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;// 之前存的[nextValue, nextDeps]const prevState = hook.memoizedState;if (prevState !== null) {if (nextDeps !== null) {const prevDeps: Array<mixed> | null = prevState[1];// 判断依赖是否相等if (areHookInputsEqual(nextDeps, prevDeps)) {// 相等就返回上次的值return prevState[0];}}}// 不相等重新计算const nextValue = nextCreate();hook.memoizedState = [nextValue, nextDeps];return nextValue;
}

更新渲染:

  • 通过updateWorkInProgressHook获取当渲染fiber的hook链表
  • 根据areHookInputsEqual判断传入的依赖deps是否变化,如果变化则返回新的结果并缓存,否则使用缓存

总结

总的来说useMemo和useCallback相对来说源码比较简单,大致就是在首次渲染时,调用mountHook将callback/结果缓存到当前fiber节点的hoos链表(通过mountWorkInProgressHook创建)的memoizedState属性中,然后在更新渲染中获取当前fiber节点的hook信息(通过updateWorkInProgressHook获取),通过areHookInputsEqual判断是否使用缓存。

函数调用流程如下:
在这里插入图片描述
虽然useCallback、useMemo利用缓存避免了重复渲染,有利于性能优化,但是在实际项目中并不是所有的函数都需要用其包裹,大多情况下是没有意义的。主要场景就是上面提到的子组件更新和作为其他函数的依赖时:

  • 将其作为 props 传递给包装在 [memo] 中的组件。如果 props 未更改,则希望跳过重新渲染。缓存允许组件仅在依赖项更改时重新渲染。
  • 传递的函数可能作为某些 Hook 的依赖。比如,另一个包裹在 useCallback 中的函数依赖于它,或者依赖于 useEffect 中的函数。

当然如果能接受所有函数都被其包裹导致的代码可读性问题,这样记忆化处理也不会有什么问题。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com