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