使用 React 的记忆化技术
关于 React 的记忆化(Memoization)原理、用法和适用场景的总结。
记忆化是指
在React中,通过进行记忆化能够重用缓存,从而避免组件不必要的重新渲染。
可以使用不同的方法对变量、函数和组件进行记忆化。
使用useMemo
useMemo是React的钩子函数,用于在渲染之间缓存计算结果。
它将用useMemo包装的函数结果保存到缓存中,并在重新渲染时从缓存中取出先前的结果。
语句结构
const result = useMemo(() => fn, [依存配列]);
示例程式
假设有一个父组件,即App组件,以及一个子组件,即TodoList组件。
App组件中有一个输入表单,当输入表单的值发生变化时,App组件和子组件TodoList都会重新渲染。
这时,在TodoList组件内部调用了一个进行复杂计算的函数。通常情况下,每次重新渲染时,这个复杂计算函数都会执行其处理过程。
const App: React.FC = memo(() => {
const { todos } = useTodos();
const [val, setVal] = useState("");
return (
<div>
<input
type="text"
value={val}
onChange={(ev) => setVal(ev.currentTarget.value)}
/>
<TodoList todos={todos} />
</div>
);
});
const TodoList: React.FC<{ todos: Todo[] }> = ({ todos }) => {
const calculatedTodos = heavyCalculate(todos); // 重い計算
return (
<ul>
{calculatedTodos.map((todo) => (
<li key={todo.id}>
<p>{todo.body}</p>
</li>
))}
</ul>
);
};
如果不进行记忆化,每次在App组件的输入表单中输入内容时,TodoList组件都会重新渲染,并且每次都会进行繁重的计算。在这种情况下,我们可以使用useMemo进行记忆化处理。
const TodoList: React.FC<{ todos: Todo[] }> = ({ todos }) => {
- const calculatedTodos = heavyCalculate(todos);
+ const calculatedTodos = useMemo(() => heavyCalculate(todos), [todos]);
return (
<ul>
{calculatedTodos.map((todo) => (
<li key={todo.id}>
<p>{todo.body}</p>
</li>
))}
</ul>
);
};
useMemo 的第一个参数是处理函数,第二个参数是依赖数组。
如果 useMemo 的第二个参数的依赖数组没有发生变化,它会返回先前保存的值,在依赖数组发生变化时,它会再次执行处理函数,并返回结果。
只要todos参数在上述代码中没有发生变化,calculatedTodos变量将从缓存中存储值。
即使输入表单发生了更改并且TodoList组件重新渲染,由useMemo包装的函数将返回缓存的值。通过这样做,之前重复执行的复杂处理只需在第一次渲染时完成,从而提高性能。
什么时候使用它?
- 値の計算コストが高い時
根据操作需要花费超过1毫秒的情况进行备忘,这是一个参考标准。
然而,并不需要对所有东西进行记忆化。useMemo本身的开销问题是其中原因,代码的可读性也会变差。
而且,如果依赖数组的声明出现错误,可能会导致无法正常运行。
我会介绍文档中提到的其他应该使用的情况。
如果使用useMemo进行的计算非常慢,并且其依赖值几乎不会改变,
则当将计算结果传递给用memo包装的组件的props时,如果值没有变化,我们希望跳过重新渲染。
通过使用记忆化,我们只能在依赖值不同时重新渲染组件。
这个值将被用作后续某些钩子的依赖值的情况。例如,如果另一个useMemo的计算结果依赖于该值,或者useEffect依赖于该值。
React.memo的备忘录
与 useMemo 类似,它是 React 中可以提高性能的功能。
memo 是 React 的 API,通过用 memo 包装组件,在组件中保存到缓存中。
当父组件进行重新渲染时,如果接收的 props 参数没有变化,组件将不会重新渲染。
结构
const MemorizedComponent: React.FC<Props> = React.memo(({props}) => {
return (
//
);
});
または
const Component: React.FC<Props> = ({props}) => {
return (
//
);
}
const MemorizedComponent = React.memo(Component);
如果发生再渲染的情况
当父组件传递给子组件的 props 与之前不同时,将触发重新渲染。
在记忆化组件中,当其自身的state发生变化时,将触发重新渲染。
此外,当组件内部调用的上下文发生变化时,也会触发重新渲染。
什么时候使用?
这对于在相同的 props 下频繁重新渲染且具有较高渲染成本的组件非常有用。
示例代码
const Heavy: React.FC<{label: string}> = ({ label }) => {
/*
とても重い描画処理を行う
*/
return (
<p>{label}</p>
);
};
const Parent: React.FC = () => {
console.log("Parent");
const [val, setVal] = useState("");
return (
<div>
<input onChange={(ev) => setVal(ev.target.value)} value={val} />
<Heavy label="Heavy Component" />
</div>
);
};
假设在 App 组件中有一个输入表单,并且调用了与输入表单无关的高成本渲染逻辑的 Heavy 组件。
每次在输入表单中输入内容时,App 组件都会重新渲染,子组件 Heavy 也会重新渲染。
由于重组件的重新渲染成本高,导致在输入表单时出现卡顿现象。
接下来,用备忘录进行包装。
+ const MemorizedHeavy = React.memo(Heavy);
const Parent: React.FC = () => {
console.log("Parent");
const [val, setVal] = useState("");
return (
<div>
<input onChange={(ev) => setVal(ev.target.value)} value={val} />
- <Heavy label="Heavy Component" />
+ <MemorizedHeavy label="Heavy Component" />
</div>
);
};
使用备忘录将Heavy组件进行内存化处理,可以消除卡顿现象。
使用useCallback
useCallback是React的一个钩子,用于在渲染之间缓存函数定义。
将函数包装在useCallback中,然后将其保存在缓存中,在重新渲染时可以从缓存中提取函数。
const cachedFn = useCallback(fn, [依存配列])
useMemo和useCallback的区别在于,useMemo用于缓存变量,而useCallback用于缓存函数。
useCallback 是在第二次渲染时比较依赖数组,并在上次相同的情况下返回缓存的函数,如果有变化则返回本次渲染传递的函数。
什么时候使用这个?
基本上与 memo 一起使用。
我们来看看将记忆化的子组件作为 props 传递函数时的行为差异。
const Child: React.FC<{ handleClick: () => void }> = React.memo(
({ handleClick }) => {
console.log("Child");
return <button onClick={handleClick}>ChildA</button>;
}
);
首先,将存储的组件作为props传递,并调用函数。
首先从不使用useCallback的情况开始。
const Parent: React.FC = () => {
console.log("Parent");
const [count, updateCount] = useState(0);
const handleClick = () => {
updateCount((count) => count + 1);
};
return (
<div>
<div>{count}</div>
<Child handleClick={handleClick} />
</div>
);
};
每当父组件的状态改变时,子组件也会重新渲染。
将 handleClick 函数用 useCallback 进行封装并进行记忆化后,将其传递给子组件。
const Parent: React.FC = () => {
console.log("Parent");
const [count, updateCount] = useState(0);
const handleClick = useCallback(() => {
updateCount((count) => count + 1);
}, []);
return (
<div>
<div>{count}</div>
<Child handleClick={handleClick} />
</div>
);
};
只有父组件被重新渲染。
这将判断作为 props 传递的函数在首次渲染时和之后的第二次渲染中是否完全相同,通过使用 Object.is 进行判断。
即使处理的函数相同,也不会认定为完全相同。
如果不使用 useCallback,每次渲染时会比较新生成的函数,如果发现传入了不同的函数,则认为它们是不同的函数。换句话说,会检测到与上次不同的 props,从而进行重新渲染。这不仅限于函数,也会在将对象作为 props 参数传递时发生。
总结
这次重新研究了一下在 React 中的记忆化,我意识到我之前仅仅是凭感觉使用了 useCallback,并没有打算进行性能调优。
此外,我也了解到使用记忆化的场景是有限的。但是,虽然有限,但这是必要的知识,我希望以后根据需要使用记忆化来优化性能。