0%

react18.2hook之useCallback和useMemo原理

useCallbackuseMemo 是一样的东西,只是入参有所不同。

useCallback 缓存的是回调函数,如果依赖项没有更新,就会使用缓存的回调函数

useMemo 缓存的是回调函数的 return,如果依赖项没有更新,就会使用缓存的 return

官网有这样一段描述,换言之 useCallback(fn, dependencies) 相当于 useMemo(() => fn, dependencies)

1
2
3
4
// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}

useCallback

useCallback 允许你在组件渲染之间保存 函数定义。

1
const cachedFn = useCallback(fn, dependencies)

参数

  1. fn :想要缓存的函数。此函数可以接受任何参数并且返回任何值。
    1. React 将会在初次渲染⽽⾮调⽤时返回该函数。
    2. 当进⾏下⼀次渲染时,如果 dependencies 相⽐于上⼀次渲染时没有改变,那么 React 将会返回相同的函数。
    3. 否则,React 将返回在最新⼀次渲染中传⼊的函数,并且将其缓存以便之后使⽤。
    4. React 不会调⽤此函数,⽽是返回此函数。你可以⾃⼰决定何时调⽤以及是否调⽤。
  2. dependencies :有关是否更新 fn 的所有响应式值的⼀个列表。
    1. 响应式值包括 propsstate,和所有在你组件内部直接声明的变量和函数。
    2. 依赖列表必须具有确切数量的项,并且必须像 [dep1, dep2, dep3] 这样编写。
    3. React 使⽤ Object.is ⽐较每⼀个依赖和它的之前的值。

返回值

在初次渲染时, useCallback 返回你已经传⼊的 fn 函数

在之后的渲染中, 如果依赖没有改变, useCallback 返回上⼀次渲染中缓存的 fn 函数;否则返回这⼀次渲染传⼊的 fn

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}

updateCallback 代码意图也很简单,如果依赖数组 deps 没有变化,或者 deps=[] 的情况下,会返回之前缓存的回调函数,否则就更新对应 fiber.memoizedState.hook.memoizedState 并返回新的回调函数。

useMemo

useMemo 是每次重新渲染的时候能够缓存计算结果的Hook。

1
const cachedValue = useMemo(calculateValue, dependencies)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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;
}

function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
// 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;
}

使用场景总结

  1. useCallBack 不要每个函数都包一下,否则就会变成反向优化。
    1. useCallBack 本身就是需要一定性能的
    2. 其次 useCallBack 并不能阻止函数重新创建, 由于使用 useCallback 时,函数会作为实参传给 useCallback,所以无论怎样 useCallback 包裹的函数都是会重新创建的,只是当 useCallback 的依赖没有改变时返回的是缓存中的函数而已
  2. useMemo 的关键是权衡。其目的是避免不必要的计算,但也要注意不要滥用,因为维持这些缓存值也是有开销的。最佳的做法是先写出清晰和可读的代码,然后在性能瓶颈出现时,再考虑优化。
    1. 如果函数组件中某个值需要大量的计算才能得出,或者渲染某个组件时有昂贵的计算,都可以考虑使用 useMemo 进行包裹
    2. 当值作为别的 hooks 的依赖时,可以考虑使用
  3. 如果某个函数或者值是传递给子组件的 props,可以考虑使用 useCallback 或者 useMemo 进行包裹(配合 React.memo 使用)