react重复渲染 for ItemList(列表项)
先看效果,左边是badCase(一般的写法),中间是用reducer来状态管理,右边是用memo和callback
可以看到第一种每一个子项都被渲染了(1+6+5)次,2,3是我们希望的,其中2的父组件没有被重复渲染,是最优的方法
React 基本的渲染逻辑
我们都知道,React 是通过虚拟 DOM 树来进行渲染的。当我们每次调用 setState 时,都会引起整个虚拟 DOM 树的重新构建,然后通过 Fiber 算法对比得出需要渲染的部分。
即使不使用 memo 和 useCallback,React 也有一个基本的优化逻辑:
React 默认的优化逻辑:虽然 React 渲染是从根节点开始的,但在遍历过程中,如果发现节点本身以及祖先节点没有更新,而是其子树发生了更新,那么该节点也不会被重新渲染。
父组件和子组件的渲染关系
- 父组件的重复渲染会导致子组件全部重复渲染。
- 单一子组件的渲染在一般情况下不会引起父组件的渲染。
在处理列表项时,我们希望列表项是独立的,其数据变化不会引起其他列表项的变化。然而,通常情况下会出现父组件被重复渲染的情况,这会导致所有子组件也被重新渲染。
不使用memo和callback,避免父组件重复渲染
const [items, setItems] = useState([
{ id: 1, renderTime: 0, count: 0 },
{ id: 2, renderTime: 0, count: 0 },
{ id: 3, renderTime: 0, count: 0 },
]);
为什么用useState本质上是要去改变状态,但是会有副作用
每次对item的更改都会引起state的变化
而state变化不会有React 默认的优化逻辑(子项变化,父项不重新渲染),就会导致react默认的优化失效
我们可以通过以下方式避免父组件的重复渲染:
- 使用
useReducer代替useState:当我们处理后端返回的数据列表时,使用useState保存列表项会导致父组件的重新渲染。通过useReducer可以避免这种情况,因为useReducer可以更细粒度地控制状态更新,从而避免不必要的重新渲染。
import React, { useReducer } from "react";
let fatherRenderTime = 0;
const itemReducer = (state, action) => {
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1 };
default:
return state;
}
};
const Item = ({ item }) => {
const [state, dispatch] = useReducer(itemReducer, item);
return (
<div>
<div>itemRenderTime: {state.renderTime++}</div>
<div>Count: {state.count}</div>
<button onClick={() => dispatch({ type: "increment" })}>
Increment Count
</button>
</div>
);
};
export function ReducerSolution() {
const items = [
{ id: 1, renderTime: 0, count: 0 },
{ id: 2, renderTime: 0, count: 0 },
{ id: 3, renderTime: 0, count: 0 },
];
return (
<div>
<div>
<div>fatherRenderTime: {fatherRenderTime++}</div>
{items.map((item) => (
<Item key={item.id} item={item} />
))}
</div>
</div>
);
}
export default ReducerSolution;
使用 useMemo 和 useCallback
在一些复杂的项目中,useMemo 和 useCallback 可以减少代码改动,同时达到优化渲染的效果。
useMemo和useCallback的核心点:避免父组件的渲染导致不应该被重复渲染的子组件被重复渲染。- 依赖项的作用:当依赖项没有发生变化时,React 会使用缓存,否则会重新渲染。这解释了为什么
useCallback和useMemo需要一起使用,因为依赖项中通常包含需要调用的函数。如果不使用useCallback,函数会发生变化,导致依赖项变化,从而使useMemo失效。
import React, { useState, memo, useCallback } from "react";
let fatherRenderTime = 0;
const Item = memo(({ item, onIncrement, add }) => {
return (
<div>
<div>itemRenderTime: {item.renderTime++}</div>
<div>Count: {item.count}</div>
<button onClick={() => onIncrement(item.id)}>Increment Count</button>
{/* <button onClick={() => add(item.id)}>Increment Count</button> */}
</div>
);
});
export function MemoAndCallBackSolution() {
const [items, setItems] = useState([
{ id: 1, renderTime: 0, count: 0 },
{ id: 2, renderTime: 0, count: 0 },
{ id: 3, renderTime: 0, count: 0 },
]);
const incrementCount = useCallback((id) => {
setItems((prevItems) =>
prevItems.map((item) =>
item.id === id ? { ...item, count: item.count + 1 } : item
)
);
}, []);
const add = (id) => {
setItems((prevItems) =>
prevItems.map((item) =>
item.id === id ? { ...item, count: item.count + 1 } : item
)
);
};
return (
<div>
<div>fatherRenderTime: {fatherRenderTime++}</div>
{items.map((item) => (
<Item
key={item.id}
item={item}
onIncrement={incrementCount}
// add={add}
/>
))}
</div>
);
}
export default MemoAndCallBackSolution;
s
示例流程
father组件渲染 ---1-----> son组件渲染 -----2---> father组件渲染
- 1 是正常现象,但可以通过
memo和useCallback来优化(实际上也会重复渲染,但使用的是缓存,没有增加性能消耗)。优化的条件是依赖项没有发生变化。 - 2 是 React 自身的优化逻辑,一般情况下 React 组件都会遵循。我们在列表处理时容易重复渲染的主要原因是使用了
useState去保存后端传递的列表项。
结论
通过这篇文章,我个人对 useMemo 和 useCallback 有了更深入的了解,也解释了一些问题(为什么有时候加了和没加一样,有时候单独用不起效果)。
建议:对于复杂的列表项操作,可以考虑使用 useReducer 代替 useState。一般情况下,父组件不会把子组件的状态全部包揽在一个 state 里,但列表项是个例外。
希望这篇文章对你有所帮助。
资料来源:
- 掘金文章
- React 官方文档