说明#
- useCallback 与 useMemo 作为优化性的 hooks,可以帮助减少 react 组件内不必要的渲染与计算,但本文只分析指两个 hooks 的实现,不讲解如何使用,版本基于 v18.1.0
- 阅读本文需先阅读 React Hooks: hooks 链表
- 和其他大多数 hooks 一样,每个 hooks 都按照 mount 与 update 两个阶段讲解
- 分析基于以下代码
import { useCallback, useEffect, useMemo, useState } from "react";
function UseCallbackAndUseMemo() {
const [a, setA] = useState('a');
const [b] = useState('b');
const memoizedCallback = useCallback(
() => {
console.log(a, b);
},
[a, b],
);
const memoizedResult = useMemo(() => {
return {
a,
b
}
}, [a, b]);
useEffect(() => {
setTimeout(() => {
setA('aaa');
}, 3000);
}, []);
useEffect(() => {
console.log(memoizedResult, 'memoizedResult change');
}, [
memoizedResult
]);
return (
<div onClick={ memoizedCallback }>click here</div>
)
}
export default UseCallbackAndUseMemo;
useCallback#
mount 阶段的 useCallback#
对 useCallback 打上断点,会进入 mountCallback
来康康里面是什么东西
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 生成 hook 对象的,见 hook 链表内的分析
const hook = mountWorkInProgressHook();
// 赋值
const nextDeps = deps === undefined ? null : deps;
// 把函数和依赖存到 hook 对象的 memoizedState 上
hook.memoizedState = [callback, nextDeps];
// 返回函数
return callback;
}
没了。。。那就再看看 update 干了啥
update 阶段的 useCallback#
保持断点不变,等定时器执行,随后进入 updateCallback
来康康里面是什么东西
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
// 移动 hooks 链表的指针,让指针指向当前 useCallback 对应的 hook 对象上
const hook = updateWorkInProgressHook();
// 获取 update 阶段传入的依赖
const nextDeps = deps === undefined ? null : deps;
// 获取上一次存在 hook 对象上的 [callback, deps]
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
/**
* prevState 不为空时(不过也不可能为空啊。。)
* nextDeps 也不为空时进入
*/
// 获取 memoizedState 内的 依赖项(deps)
const prevDeps: Array<mixed> | null = prevState[1];
// 比较前后依赖是否相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 相同就把上一次缓存的函数 return 出去
return prevState[0];
}
}
}
// 如果上述条件不满足,就更新 memoizedState 内的数据
hook.memoizedState = [callback, nextDeps];
// 返回函数
return callback;
}
大致上就是通过比较前后依赖项来决定是否复用上一次缓存的函数,一个类似闭包的实现。举例来解释的话,类似下面的代码。
function exampleA(a) {
let number = a;
return function example() {
return a + 2
}
}
function exampleB(b) {
let number = b;
return function example() {
return b + 2
}
}
const func1 = exampleA(2);
const func2 = exampleB(2);
当两个函数都传入同一个值时,虽然都会得到执行结果都为 4 的函数,但是 func1 与 func2 绝不是同一个函数。如果传入到 react 的组件内必然会导致 props 发生变化从而导致重新渲染组件,但即使重新渲染后,页面也不会发生变化。所以为了防止出现没有必要的重渲染,索性就用了执行结果相同的缓存函数。
接下来看看 areHookInputsEqual 是怎么比较前后依赖的
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
return false;
}
// 同时遍历两个数组,然后一一比较
// is 是个浅比较 ===
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
useCallback 总结#
useCallback 通过将函数与依赖存入到对应的 hook 对象上,在更新时会通过对前后依赖进行一一的浅比较来判断函数是否可以复用,从而来实现复用函数,避免组件发生不必要的更新。
useMemo#
mount 阶段的 useMemo#
对 useMemo 打点,会进入 mountMemo
/**
* 和 useCallBack 的 mount 阶段几乎一样的逻辑
*/
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 唯一的不同在这里,useMemo 存的是函数执行后的结果
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
update 阶段的 useMemo#
保持断点不变,等定时器执行,随后进入 updateMemo
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;
}
还是和 useCallback 的 update 阶段一模一样的逻辑。不讲了。
useMemo 总结#
useMemo 与 useCallback 的实现几乎一致,唯一的不同是,useMemo 存储函数运行后的结果,useCallback 则是存储函数本身。