8.useMemo&useCallback
# useMemo
来缓存每次重新渲染都需要计算的结果
const cachedValue = useMemo(calculateValue, dependencies)
import { useMemo } from 'react';
function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
// ...
}
2
3
4
5
6
7
8
9
10
# useCallback
useCallback 是一个允许你在多次渲染中缓存函数的 React Hook
const cachedFn = useCallback(fn, dependencies)
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
2
3
4
5
6
7
8
9
10
# 注意
useMemo, useCallbck 是 react hooks 中用来缓存数据的重要手段,使用这两个 hook 可以一定程度上的优化组件的性能。
很多同学倾向于给每个变量都套上 useMemo / useCallback ,不过这种方式是不对的,因为这种优化方式是有成本的。
每个 useMemo / useCallback 都是天然的闭包, 里面的垃圾数据得不到及时的释放,不合理的使用会造成数据堆积,变成负优化。
# 什么时候该去使用useMemo捏
# 1.当自身是引用类型且要作为其他hook的依赖时
export const Component: React.FC = () => {
const [someDatas, setSomeDatas] = useState([1, 2, 3]);
const [otherData, setOtherData] = useState<{ bool: boolean }>({ bool: true });
const datas100 = someDatas.map((item) => {
return item + 100;
});
const { bool } = otherData;
// Effect 1
useEffect(() => {
console.log('Effect1 : ', datas100);
}, [datas100]);
// Effect 2
useEffect(() => {
console.log('Effect2 : ', bool);
}, [bool]);
return (
<div>
<button
onClick={() => {
setSomeDatas((draft) => {
return [...draft, 1];
});
}}
>
update someDatas
</button>
<button
onClick={() => {
setOtherData((draft) => {
return { bool: !draft.bool };
});
}}
>
update otherDatas
</button>
</div>
);
};
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
如果点击 update someDatas , 会更新 someDatas,从而引起 datas100 数据的变化,从而导致 Effect1 的重新执行,这没问题:打印:Effect1 : [101, 102, 103, 101]
如果点击 update otherDatas ,期望的是更新 otherDatas,从而引起 bool 数据的变化,从而引起 Effect2 的重新执行,但是…… 打印:Effect1 : [101, 102, 103, 101], Effect2 : false
在点击update otherDatas后,Effect1和Effect2被同时执行了!
原因: 当useState的数据改变时,会重新render,重新执行整个函数体。此时datas100和bool这两个变量将被重新计算生成。
但 react 的 hook(useEffect, useMemo, useCallback 等) 会有一个 deps array(第二个参数),而这些 hook 要不要再次被计算取决于本次 deps array 中的数据与上次 deps array 中的数据进行一次浅 diff,如果不相等则再次运行这个 hook,反之则不执行。
我们都知道在 js 在存储变量时,简单来说会把引用类型的地址存在数据栈中,而将值存在数据堆中,而浅diff的原理是比较二者在栈中存储的数据是否相同,并不关心堆中的情况
由于在每次 render 中, datas100 都会被重新计算生成,因此每次 data100 在栈中存储的地址都是新分配的,即使在堆中的值相同,但是在 deps array 看来,当前状态和上一个状态的 datas100 就是不同的,因此每次 render 都会重新执行 Effect1。
虽然每次 render 中,bool 这个变量都会被重新获取,但因为 bool 是一个简单类型,值直接存在栈中,虽然每次都被重新生成,不过只要他们的值相同, deps array 就会认为他们是相等的,所以只会在 bool 的值本身改变后执行 Effect2
对上面的组件进行一次改进:
export const Component: React.FC = () => {
const [someDatas, setSomeDatas] = useState([1, 2, 3]);
const [otherData, setOtherData] = useState<{ bool: boolean }>({ bool: true });
const datas100 = useMemo(
() =>
someDatas.map((item) => {
return item + 100;
}),
[someDatas],
);
const { bool } = otherData;
// Effect 1
useEffect(() => {
console.log('Effect1 : ', datas100);
}, [datas100]);
// Effect 2
useEffect(() => {
console.log('Effect2 : ', bool);
}, [bool]);
return (
<div>
<button
onClick={() => {
setSomeDatas((draft) => {
return [...draft, 1];
});
}}
>
update someDatas
</button>
<button
onClick={() => {
setOtherData((draft) => {
return { bool: !draft.bool };
});
}}
>
update otherDatas
</button>
</div>
);
};
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
改动核心:使用useMemo将获取datas100的逻辑包裹起来,这样只要someDatas不更新,datas就不会更新。那么Effect1将不会每次都执行。
点击update otherDatas:打印:Effect2:false。成功了,只有Effect2被执行了,再尝试点击update someDatas:Effect1 : [101, 102, 103, 101]。可以得出结论:当自身是引用类型,且要作为其他 hook 的依赖时,需要包裹 useMemo
# 2.当数据为引用类型,且要作为props传递给子组件时,推荐用useMemo包裹
interface Props {
datas100: number[];
bool: boolean;
}
export const ChildComponent: React.FC<Props> = ({ datas100, bool }) => {
// Effect 1
useEffect(() => {
console.log('Effect1 : ', datas100);
}, [datas100]);
// Effect 2
useEffect(() => {
console.log('Effect2 : ', bool);
}, [bool]);
return <div>我是子组件</div>;
};
export const Component: React.FC = () => {
const [someDatas, setSomeDatas] = useState([1, 2, 3]);
const [otherData, setOtherData] = useState<{ bool: boolean }>({ bool: true });
const datas100 = someDatas.map((item) => {
return item + 100;
});
const { bool } = otherData;
return (
<div>
<button
onClick={() => {
setSomeDatas((draft) => {
return [...draft, 1];
});
}}
>
update someDatas
</button>
<button
onClick={() => {
setOtherData((draft) => {
return { bool: !draft.bool };
});
}}
>
update otherDatas
</button>
<ChildComponent datas100={datas100} bool={bool} />
</div>
);
};
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
点击update otherDatas,打印如下:
Effect1 : [101, 102, 103]
Effect2 : false
2
Effect1 又被错误的执行了,原因很简单,看了上文的大家已经懂了,这里不做过多阐述。
这个例子就是典型的,将一个引用类型传递给了子组件,但是没有做缓存。
如果子组件的某个 hook 以这个变量做了依赖,那这个 hook 也就失效了,他就像病毒一样,如果这个 hook 是 useMemo ,且他被传递给了更深一层的组件,那可想而知,后果很严重。
所以我通常给会传递给子组件的引用类型都加上 useMemo ,可能这个做法会让很多人反感,但毕竟业务代码不只有一个人维护,其他人也可能会更新这部分代码,如果他在子组件中使用了某个没有做缓存的引用类型的 props,那岂不原地裂开?
# 3.当处理数据的时间复杂度较高时,应当用useMemo包裹
interface Props {
datas: number[];
anyProps: any;
}
const Component: React.FC<Props> = ({ datas }) => {
const str = datas.sort((a, b) => a > b ? 1 : -1).join('->');
return <div>{str}</div>;
};
2
3
4
5
6
7
8
9
在这个 demo 中,假设 datas 的 length 非常大,那 datas.sort 的计算量会变得比较恐怖,每次 Component 更新(不是 datas 自身引起的更新)都会引起 str 被重新计算,这明显是不符合预期的,并且会非常卡顿。
因此我们可以给 str 也包裹上 useMemo:
const Component: React.FC<Props> = ({ datas }) => {
const str = useMemo(
() => datas.sort((a, b) => (a > b ? 1 : -1)).join('->'),
[datas],
);
return <div>{str}</div>;
};
2
3
4
5
6
7
Component的更新不是由datas引起的,str就不会被重新计算,从而提高性能。
# 4.当你不知道应不应该加 useMemo,且它恰好是引用类型时
警告:最好不要这么做,除非你不熟悉react
可以想象一下,如果所有的引用类型都加了 useMemo,就绝不会出现 deps arr diff 混乱的问题,这种方式只能保证逻辑不会出错,但是性能就不一定了……
# 什么时候该去使用useCallback捏
useCallback其实是memo一个方法的useMemo的简写版本
useMemo(() => {
return () => {
console.log("do something");
};
}, []);
2
3
4
5
简化成
useCallback(() => {
console.log("do something");
}, []);
2
3
那我们该如何判断一个 function 是否需要被包裹 useCallback 呢,其实我们只要遵循 上文中 useMemo 返回 引用类型时的使用原则即可:
- 当这个 function 作为其他 hook 的依赖时
- 当这个 function 作为子组件的 props 传递时
参考文章 (opens new window)