沐光

记录在前端之路的点点滴滴

React.memo、useMemo 和 useCallback 的区别

前言

在看完了一遍 React 的基础内容后,现在又回过头来看 React 组件更新与优化相关的知识了,在使用 Hook 之后,与这个话题脱不开关系的便有 useMemouseCallbackReact.memo 了,正好项目优化时碰到了这么些知识,刚好总结一番。

三者区别

首先从使用位置来看可以划分为两种:

  • 作用于组件: React.memo

  • 作用于组件内:useMemouseCallback

那么分别来看有什么作用吧~

React.memo

React.memo 主要是为了缓存组件当父组件 state 更新,但未改变子组件的入参的 state 值时,子组件应该不自我更新,而是用先前的缓存值

父组件:

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
28
// 父组件 App.tsx
import React, { useState, useCallback, useEffect } from 'react';
import Counter from './memo-test/counter';

function App() {
const [counter1, setCounter1] = useState(1);
const [counter2] = useState(2);

const updateCounter1 = () => { setCounter1(counter1 + 1); };

useEffect(() => {
console.log('parent update');
});

return (
<>
<button onClick={updateCounter1}>更新 Counter1</button>

<h3>Counter 1:</h3>
<Counter count={counter1} cname="Counter1"></Counter>

<h3>Counter 2:</h3>
<Counter count={counter2} cname="Counter2"></Counter>
</>
);
}

export default App;

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { memo } from 'react';

interface IProps {
cname: string;
count: number;
}

const Counter = (props: IProps) => {
const { count, cname } = props;

// 每次重新加载时触发
console.log(`updated ${cname}`);

return (
<p>
{cname}'s value is: {count}
</p>
);
};

export default memo(Counter);

这里点击“更新 Counter1”的按钮后,会导致父组件更新,父组件更新后因为无缓存,因此子组件也就一起更新了,但是仔细分析一下,我们的 Counter2 的传参并无任何变动,这里更新后造成了多余的渲染,资源浪费,因此需要做一下缓存(子组件的注释替换一下即可),这样就 Counter2 就不会重复渲染了。为缓存和缓存后写法的结果分别为:

未缓存:

未缓存

已缓存:

已缓存

可见缓存后的组件在参数不变的情况下是不会重新渲染的!

useMemo

useMemo 主要是对变量进行缓存(当然函数也可以,但推荐使用 useCallback,除非需要一些定制化逻辑操作),该 Hook 一般于 useEffect 进行比较。useMemo 是在依赖变动后,Dom 变动前触发的,而 useEffect 则是依赖变动后,Dom 变动后才出发(副作用)。

因此,如果依赖变更后需要立即做出反应时可以使用 useMemo ,如果是依赖变动后,需要带着触发一些其它的操作,则使用 useEffect

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
28
29
30
31
32
33
34
35
36
37
38
39
40
import React, { useState, useMemo, useEffect } from 'react';

function App() {
const [counter1, setCounter1] = useState(1);

const updateCounter1 = () => {
setCounter1(counter1 + 1);
};

const getRandom = () => {
return Math.random() * 10;
};
const memoRandom = useMemo(() => {
// 此处会立即触发哟~
return Math.random() * 10;
}, []);

useEffect(() => {
console.log('counter1 变动了');
}, [counter1]);
useEffect(() => {
console.log('getRandom 变动了');
}, [getRandom]);
useEffect(() => {
console.log('memoRandom 变动了');
}, [memoRandom]);

return (
<>
<button onClick={updateCounter1}>更新 Counter1</button>
<span>&nbsp;&nbsp;</span>

<p>Counter1: {counter1}</p>
<p>Random: {getRandom()}</p>
<p>MemoRandom: {memoRandom}</p>
</>
);
}

export default App;

这里变更 counter1 时,咱们的 randome 会一直变更,但这显然不是我们需要的,此处就可以使用 useMemo 做一次缓存,这样就不会随着父组件的刷新而变动了。

值缓存

useMemo 可以直接理解为 Vue 的 computed 钩子

useCallback

useCallback 是对组件内的函数进行缓存。当组件的 state 变动后,其会更新整个组件内容,未做缓存的处理的函数都会重新生成一次,这在有 useEffect 监听的情况下会造成每次都触发,因此可用 useCallback 进行函数的缓存:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import Counter from './memo-test/counter';

function App() {
const [counter1, setCounter1] = useState(1);
const [counter2, setCounter2] = useState(2);

const updateCounter1 = () => {
setCounter1(counter1 + 1);
};

const updateCounter2 = () => {
setCounter2(counter2 + 1);
};
const callbackedCounter2 = useCallback(() => {
setCounter2(counter2 + 1);
}, [counter2]);

useEffect(() => {
console.log('updateCounter1 变动了');
}, [updateCounter1]);
useEffect(() => {
console.log('updateCounter2 变动了');
}, [updateCounter2]);
useEffect(() => {
console.log('callbackedCounter2 仅在 counter2 变更时变更');
}, [callbackedCounter2]);

return (
<>
<button onClick={updateCounter1}>更新 Counter1</button>
<span>&nbsp;&nbsp;</span>
<button onClick={updateCounter2}>更新 Counter2</button>

<h3>Counter 1:</h3>
<Counter count={counter1} cname="Counter1"></Counter>

<h3>Counter 2:</h3>
<Counter
count={counter2}
cname="Counter2"
callbackedCounter2={callbackedCounter2}
></Counter>
</>
);
}

export default App;

点击“更新 Counter1”的结果如下:

函数缓存

如果仅仅只是父组件内有函数变动,用不用 useCallback 影响不大,但是一旦有子组件有传入父组件的函数时,一定得注意 useCallback 来缓存父组件的函数,否则会带来不必要的渲染。

useCallbackuseMemo 的语法糖:useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

总结

  • React.memo 的使用场景:父组件频繁变更的内容不会影响到子组件时,需要对子组件做 React.memo 缓存处理;
  • useMemo 的使用场景:当组件内的函数返回值不需要随着组件的刷新而变更时,需要对其做 useMemo 缓存处理
  • useCallback 的使用场景:父组件传递函数给子组件,该函数均需做 useCallback 缓存处理,减少子组件重复渲染;
1
2
3
4
5
6
7
8
9
10
11
12
13
// useMemo 和 useCallback 函数缓存写法
const memoFunc = useMemo(() => {
// 每次父组件重新渲染时会执行的部分,可以做额外操作

return () => {
// 函数内容
}
}, []);

// 只有 useMemo 的 return 的部分
const callbackFunc = useCallback(() => {
// 函数内容
}, [])

⚠️注:开发环境请根据具体情况进行缓存优化,因为缓存的监听函数也是需要消耗资源的,过度缓存有时会适得其反。

参考文章