React Hook实现原理之useCallback与useMemo
本章节将讲解两个关于性能优化的hook:useCallback与useMemo。
本节主要是介绍这两个hook的实现原理,关于函数组件更新优化的具体逻辑会在后面新的章节介绍。
1,useCallback
这个hook的作用是返回一个固定引用地址的函数,相当于缓存一个声明的函数,通常用它进行性能优化。
const cachedFn = useCallback(fn, dependencies)
import { useState, useCallback } from 'react'
export default function MyFun(props) {
console.log('MyFun组件运行了')
const [count, setCount] = useState(1)
const testFn = useCallback(() => {
console.log('测试useCallback')
}, [])
function handleClick() {
setCount(2)
}
return (
<div className='MyFun'>
<div>state: {count}</div>
<button onClick={handleClick}>更新</button>
</div>
)
}
下面我们开始解析它的实现原理。
加载阶段处理
在函数组件加载时,会调用一次函数MyFun,这时就会开始对我们定义的hook进行加载处理:
const HooksDispatcherOnMount: Dispatcher = {
useCallback: mountCallback,
}
查看mountCallback方法:
// packages\react-reconciler\src\ReactFiberHooks.new.js
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对象,这个对象会存储我们定义的hook相关信息,如果一个函数组件中使用了多个hook,则mountWorkInProgressHook方法内部会将它们按加载顺序构建成一个单向hook链表,存储到函数组件Fiber节点的memoizedState属性之上,如图所示:

这个对象有以下几个属性:
const hook: Hook = {
memoizedState: null, // 存储hook数据或者回调函数
baseState: null,
baseQueue: null,
queue: null,
next: null, // 指向下一个hook对象
};
这里我们只需要关注memoizedState属性,因为useCallback只使用了这个属性:
hook.memoizedState = [callback, nextDeps];
将hook对象的memoizedState属性设置为一个数组,它只有两位元素,存储的就是我们传入的callback回调函数和deps依赖。
return callback;
最后直接返回callback回调函数,useCallback hook的加载就处理完成,也是比较简单的。
更新阶段处理
在函数组件更新时,同样会调用一次函数MyFun,这时就会开始对我们定义的hook进行更新处理:
const HooksDispatcherOnUpdate: Dispatcher = {
useCallback: updateCallback,
}
查看updateCallback方法:
// packages\react-reconciler\src\ReactFiberHooks.new.js
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 = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
updateWorkInProgressHook方法是每个hook更新时都会调用的一个公用方法,它的作用是根据函数组件加载时形成的hook链表,找到该hook对应的原内部hook对象,复用该对象信息,创建新内部hook对象。
注意: 这里的重点就是复用了原内部hook对象的信息,因为前面我们已经知道,在加载阶段我们的传入callback回调函数存储在hook.memoizedState属性中,在我们复用了原对象信息后,就可以拿到原来的回调函数。
const prevState = hook.memoizedState; // [callback, nextDeps]
此时的prevState就是我们需要的内容,在我们拿到原数据之后,有一个依赖变化的判断,这个就是useCallback缓存的重点。
// 新的依赖数据
const nextDeps = deps === undefined ? null : deps;
// 旧的依赖数据
const prevDeps = prevState[1];
在确定新旧依赖数据之后,调用了一个areHookInputsEqual方法来判断依赖是否发生变化。
如果依赖没有发生变化,则返回true,表明新旧依赖相等,则直接返回原callback回调函数。
return prevState[0];
这就是useCallback hook的缓存功能的实现,它的本质就是将callback回调函数存储到内部hook对象的memoizedState属性之上,然后这个hook对象会存储到函数组件的Fiber节点上,只要函数组件一直存在,则此callback会被一直缓存。
以上就是useCallback的实现原理,也是比较简单的。
areHookInputsEqual
最后我们再来看一下比较依赖变化的方法areHookInputsEqual,其实它的逻辑也非常简单,我们一看便知。
查看areHookInputsEqual方法:
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
// 情况1,无依赖参数,每次渲染都会执行副作用
if (prevDeps === null) {
return false;
}
// 情况2,有至少一项依赖参数,循环判断每个依赖是否相等,任何一个依赖变化则会重新执行副作用
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
// 情况3,参数为空数组或者所有依赖都没有变化的情况,重新渲染不执行副作用
return true;
}
根据此方法校验逻辑,可以分为以下三种情况:
情况一: 如果prevDeps为null,代表没有依赖参数,此时直接返回false,则函数组件每次渲染之后,都会执行此副作用回调。
情况二: 参数存在且有至少一个依赖项,则循环每个依赖,使用Object.is判断新旧依赖是否变化,任何一个依赖变化都会返回false,则本次更新后会执行副作用回调。如果都没有变化,最终会返回true,则不会执行副作用回调。
情况三: 参数为空数组或者所有依赖都没有变化的情况,直接返回true,组件更新不会执行副作用回调。
2,useMemo
这个hook的作用是在函数组件重新渲染的时候能够缓存计算的结果,通常用它进行性能优化。
const cachedValue = useMemo(calculateValue, dependencies)
useMemo用于保持一些比较稳定的数据,它的功能类似于Vue中的计算属性computed,依赖项没有变化,返回值就不会变化。
这个hook比useCallback更加强大,useCallback只是固定函数的引用,useMemo可以固定所有返回值的引用或者说结果。
import { useState, useMemo } from 'react'
export default function MyFun(props) {
console.log('MyFun组件运行了')
const [count, setCount] = useState(1)
// 缓存计算的结果
const countPx = useMemo(() => count + 'px', [count])
function handleClick() {
setCount(2)
}
return (
<div className='MyFun'>
<div>state: {count}</div>
<button onClick={handleClick}>更新</button>
</div>
)
}
- 当
useMemo返回一个计算结果时,它的效果与Vue中computed基本一致,它们的区别在于computed自动收集依赖,useMemo需要我们手动添加依赖。
const countPx = useMemo(() => count + 'px', [count])
const countPx = computed(() => count + 'px')
- 当
useMemo返回一个函数时,它的效果与useCallback基本一致。
const fn = useMemo(() => {
return () => {
console.log('useMemo')
}
}, [])
const fn = useCallback(() => {
console.log('useCallback')
}, [])
computed在需要传递参数时也可以返回一个函数,此时和useMemo功能完全一样。
下面我们开始解析它的实现原理。
加载阶段处理
在函数组件加载时,会调用一次函数MyFun,这时就会开始对我们定义的hook进行加载处理:
const HooksDispatcherOnMount: Dispatcher = {
useMemo: mountMemo,
}
查看mountMemo方法:
// packages\react-reconciler\src\ReactFiberHooks.new.js
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 执行回调函数,生成初始结果
const nextValue = nextCreate();
// 缓存初始结果
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
根据源码就可以看useMemo和useCallback的实现原理基本一致,唯一的区别就是:
useCallback是直接缓存的我们传入的回调函数。useMemo是执行一次我们传入的回调函数,缓存的是执行后的结果。
更新阶段处理
在函数组件更新时,同样会调用一次函数MyFun,这时就会开始对我们定义的hook进行更新处理:
const HooksDispatcherOnUpdate: Dispatcher = {
useCallback: updateMemo,
}
查看updateMemo方法:
// packages\react-reconciler\src\ReactFiberHooks.new.js
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];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
根据updateMemo源码可以看出,更新阶段useMemo和useCallback也是一样的实现原理。useMemo根据依赖是否变化来决定是否重新调用一次回调函数,缓存新的计算结果。区别就只是useMemo缓存回调函数计算后的结果,useCallback直接缓存回调函数。
最后我们再来做一个总结: 根据源码我们可以发现useCallback和useMemo实现原理是基本一样的。这种实现react框架中比较常见的,比如useState与useReducer,useEffect与useLayoutEffect等等,它们都是通过一个小的标识或者细节来区分两个API,而框架选择新增一个API而不是采用配置项,目的都是为了方便开发者的使用。
结束语
以上就是useCallback和useMemo实现原理的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!
转载自:https://juejin.cn/post/7283150911177621563