Memoization技术在React中的应用

最近在研究React Hook优化时,发现Memoization技术被官方广泛的使用,比如useCallbackuseMemo这两个API,分别用来返回函数的memoized版本和memoized值。Memoization其实并不是什么新技术,只是一种优化技巧,维基百科对Memoization的定义为:

In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

简单来说Memoization就是存储前一个值,然后每次更新时都将新传入的值和存储的值作比较,然后根据比较结果是否相同来返回存储的值还是新的值。React中使用该技术主要是为了避免不必要的重复渲染。

在Redux中的应用

我们用来优化Redux数据的reselect其实就使用了Memoization技术,这篇文章分析了reselect源码,可以学习reselect是如果使用该技术来优化Redux数据的。

在React Hook中的应用

熟悉了Memoization定义后,我们看下如果在实战中使用这个技术。在React Hook中,useEffect方法可以使用该技术来优化组件的渲染。本例子可以在CodeSandbox中查看:smile:。

我们知道useEffect的第二个数组参数作为effect依赖存在,如果依赖数组发生改变,那么useEffect就会重新执行。useEffect使用===来比较数组参数是否相等,使用Object.assign或者splice这类会操作时会返回新的堆来存储引用值,就会导致值本身没有变化却会重复执行useEffect方法。为了避免这种情况的发生,我们来看下该如果来做。

首先,先来介绍下useRef方法,这个方法和原来的createRef非常的相似,都是创建一个可变的ref对象。在函数组件中,因为ref对象在组件的整个生命周期内保持不变,所以我们可以用它来存储不会被组件re-render影响的值。所以,在下面的函数中,我们使用ref对象来存储useEffect中传入的第二个数组参数。

1
2
3
4
5
6
7
8
9
10
11
12
import React, { useRef } from "react";
import { isEqual } from "lodash";

function useDeepCompareMemoize(value) {
const ref = useRef();

if (!isEqual(value, ref.current)) {
ref.current = value;
}

return ref.current;
}

可以看到我使用了lodash中的深比较方法isEqual来对比两个值,这个可以根据需求自定义方法也可以。

接下来编写自定义Hook来使用useEffect

1
2
3
4
5
function useDeepCompareEffect(callback, dependencies) {
useEffect(callback, useDeepCompareMemoize(dependencies));
}

export default useDeepCompareEffect;

然后使用useDeepCompareEffect代替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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
import useDeepCompareEffect from "./useDeepCompareEffect.js";

import "./styles.css";

const tomCat = {
name: "Tom",
race: "cat"
};
const jerryMouse = {
name: "Jerry",
race: "mouse"
};

function App() {
const [character, updateCharacter] = useState(tomCat);
const effectCount = useRef(0);
const deepCompareEffectCount = useRef(0);

useEffect(() => {
effectCount.current++;
}, [character]);

useDeepCompareEffect(() => {
deepCompareEffectCount.current++;
}, [character]);

const changeStar = () => {
const star = character.name === "Tom" ? jerryMouse : tomCat;
updateCharacter(star);
};

const assignObj = () => {
updateCharacter(Object.assign({}, character));
};

return (
<div className="App">
<p>Hello, useEffect</p>
<p className="star">
{character.name} {character.race} {character.friend}
</p>
<p>useEffect count = {effectCount.current}</p>
<p>deepCompareEffectCount count = {deepCompareEffectCount.current}</p>
<p>
<button onClick={changeStar}>Change star</button>
</p>
<p>
<button onClick={assignObj}>Click</button>
</p>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

同样的,点击Click按钮,使用useEffect方法每次都会执行,而使用useDeepCompareEffect的只会执行一次。不过需要注意的是,该优化并不是万金油,而是要根据情况来使用。因为深比较本来就会对性能造成一定的损耗,所以要按需使用。比如useCallbackuseMemo最好在需要重复计算时才使用,在其他场景应用不当可能会有副作用,关于这一块的内容推荐阅读本文末尾中的参考文章。

参考

  1. use-deep-compare-effect
  2. memoize-one
  3. You’re overusing useMemo: Rethinking Hooks memoization
Author: linxiangjun
Link: http://www.linxiangjun.com/memoization-in-react.html
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.