useMemo和useCallback用法总结(基础版) | 青训营笔记

324 阅读5分钟

前言

这是我参与「第四届青训营 」笔记创作活动的第 4 天,今天给大家分享一下我学习React hooks中两个缓存相关的hook——useMemo和useCallback的一些理解。

useCallback & useMemo

在函数组件中,默认情况下,只要父组件状态变了,则无论子组件是否有依赖该状态,都会重新渲染。那么如何优化呢?

一般的优化如下:

  • 对于类组件,使用pureComponent

  • 对于函数组件,可以使用React.memo,它接收一个函数组件和判断函数。如果忽略判断函数,则默认对组件的props进行浅比较。

    返回一个新的组件,该组件的功能与原组件的区别在于:如果接收到的属性props或state不变,则不重新渲染组件。

const NewChild = React.memo(Child, (prevProps, nextProps) => {
          // 自定义对比方法,也可忽略不写
          // return true 则不重新渲染
          // return false 重新渲染
})

但有缺陷,如果使用了useState状态,每次更新都是独立的,即使值没变化,也是新的值,也会使其重新渲染。

怎么办?这就要提出useCallback和useMemo。

useCallback

用于缓存函数,在第一次渲染时执行,只有在依赖项改变时才会更新缓存。缓存的是 函数本身以及它的引用地址 ,而不是返回值。

  • 接收一个内联回调函数和一个依赖项数组,数组中包含子组件依赖的父组件状态。

    • 如果没有传入依赖数组,则还是会重新渲染。
    • 如果依赖数组中没有值,即使状态改变也不会触发回调。
  • 返回该回调函数的memoized版本,当某依赖项改变时才会更新。

例子:

// 一般写法
const addClick = ()=>{
        setNumber(number+1);
}
// 优化写法
const func = useCallback(()=>{
    setNumber(number+1);
},[number])
​
​
// 传入子组件
<SubCounter data={data} onClick={addClick}/>

useMemo

第一次渲染期间执行,缓存变量。不只是缓存了函数的返回值,同时保证了返回值的引用地址不变

  • 接收一个值创建函数(也就是返回一个值的)和依赖数组

    const data = useMemo(()=>({number}),[number]);  
    

    如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值

  • 返回一个memoized,可以是函数或一般数据。这个值在依赖变化时才会重新渲染。

    useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

  • 第二个参数依赖,可以是数组也可以是一个函数,这个函数可以接收原先的props和下一个props

举个例子:

可以看到memoData是依赖于b的,而data没有任何依赖。

export default function Test(){
    const [a,setA] = useState(0);
    const [b,setB] = useState(0);
    const memoData = useMemo(()=>({data:1}),[b]);
    const data = {data:2};
    useEffect(()=>{
        console.log('config重新渲染了!!',memoData);
    },[memoData])
​
    useEffect(()=>{
        console.log('data重新渲染了!!',data);
    },[data])
​
    const add = ()=>{
        setA(v=>v+1);
    }
​
    return <div>
        <span>{a}</span>
        <button onClick={add}>a加一</button>
    </div>
}

一开始的情况是:

image.png

如果点击加1按钮,点多少次data就会重新渲染多少次,无论值是不是有变化

image.png

由于每次点击,a都会变化,而a的变化引起了整个组件的重新渲染

  • 由于data没有任何依赖,所以会直接重新初始化。

    • 如果data是引用类型,那么每次重新渲染都是不同的值,所以就会变化
    • 如果是原始类型,那因为值相同,所以不算有变化,不触发useEffect,但实际上是重新赋值了
  • memoData是依赖于b的,由于b是由useState管理,没有经过set函数是不会改变的,也不会重新初始化,它始终保持最新的一个值。

    除非组件被卸载,b才会改变。

    所以memoData只会在b主动发生改变时才会改变。

优化实例

通过状态保存的数组,使用map渲染组件时,如果这个数组发生了变化,比如其中一个元素无了,那么其他所有的组件都会重新渲染

image.png

为什么呢?因为这个数组本身发生改变,state发生改变就会导致重新渲染。

那可以怎么优化呢?通过useMemo和useCallback。

首先,可以通过useMemo对组件进行包装,当其依赖的props发生变化时再更新其值

image.png

但是还不够,其依赖的onRemove函数是下面这样的,每次重新渲染都会返回一个新的函数

const onRemove = tickerToRemove => {
       setWatchlist(
           watchlist.filter(ticker => ticker !== tickerToRemove)
       );
};

所以要用useCallback进行包装: 如果包装成下面这样,每次重新渲染还是会得到新的onRemove:

image.png

为什么呢?因为组件会重新渲染就是因为watchlist发生变化,那这里又依赖了watchlist,所以每次变化也跟着变

怎么办?想一想为什么要依赖watchlist?为了拿到最新的watchlist

那为啥不用setter自带的参数?对哦!

最终版

image.png

总结

  • 使用useMemo或useCallback时,必须仔细注意其依赖的props等东西,这些东西是否也需要进行包装
  • 弄清楚useMemo的目的,要么是存对象props,要么是组件jsx
  • useMemo的粒度是原子性的,也就是说,如果useMemo中的函数用到了其他引用类型变量,那这个引用类型也要做memo,不然等于白优化

参考文章

React Hooks - 组件重新渲染原理 - 专栏 - 声网 Agora RTC 开发者社区

深入剖析React.memo和React.useEffect