前言
这是我参与「第四届青训营 」笔记创作活动的第 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>
}
一开始的情况是:
如果点击加1按钮,点多少次data就会重新渲染多少次,无论值是不是有变化
由于每次点击,a都会变化,而a的变化引起了整个组件的重新渲染
-
由于data没有任何依赖,所以会直接重新初始化。
- 如果data是引用类型,那么每次重新渲染都是不同的值,所以就会变化
- 如果是原始类型,那因为值相同,所以不算有变化,不触发useEffect,但实际上是重新赋值了
-
memoData是依赖于b的,由于b是由useState管理,没有经过set函数是不会改变的,也不会重新初始化,它始终保持最新的一个值。
除非组件被卸载,b才会改变。
所以memoData只会在b主动发生改变时才会改变。
优化实例
通过状态保存的数组,使用map渲染组件时,如果这个数组发生了变化,比如其中一个元素无了,那么其他所有的组件都会重新渲染
为什么呢?因为这个数组本身发生改变,state发生改变就会导致重新渲染。
那可以怎么优化呢?通过useMemo和useCallback。
首先,可以通过useMemo对组件进行包装,当其依赖的props发生变化时再更新其值
但是还不够,其依赖的onRemove函数是下面这样的,每次重新渲染都会返回一个新的函数
const onRemove = tickerToRemove => {
setWatchlist(
watchlist.filter(ticker => ticker !== tickerToRemove)
);
};
所以要用useCallback进行包装: 如果包装成下面这样,每次重新渲染还是会得到新的onRemove:
为什么呢?因为组件会重新渲染就是因为watchlist发生变化,那这里又依赖了watchlist,所以每次变化也跟着变
怎么办?想一想为什么要依赖watchlist?为了拿到最新的watchlist
那为啥不用setter自带的参数?对哦!
最终版
总结
- 使用useMemo或useCallback时,必须仔细注意其依赖的props等东西,这些东西是否也需要进行包装
- 弄清楚useMemo的目的,要么是存对象props,要么是组件jsx
- useMemo的粒度是原子性的,也就是说,如果useMemo中的函数用到了其他引用类型变量,那这个引用类型也要做memo,不然等于白优化