前言
将useMemo和useCallback放在一起来说,是因为这两个Hook都具有缓存效果,它们的返回值在依赖没有变化时总是返回旧值,通常作为优化手段来使用,特别是一些高性能的计算。useMemo类似于Vue中computed的作用,useEffect类似于Vue watch的作用,useMemo和useEffect的使用场景也可以类比。本文会梳理useMemo和useCallback的执行逻辑,加深对它们的认知和理解。
useMemo
该hook会返回一个Memoized值,接受两个参数:
1
2const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值,这种优化有助于避免在每次渲染时都进行高开销的计算。useMemo会在渲染期间执行,如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。接下来就通过源码来看看useMemo的具体逻辑,按照初始化阶段和更新阶段来看。
初始化阶段useMemo执行逻辑
和之前useEffect、useState的初始逻辑相似调用dispatcher对象上同名方法,具体逻辑如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function useMemo() { ... checkDepsAreArrayDev(deps); ... return mountMemo() ... } function mountMemo(nextCreate, deps) { var hook = mountWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; var nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
检查依赖项是否是数组,之后调用mountMemo函数,而该函数中会立即执行传入给useMemo的函数,得到初始返回值,并且同依赖项一起保存到hook对象中。从上面的逻辑中实际上已知了,传递给useMemo的函数在初始化阶段函数组件调用时就同步执行了。
更新阶段useMemo的处理逻辑
更新阶段也是用过改变dispatcher对象来调用不同的方法,useMemo在更新阶段实际上是执行updateMemo函数,该函数的处理逻辑具体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function updateMemo(nextCreate, deps) { // hook对象 var hook = updateWorkInProgressHook(); // 最新值的依赖项 var nextDeps = deps === undefined ? null : deps; var prevState = hook.memoizedState; if (prevState !== null) { // Assume these are defined. If they're not, areHookInputsEqual will warn. if (nextDeps !== null) { var prevDeps = prevState[1]; // 比较依赖项是否发生了变化 if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } var nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; }
上面逻辑还是非常清晰的:
对于依赖项没有变化的直接返回旧值,需要注意的是使用Object.is进行浅层比较。依赖项存在变化的话,就会立即执行useMemo的函数并记录并返回新值。
为什么说useMemo是在渲染期间运行呢?通过上面的逻辑实际上可知其是在函数调用时同步执行的,而函数组件调用以及class组件处理就是在视图渲染时处理的,都是通过performSyncWorkOnRoot这个入口开启的。
所以不要在useMemo函数内部执行与渲染无关的操作,诸如副作用这类的操作。React官方的建议是先编写在没有 useMemo 的情况下也可以执行的代码,之后再在你的代码中添加 useMemo,以达到优化性能的目的。
useCallback
useCallback有些类似于useMemo,useCallback也是返回memorized值,只不过该值是函数。而useMemo的返回值没有限制返回值的类型,所以useMemo也可以返回一个函数,即useCllback(fn, deps) 相当于useMemo(() => fn,deps)。把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
useCallback处理在挂载阶段和更新阶段是通过改变dispatcher来实现的,这个逻辑和之前分析的其他的hook处理逻辑是相同的,所以还是按照初始化阶段和更新阶段来具体看看。
初始化阶段的useCallback处理逻辑
初始化阶段useCallback的逻辑主要是执行mountCallback函数,而该函数的主要处理如下:
1
2
3
4
5
6
7function mountCallback(callback, deps) { var hook = mountWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; hook.memoizedState = [callback, nextDeps]; return callback; }
创建hook对象保存相关的函数和依赖项到指定属性中
更新阶段的useCallback处理逻辑
更新阶段useCallback的处理逻辑主要是执行updateCallback函数,该函数的处理逻辑如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function updateCallback(callback, deps) { var hook = updateWorkInProgressHook(); var nextDeps = deps === undefined ? null : deps; var prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { var prevDeps = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } hook.memoizedState = [callback, nextDeps]; return callback; }
判断依赖是否更新了,主要还是Object.is的浅层比较,如果没有更新就返回旧的函数,整个的处理逻辑非常清晰。而返回后的callback的处理完全取决于开发者在哪里使用的问题。
总结
本文梳理了useMemo和useCallback的逻辑,总结如下知识点:
- useMemo作为一种性能优化的手段,会在依赖更新后计算相关值,类似于Vue中computed的功能,React官网也有对其使用的建议,先编写在没有 useMemo 的情况下也可以执行的代码,之后再在你的代码中添加 useMemo,以达到优化性能的目的,即优化是有针对性的
- useMemo会在渲染阶段执行,即函数调用时同步执行,所以要注意useMemo内部函数是不是存在构成渲染循环的逻辑
- useCallback是返回memorized回调函数,有两种常见场景:
- 当把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用
- 函数组件使用React.memo,如果传递函数,那么每次都是新函数就不起作用了,所以需要useCallback
最后
以上就是热心高山最近收集整理的关于React v16源码之useMemo与useCallback的全部内容,更多相关React内容请搜索靠谱客的其他文章。
发表评论 取消回复