本文探究useMemo和useCallback两个Hook究竟应该如何使用。
比较useMemo和useCallback
首先,useMemo和useCallback在函数的参数方面类似
useMemo(callback, deps)
useCallback(callback, deps)
两者的形式的确相似,第一个参数都是回调函数,第二个参数是依赖关系(useEffect的依赖关系),先解释依赖关系
- 当deps是null/undefined时候无论是挂载还是更新阶段都会返回新的值,所以在useMemo或useCallback中deps是null值是没有意义的!(但是对于useEffect是有意义的);
- 当deps是空数组[],则只在挂载阶段返回一次值;
- 当deps是变量列表时,如[a, b]则a或b中任何一个值发生改变则要返回新值。
这是useMemo和useCallback的相似之处,但实际上两个函数在参数的签名上不完全一样,第一个回调函数参数,useCallback是允许有参数的,但useMemo是没有参数的!
const memo = useMemo((a) => a+b, [b]) // 参数a没有机会传
const memo = useMemo(() => a+b, [a, b]) // 这样才可能是正常的
const handleClick = useCallback((e) => { // 这是很常见的
e.preventDefault()
...
}
useMemo和useCallback 的返回值不同
- useMemo返回的是回调函数运行后的返回值
- useCallback返回的是回调函数
可以把下面的代码视为同等
const handleClick = useCallback((page) => dispatch(fetchFeeds(page)), [dispatch])
const handleClick = useMemo(() => (page) => dispatch(fetchFeeds(page)), [dispatch])
这里用useMemo返回了一个function,和上一行useCallback做的事情一样,只不过通常我们并不会这样写。
useMemo要点
useMemo的主要作用在于如果组件产生一次新的渲染,只要缓存值的依赖关系没有变,就仍然返回上次缓存的值,节省了运算时间,并且保持计算值不变(有可能避免额外的子组件渲染)。
官网说明也需要注意
你可以把 useMemo
作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo
的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo
,以达到优化性能的目的。
其实就是说缓存值并不一定是缓存的,缓存有可能丢失!
另外要注意无论是useMemo还是useCallback它们并不会把历史缓存值返回,返回的都是上一次缓存住的值!
useCallback的使用
有人在凡是用到回调函数的地方不论什么情况都用useCallback处理一下,认为这样能提高性能,是不对的,不清楚useCallback的工作原理反而可能会造成效率下降运行变慢。我们必须厘清在什么情况下才应该使用useCallback。
首先,之所以要使用useCallback是为了避免不必要的属性变化而造成子组件重新渲染,从这个角度来说,直接绑定在dom上的回调事件没有必要用useCallback处理。
function MyComponent() {
const handleClick = useCallback(() => {...}, [...]) // 这样没什么用
return (<button onClick={handleClick}></button>)
}
要正确使用useCallback,需要理解function是一种引用类型,在闭包内创建的function每次都会是一个新的对象,所以如果这样将这个函数作为参数传给子组件的时候,虽然函数做的事情没有变,但是作为函数的对象已经发生了改变,则会造成子组件重新渲染,所以为了避免这种情况,应该使用useCallback。
假设,我们渲染一个很长的数组列表,所有列表元素上都有一个父元素传来的onClick回调,这时如果父组件刷新,某个不应该影响子组件渲染的属性发生改变,如果用了useCallback将减少很多不必要的子组件渲染。
示例:https://codesandbox.io/s/usecallback-demo-gozqsm?file=/src/App.js
此示例中有一个大列表,如果传给它的onItemClick回调用useCallback处理和不用它处理的效果不同(需要注意的是,MyList是一个memo后的组件,否则父组件只要重新render,即使子组件的属性完全没变也仍然会渲染)。现在当父组件重新渲染时handleClickCached函数每次都是一样的值,因此list2不会重新渲染,而list1重新渲染其下的每个子组件也会随之重新渲染造成资源浪费。
因此要注意区分开什么情况下才应该使用useCallback,useCallback的返回值并非没有开销,如果是没有大量的子组件渲染,则不必一定useCallback处理。
最后,凡是开发过程中其实都可以先不用useMemo或useCallback处理,只在事后优化的时候再酌情添加,而且这些值的使用往往也要和React.memo配合!