基于useMemo构建计算缓存

useMemo 是一个 React hook,它用于在组件内部缓存计算的结果。当你想要避免在每次渲染时都进行高代价的计算时,这个 hook 就非常有用。useMemo 会在依赖项没有发生变化的情况下,返回缓存的值,避免不必要的计算。

应用场景——复杂逻辑避免每次状态更新重新执行

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
/*
* @Description:
* @Author: xiuji
* @Date: 2024-01-29 09:40:15
* @LastEditTime: 2024-01-29 15:13:00
* @LastEditors: Do not edit
*/
import { useMemo, useState, useEffect } from "react";

const UseMemoDemo = () => {
console.log('UseMemoDemo render');
const [correct, setCorrect] = useState(998),
[wrong, setWrong] = useState(475),
[x, setX] = useState(0);

// 计算正确率
// 每次无论任何状态发生变化,都会重新计算正确率,影响性能
// let accuracy = (correct / (correct + wrong) * 100).toFixed(2) + '%';

// 使用useMemo,只有当correct和wrong发生变化时,才会重新计算正确率
let accuracy = useMemo(() => {
console.log('useMemo');
return (correct / (correct + wrong) * 100).toFixed(2) + '%';
}, [correct, wrong]);



return (
<div>
<h1>正确:{correct}</h1>
<h1>错误:{wrong}</h1>
<h1>正确率:{accuracy}</h1>
<hr />
<h1>无关状态:{x}</h1>
<div>
<button onClick={() => setCorrect(correct + 1)}>正确</button>
<button onClick={() => setWrong(wrong + 1)}>错误</button>
<button onClick={() => setX(x + 1)}>无关状态</button>
</div>
</div>
)
}

export default UseMemoDemo;

每次状态修改(任何状态修改)都会重新执行函数组件,生成新的闭包,函数内的逻辑自上而下都会重新执行,上面示例中计算正确率实际上只与correctwrong有关,其余任何状态的改变都不必执行计算正确率(影响性能),使用useMemo可以避免在每次组件渲染时都执行复杂的计算。

在依赖的状态值没有改变,callback没有触发执行的时候,接收返回参数的变量获取的是上一次计算出来的结果。

然而,它并不保证缓存一直不变,因为 React 可能会决定“忘记”一些之前缓存的值以释放内存。因此,不应该依赖 useMemo 来处理副作用,而仅仅用于优化性能。如果你需要处理副作用,应该使用 useEffect 或其他适用的 hook。

基于useCallBack缓存函数引用

useCallback 是 React 的一个 hook,它返回一个记忆化的回调函数。这个 hook 在将回调函数传递给经过优化的子组件并且希望避免不必要的子组件重渲染时特别有用。useCallback 会在依赖项没有改变的情况下返回同一个回调函数实例,从而避免因为函数引用的变化而触发子组件的重渲染。

应用场景——父组件嵌套子组件时,父组件更新导致子组件重新渲染

父组件

1
2
3
4
5
6
7
8
9
10
11
const UseCallBackDemo = () => {
const [count, setCount] = useState(0);

const faChange = useCallback(() => { }, []);

return <div>
<Son handle={faChange} />
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
}

子组件

1
2
3
4
5
6
7
8
class Son extends React.Component {
render() {
console.log('Son render'); // 每次父组件状态更新都会执行
return <div>
<h1>Son Component</h1>
</div>
}
}

上述示例父组件每次状态更新重新执行函数组件,内部faChange函数的内存引用地址都会重新生成,faChange作为子组件的props引用地址不同会导致子组件每次随着父组件状态更新而重新渲染。

性能优化:当父组件更新时,由于传递给子组件的是函数,因此不让子组件随着父组件更新而更新

  • 基于useCallback处理,使传递给子组件的函数每次引用地址都一致
  • 子组件内部,验证父组件传递的属性是否发生改变,没有变化,子组件则不更新,有变化则更新。
    • 子组件为类组件:继承React.PureComponent
    • 子组件为函数组件:使用React.Memo

父组件

1
2
3
4
5
6
7
8
9
10
11
const UseCallBackDemo = () => {
const [count, setCount] = useState(0);

const faChange = useCallback(() => { }, []); // 使用useCallback不生成新的函数,保持引用地址一致

return <div>
<Son handle={faChange} />
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
}

子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// class组件
class Son extends React.PureComponent { // 继承React.PureComponent对组件属性进行浅比较
render() {
console.log('Son render');
return <div>
<h1>Son Component</h1>
</div>
}
}

// 函数组件
const Son = React.memo((props) => { // 基于React.memo对新老属性做比较
console.log('Son render');
return <div>
<h1>Son Component</h1>
</div>
})

基于应用场景使用

不是所有的小函数都需要用 useCallback 包起来。是否使用 useCallback 取决于具体的使用场景和性能优化需求。以下是一些决定是否使用 useCallback 的指导原则:

  1. 传递给纯组件:如果你正在将一个回调函数传递给一个经过优化的子组件(如 React.memo 包装的组件),并且你希望防止这个子组件因为父组件的渲染而进行不必要的重渲染,那么应该使用 useCallback

  2. 渲染优化:在有大量渲染操作或列表,并且需要避免不必要的重新渲染时,使用 useCallback 可以提升性能。

  3. 依赖稳定性:当函数被作为依赖传递给其他 useEffectuseMemouseCallback 时,为了避免因为函数引用的变化而触发重执行,应该使用 useCallback

  4. 事件处理器:如果事件处理器被频繁触发,使用 useCallback 可以避免由于组件重渲染而导致的事件处理器重复创建。

然而,如果这个函数:

  • 不被传递给子组件,
  • 不作为依赖项被其他 hooks 使用,
  • 不是在经过性能优化的组件中使用,或者
  • 即使父组件重渲染,也不会导致性能瓶颈,

那么将它包装在 useCallback 中可能是不必要的。实际上,在这些情况下使用 useCallback 可能会引入额外的性能开销,因为记忆化函数也是有成本的。

因此,尽管 useCallback 可以避免不必要的渲染,但它并不是一个万能的解决方案,而应该基于性能优化的需要来决定是否使用。在很多情况下,简单的函数组件不使用 useCallback 也能很好地工作。