前端小伙伴们,有没有被“组件性能优化”搞到头发掉?写个列表页,改个无关状态子组件就疯狂渲染;想用优化手段,又纠结用React.memo
还是shouldComponentUpdate
……今天咱们就用“快递检查”和“门卫大叔”的比喻,把这俩优化工具的区别讲得明明白白,再搞定深层对象比较的痛点!
先讲个我上周的真实案例:给客户做电商后台,有个OrderList
组件显示订单列表,还有个SearchFilter
组件处理搜索。结果发现,修改搜索关键词(父组件状态变化)时,OrderList
居然跟着重新渲染!明明订单数据(props.orders
)根本没变化,这不是纯浪费性能吗?
想优化吧,又犯难:
React.memo
吗?shouldComponentUpdate
?props
是嵌套对象,浅比较不管用咋办?这些问题总结成一句话:面对不同类型的组件和复杂的props结构,该选哪种优化方式?深层对象比较又该怎么处理?
要搞懂React.memo
和shouldComponentUpdate
,先想象一个场景:小区的快递驿站有两种“包裹检查员”——
React.memo
是React为函数组件设计的“记忆化工具”,相当于给组件装了个“智能快递柜”:
props
是否“看起来一样”(浅比较);string
/number
)直接比数值,对象/数组比“包裹皮”(引用地址);shouldComponentUpdate
(简称SCU
)是类组件的生命周期方法,相当于小区的“门卫大叔”:
props
和state
);true
(允许进入,重新渲染)或false
(拒绝进入,跳过渲染);维度 | React.memo | shouldComponentUpdate |
---|---|---|
组件类型 | 仅函数组件 | 仅类组件 |
触发时机 | 父组件渲染后,检查props 是否变化 |
类组件更新前,检查props /state |
比较范围 | 仅props |
props +state |
默认逻辑 | 浅比较props |
默认返回true (总是重新渲染) |
自定义能力 | 可选传入比较函数 | 必须手动实现比较逻辑 |
先看函数组件未优化的情况,再用React.memo
优化:
// 未优化的函数组件:每次父组件渲染都重新渲染
function OrderList({ orders }) {
console.log("OrderList重新渲染");
return (
<ul>
{orders.map(order => (
<li key={order.id}>{order.name}</li>
))}
</ul>
);
}
// 用React.memo优化(浅比较)
const MemoizedOrderList = React.memo(OrderList);
// 父组件:每次搜索词变化触发渲染
function Parent() {
const [search, setSearch] = useState('');
const [orders] = useState([{ id: 1, name: '订单1' }]); // 订单数据不变
return (
<div>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<MemoizedOrderList orders={orders} />
</div>
);
}
效果:
orders
的引用未变,React.memo
浅比较通过,OrderList
不重新渲染,无控制台输出。类组件未优化时,每次父组件渲染都会触发render
,用SCU
手动控制:
// 未优化的类组件:每次更新都渲染
class OrderList extends React.Component {
render() {
console.log("OrderList重新渲染");
return (
<ul>
{this.props.orders.map(order => (
<li key={order.id}>{order.name}</li>
))}
</ul>
);
}
}
// 用shouldComponentUpdate优化(自定义比较)
class OptimizedOrderList extends React.Component {
// 自定义比较逻辑:仅当orders的长度变化时重新渲染
shouldComponentUpdate(nextProps) {
return this.props.orders.length !== nextProps.orders.length;
}
render() {
console.log("OrderList重新渲染");
return (
<ul>
{this.props.orders.map(order => (
<li key={order.id}>{order.name}</li>
))}
</ul>
);
}
}
效果:
orders
长度未变,SCU
返回false
,render
不执行;orders
长度变化):SCU
返回true
,触发render
。当props
是深层对象(如{ user: { name: '张三' } }
),浅比较会失效(即使内容没变,引用变了)。这时候需要手动处理:
// 父组件:每次渲染重新创建user对象(引用变化)
function Parent() {
const [count, setCount] = useState(0);
const user = { name: '张三', address: { city: '北京' } }; // 每次渲染新对象
return (
<div>
<button onClick={() => setCount(c => c + 1)}>点击次数:{count}</button>
<MemoizedUserCard user={user} />
</div>
);
}
// 子组件:用React.memo包裹(浅比较)
const MemoizedUserCard = React.memo(({ user }) => {
console.log("UserCard重新渲染");
return (
<div>
<p>姓名:{user.name}</p>
<p>城市:{user.address.city}</p>
</div>
);
});
问题:点击按钮时,user
被重新创建(引用变化),React.memo
浅比较失败,UserCard
重新渲染。
用lodash.isEqual
深比较,或用useMemo
稳定引用:
// 方案1:自定义比较函数(深比较)
import isEqual from 'lodash.isEqual';
const MemoizedUserCard = React.memo(
({ user }) => { /* 渲染逻辑 */ },
(prevProps, nextProps) => {
// 用lodash的isEqual进行深比较
return isEqual(prevProps.user, nextProps.user);
}
);
// 方案2:用useMemo稳定对象引用(推荐)
function Parent() {
const [count, setCount] = useState(0);
// 仅当name或city变化时,才重新创建user对象
const user = useMemo(() => ({
name: '张三',
address: { city: '北京' }
}), []); // 依赖数组为空:只创建一次
return (
<div>
<button onClick={() => setCount(c => c + 1)}>点击次数:{count}</button>
<MemoizedUserCard user={user} />
</div>
);
}
用表格总结React.memo
和shouldComponentUpdate
的适用场景和注意事项:
优化方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
React.memo | 语法简洁,函数组件专用 | 默认浅比较,复杂对象需额外处理 | 函数组件,props为简单类型或稳定引用的对象 |
shouldComponentUpdate | 类组件专用,可控制props+state,自定义逻辑灵活 | 需手动实现比较逻辑,代码量较大 | 类组件,需精细控制state或深层props变化 |
深比较(如lodash.isEqual) | 准确判断深层对象变化 | 性能开销大(递归比较),可能比重新渲染更慢 | 极少数必须深层比较的场景(如嵌套层级少) |
useMemo稳定引用 | 性能最优(避免深比较) | 需正确管理依赖数组 | 函数组件,props为复杂对象/数组 |
“
React.memo
与shouldComponentUpdate
的核心区别体现在以下方面:
- 组件类型:
React.memo
用于函数组件,shouldComponentUpdate
用于类组件;- 比较范围:
React.memo
仅比较props
,shouldComponentUpdate
同时比较props
和state
;- 默认行为:
React.memo
默认浅比较props
,shouldComponentUpdate
默认返回true
(总是更新);- 自定义能力:
React.memo
可选传入比较函数,shouldComponentUpdate
必须手动实现逻辑。
处理深层对象比较时,推荐用useMemo
稳定对象引用(性能最优),或用lodash.isEqual
深比较(需谨慎,避免性能问题)。”
“
React.memo
就像函数组件的‘快递柜’——只看包裹的‘外皮’(引用地址),外皮没变就直接取上次的快递。shouldComponentUpdate
是类组件的‘门卫大叔’——不仅看外皮,还能打开包裹检查里面(自定义比较逻辑),甚至连你家的狗(state)有没有变都要问。
要是包裹里套包裹(深层对象),快递柜的外皮检查就不准了。这时候要么用useMemo
给包裹上把锁(稳定引用),要么让门卫大叔仔细翻(深比较),但翻太仔细会慢,所以尽量少用~”
state
或复杂props
时,手动实现比较逻辑;useMemo
/useCallback
避免对象/函数频繁创建,减少浅比较失败。lodash.isEqual
)的递归逻辑可能比重新渲染更慢,尤其对大对象;Immer
生成不可变对象,修改时返回新引用,配合useMemo
稳定未修改部分的引用:import { produce } from 'immer';
// 修改深层属性时,用Immer生成新对象
const newUser = produce(oldUser, draft => {
draft.address.city = '上海';
});
解答:不能!React.memo
仅比较props
,类组件的shouldComponentUpdate
才能比较state
。如果函数组件需要根据state
优化,需将state
作为props
传递(或用useMemo
缓存渲染逻辑)。
解答:函数组件可以通过React.memo
+自定义比较函数,模拟shouldComponentUpdate
的props
比较逻辑。如果需要比较state
,需将state
作为props
传递给自身(不推荐,会增加耦合)。
解答:不一定!深比较无法处理循环引用的对象(会导致无限递归),且对Date
/RegExp
等特殊对象的比较可能不符合预期(lodash.isEqual
已处理这些情况)。
解答:不影响!React.memo
仅阻止组件重新渲染,不影响useEffect
的执行(useEffect
依赖数组变化时仍会触发)。
React.memo
和shouldComponentUpdate
是React性能优化的“左右护法”,但选择哪一个、怎么用,关键在理解它们的“脾气”——函数组件用React.memo
,类组件用shouldComponentUpdate
,深层对象尽量用useMemo
稳定引用。
记住:优化的目标是“该渲染时渲染,不该渲染时不渲染”,而不是盲目追求所有组件都不渲染。下次遇到组件性能问题,你就能自信地说:“我知道该用哪个工具!”
如果这篇文章帮你理清了思路,记得点个赞,咱们下期聊,不见不散~