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 官方文档