useMemo和useCallback

本文探究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配合

发表回复