useCallback、useMemo的使用场景

Hooks是React 16.8版本的新增特性,它的出现让我们可以不再写class组件来维护组件的内部状态。
在Hooks我们常用的基础方法为useState和useEffect,而对于useCallback和useMemo这两个方法,大家看到它的第一眼想到的可能就是性能优化吧。那么这两个方法是不是适用于所有的场景呢,这就是我们今天想要探讨的问题。

useMemo

1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo缓存计算结果,它接收一个计算的过程(回调函数,它将返回结果)和依赖项数据,返回一个memoized值。当依赖项发生变化的时候,回调函数会重新计算。

useCallback

1
2
3
4
5
6
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

useCallback缓存一个函数体,它接收回调参数和依赖项数组,返回一个 memoized 回调函数,只有依赖项发生变化的时候才会返回一个新的函数。

那么是不是所有场景下使用useCallback都能达到性能优化的效果呢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Example() {
const [value, setValue] = useState();
const onChange = (e) => {
setValue(e.target.value);
};

return <input value={value} onChange={onChange} />;
}

----------------------------------------------------------------------------------------------
const onChange = useCallback(e=> {
setValue(e.target.value);
}, []);
//等同于
const onChange = (e) => {
setValue(e.target.value);
};
const onChangeMemoized = useCallback(onChange, []);

我们给一个input框传入onChange方法,当我们将它加上useCallback后,我们会发现这个方式除了定义了onChange方法外,还有调用useCallback产生了额外的开销,导致适得其反。
可能会有同学有疑问,我们不是用了useCallback吗,为啥onChange还会重新定义呢。这是因为函数组件每次state一变化,就重新执行,会重复声明。useCallback会缓存之前传入的回调函数,但是一旦依赖项发生变化,将返回新的函数。
实际上,useCallback在很多时候需要和React.memo搭配使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//子组件
const BigData = ({ showNum }) => {
const [num, setNum] = useState(() => showNum());

console.log("子组件重新渲染了喔");
useEffect(() => {
setNum(showNum());
}, [showNum]);
return (
<div className='BigData'>
{'child:'+num}
<br></br>
{'假设子组件渲染大量数据...'}
</div>
);
};
export default React.memo(BigData)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//父组件
import BigData from './Child'

const App = () => {
const [value, setValue] = useState('');
const [num, setNum] = useState(1);
const showNum = useCallback(() => { return num; }, [num]);
// const showNum =() => { return num; };

return (
<div className='app'>
<div>{'parent:' + num}</div>
<div>
<button onClick={() => setNum(num * 2)}>*2</button>
</div>
<input value={value} onChange={event => setValue(event.target.value)} />
<BigData showNum={showNum} />
</div>
);
}

我们假设一个场景,子组件需要展示大量的数据,它从父组件接收一个函数。在很多时候,父组件更新的时候,我们不需要子组件的更新。可能大家会给子组件包装到React.memo中(作用可参考shouldComponentUpdate(),但仅适用于函数组件),来保证props相同的情况下不重复渲染组件。但是函数式组件更新的时候函数会重新声明,引用发生了变化。而React.memo函数只会浅比较props,因此子组件仍然会重新渲染。此时我们给要传入子组件的函数加上useCallback来保证函数引用的相等,从而达到子组件不重复渲染的效果,实现性能优化。
我们来看一下不加useCallback的时候
不使用useCallback.png
可以看到state改变父组件重新渲染的时候,子组件也重新渲染了。

如果加上useCallback
使用useCallback.png
可以看到state改变父组件重新渲染的时候,子组件没有重新渲染,达到了我们想要的效果。

那么useMemo其实也是类似的,当我们需要给子组件传入一个引用类型的对象时,父组件重新渲染会导致值的引用发生变化。如果此时我们不需要重新渲染子组件时,可以用useMemo来记住这个值。

1
2
3
4
5
6
7
const Example = () => {
const value = useMemo(() => {
compute(num)
}, [num]);
//compute方法返回值为数组
return <BigData value={value} />
}

我们假设渲染子组件的开销较大(又是一个渲染大量数据的组件2333),那么value(返回值为引用类型)的引用变化而依赖项num没有变化时,我们可能不想子组件重新渲染。因此可以用useMemo来避免Example组件的渲染导致compute方法重新计算。此时value的引用不会发生变化,子组件不会重新渲染。
我们来看一个具体的例子

1
2
3
4
5
6
7
8
9
10
11
//子组件
const ChildMemo = ({ childData, onClick}) => {
console.log('我是子组件,我渲染了he')
return(
<div>
<span >{'子组件:'+childData.name}</span>
<button onClick={() => onClick('变身2333')}>改变name</button>
</div>
);
}
export default React.memo(ChildMemo);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//父组件
const Example = () => {
const [num, setNum] = useState(0);
const [name, setName] = useState('子组件');

return (
<>
<span>number:{num}</span>
<button onClick={(e) => { setNum(num + 1) }}>加1</button>
<ChildMemo
// childData={
// {
// name,
// color: name.indexOf('2333') !== -1 ? 'blue' : 'purple'
// }
// }
childData={
useMemo(() => ({
name,
color: name.indexOf('2333') !== -1 ? 'blue' : 'purple'
}), [name])
}
onClick={useCallback((newName) => setName(newName), [])}
/>
</>
)
}

当我们点击按钮时,如果childData返回值不加上useMemo,由于传入的参数为引用类型,引用变化会导致子组件的重新渲染。这种场景和上一个例子相似,只不过传入的参数不是一个方法,而是一个引用类型的值了。用useMemo可以保证在依赖项不变的时候,传入子组件的是同一个引用。

关于useMemo,还有一种情况我们可以使用。当一个函数的开销很大时(有较复杂的计算过程),我们可以用useMemo来记住它的返回值,这样可以避免性能消耗较高的重复计算。

1
2
3
4
5
6
7
8
9
10
11
const Example = () => {
const result = useMemo(() => expensiveCompute(value), [value]);
function expensiveCompute(value){
//...较复杂的计算过程
}
return (
<div>
{result}
</div>
)
}

加上useMemo后,虽然组件在重新渲染的时候将会重新定义这个开销较大的函数,但是它只会在被需要的时候才会被调用。当依赖项不变时,该方法将返回之前已经计算好的值。

总结
useCallback和useMemo的使用场景可以大致有以下两种。
1、保证传入子组件的引用相等
当子组件或者需要接收父组件传来的函数、对象、数组等引用类型时,或者它们被用在其他hook中的依赖数组中,我们应该使用。
2、开销大的运算
使用useMemo避免重复计算相同的结果。