何时使用 useMemo 与 useCallback

React

React 中有许多性能优化的手段,useMemouseCallback 是 hooks 推行后最为常用的两种方法,但是任何优化方案都是有成本的,如果为了图方便给每个函数、组件都套上 useCallbackMemo,不仅代码可读性会变差,还会因为参数的传递问题产生非预期的 bug。

正好最近在看代码规范的问题,自己也时常滥用这两个函数,重新整理学习下喵~

简介

先简单看下 React docs 中关于两者的介绍:

useCallback

useCallback reference

1
2
3
4
5
6
7
// a, b 参数不变时,memoizedCallback 的引用不变
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

Returns a memoized callback.

useMemo

useMemo reference

1
2
// a, b 参数不变时,memoizedValue 的值不变(即 computeExpensiveValue 不被执行)
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Returns a memoized value.

相同点

无论 useCallback 还是 useMemo 都是通过缓存来优化性能的,只不过 useCallback 缓存的是函数的引用,useMemo 缓存的则是(函数)返回值。两者可以做到等价:

1
useCallback(fn, deps) === useMemo(() => fn, deps))

这两个函数的第二个参数中,比较值更新的方法采用的是 JavaScript 的绝对相等 ===,需要注意的是,JS 中函数被视为对象,除非他们引用相等,否则两个对象就算值一样,他们也不是全等的:

1
2
3
4
5
6
7
8
9
10
11
12
const string1 = 'hi';
const string2 = 'hi';

console.log(string1 === string2); // true
console.log('hi' === 'hi') // true

const foo1 = () => 'bar';
const foo2 = () => 'bar';
const foo1Ref = foo1;

console.log(foo1 === foo2) // false
console.log(foo1 === foo1Ref) // true

区别

useCallbackuseMemo 返回的内容不同。

useCallback 会返回一个未执行的函数,而 useMemo 则返回执行完函数后的值。

1
2
3
4
5
6
7
8
9
const foo = () => 'bar';

const testCallback = useCallback(foo, []);
const testMemo = useMemo(foo, []);

console.log(testCallback); // foo() {}
console.log(testMemo); // bar
console.log(testCallback()); // bar
console.log(testMemo()); // TypeError: testMemo is not a function

在真实场景中,我们一般会这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const App = ({...props}) => {
// ...

// 在 useCallback 中传入的第一个函数,必须是一个函数。
const memoizedCallback = useCallback(() => {
someFunc(foo, bar);
}, [foo, bar]);

const memoizedResult = useMemo(() => someOtherFunc(foo, bar), [
foo,
bar,
]);

// ...
}

何时使用?

在使用函数式组件时,常常会产生组件的重渲染,而组件的重渲染又会带来函数的引用改变或值的重计算,都是一笔开销。

useMemo 适合用于大量数据运算的场景,如:

1
2
3
const value = React.useMemo(() => {
return Array(100000).fill('').map(v => v);
}, [a]);

useCallback 则适用于与 memo 搭配,减少由函数操作带来的重渲染,比如下面这个例子:

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
import React, { useState } from "react";

// 子组件
const Child = ({ handleChildClick }) => {
console.log("<=== Child render");
return <button onClick={handleChildClick}>handleChildClick</button>;
};

// 父组件
const Parent = () => {
const [count, setCount] = useState(0);

const handleChildClick = () => {
console.log("Click Child");
};

const handleParentClick = () => {
console.log("Click Parent");
setCount(count => count + 1);
};

return (
<div>
Count: {count}
<hr />
<button onClick={handleParentClick}>handleParentClick</button>
<Child handleChildClick={handleChildClick} />
</div>
);
};

export default Parent;

在不加任何操作的情况下,每次点击 handleParentClick 的按钮,都会打印出 Child render 的信息:

1
2
3
4
5
6
<=== Child render // init

click Parent
<=== Child render
click Parent
<=== Child render

尝试给 handleChildClick 函数套上 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
import React, { useState, useCallback } from "react";

const Child = ({ handleChildClick }) => {
console.log("<=== Child render");
return <button onClick={handleChildClick}>handleChildClick</button>;
};

const Parent = () => {
const [count, setCount] = useState(0);

// Only changes here
const handleChildClick = useCallback(() => {
console.log("Click Child");
}, []);

const handleParentClick = () => {
console.log("Click Parent");
setCount(count => count + 1);
};

return (
<div>
Count: {count}
<hr />
<button onClick={handleParentClick}>handleParentClick</button>
<Child handleChildClick={handleChildClick} />
</div>
);
};

export default Parent;

发现效果还是一样:

1
2
3
4
5
6
<=== Child render // init

click Parent
<=== Child render
click Parent
<=== Child render

再给 Child 套一层 memo

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
import React, { useState, memo, useCallback } from "react";

// 使用了 memo 的子组件
const Child = memo(({ handleChildClick }) => {
console.log("<=== Child render");
return <button onClick={handleChildClick}>handleChildClick</button>;
});

// 父组件
const Parent = () => {
const [count, setCount] = useState(0);

// 使用了 useCallback
const handleChildClick = useCallback(() => {
console.log("Click Child");
}, []); // 如果这里填了 count,就无效了~

const handleParentClick = () => {
console.log("Click Parent");
setCount(count => count + 1);
};

return (
<div>
Count: {count}
<hr />
<button onClick={handleParentClick}>handleParentClick</button>
<Child handleChildClick={handleChildClick} />
</div>
);
};

export default Parent;

这样就符合预期啦:

1
10 Click Parent 

为什么该例中需要 useCallbackmemo 搭配起来用才有效果?

首先,将 Child 组件使用 memo 包裹,以达到 React.PureComponent 的效果,即能够对 props 进行浅比较,但是父组件因为 count 的改变,handleChildClick 会不断刷新引用,只有给这个函数套上 useCallback 进行缓存后,memo 的比较才能起作用,最终使得 Child 组件不被重复渲染,性能得到提升。

Author: Cyris

Permalink: https://sound.cyris.moe/posts/when-to-memo-react/

文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。

Comments

Unable to load Disqus, please make sure your network can access.