React性能优化必看!memo与PureComponent在函数式和类组件中的差异与选择
在当今前端开发的激烈竞争中,React作为最受欢迎的JavaScript框架之一,其性能优化一直是前端工程师关注的焦点。在React的性能优化武器库中,memo和PureComponent是两把非常锋利的“宝剑”。它们能有效减少组件的不必要渲染,提升应用性能。但很多小伙伴对这两者在函数式组件和类组件中的性能优化差异感到困惑,不知道该如何选择合适的优化方式。今天,咱们就用大白话,掰开了、揉碎了好好聊聊!
一、React组件渲染机制基础
在深入了解memo和PureComponent之前,咱们先简单回顾一下React组件的渲染机制。在React中,组件就像是一个个“小零件”,当组件的props或者state发生变化时,这个“小零件”就会重新渲染。想象一下,你在组装一个玩具,某个零件一有变动,你就得重新把它装一遍,这就是组件的渲染。
不过,有时候这个“小零件”的变动其实并没有影响到它最终呈现的样子,但是React还是会重新渲染它,这就有点浪费性能了。就好比玩具上一个装饰小零件,不管怎么换位置,玩具整体功能和外观都没变化,但你还是重新装了一遍,是不是有点多余?而memo和PureComponent就是来解决这种“多余操作”的。
二、函数式组件的性能优化利器——memo
2.1 memo是什么?
memo是React提供给函数式组件的一个高阶组件(简单理解就是一个能包装其他组件的组件),它就像是一个“智能缓存器”。它可以帮我们记住函数式组件上一次渲染的结果,如果下一次渲染时传入的props没有变化,它就直接把上次的结果拿出来用,不再重新执行组件函数,从而避免了不必要的渲染。
2.2 如何使用memo?
使用memo非常简单,咱们来看段代码:
// 引入React和memo
import React, { memo } from'react';
// 定义一个普通的函数式组件
const MyComponent = (props) => {
console.log('MyComponent is rendering');
return (
<div>
<p>{props.message}</p>
</div>
);
};
// 使用memo包装组件
const MemoizedMyComponent = memo(MyComponent);
export default MemoizedMyComponent;
在上面的代码中:
- 首先从
react库中引入memo。 - 定义了一个普通的函数式组件
MyComponent,每次渲染时会在控制台打印MyComponent is rendering。 - 然后使用
memo包装MyComponent,得到MemoizedMyComponent。这样,只有当props发生变化时,MemoizedMyComponent才会重新渲染,否则就会使用缓存的结果。
2.3 memo的浅比较原理
memo内部使用的是浅比较(shallow compare)来判断props是否发生变化。所谓浅比较,就是只比较对象或者数组的第一层属性是否相同,不会深入比较对象内部的属性。比如:
import React, { memo } from'react';
const MyComponent = memo((props) => {
return (
<div>
<p>{props.data.name}</p>
</div>
);
});
const parentComponent = () => {
const data = { name: 'John' };
return (
<div>
{/* 第一次渲染 */}
<MyComponent data={data} />
{/* 这里只是重新赋值了data变量,但对象内部属性未变 */}
{() => {
const newData = data;
return <MyComponent data={newData} />;
}}
</div>
);
};
在上面的例子中,虽然newData和data指向同一个对象,MyComponent不会重新渲染,因为memo进行浅比较时发现props.data的引用没有变化。但如果这样改:
import React, { memo } from'react';
const MyComponent = memo((props) => {
return (
<div>
<p>{props.data.name}</p>
</div>
);
});
const parentComponent = () => {
const data = { name: 'John' };
return (
<div>
{/* 第一次渲染 */}
<MyComponent data={data} />
{/* 重新创建了一个新对象,即使属性值相同,引用不同 */}
{() => {
const newData = { name: 'John' };
return <MyComponent data={newData} />;
}}
</div>
);
};
这时MyComponent就会重新渲染,因为props.data的引用发生了变化,memo通过浅比较判断props改变了。
2.4 自定义比较函数
有时候浅比较不能满足我们的需求,我们希望能自定义比较逻辑,这时可以给memo传入第二个参数,一个自定义的比较函数。比如:
import React, { memo } from'react';
const areEqual = (prevProps, nextProps) => {
// 这里可以自定义比较逻辑,比如只比较特定属性
return prevProps.someProp === nextProps.someProp;
};
const MyComponent = memo((props) => {
return (
<div>
<p>{props.message}</p>
</div>
);
}, areEqual);
export default MyComponent;
在这个例子中,只有当props.someProp发生变化时,MyComponent才会重新渲染,其他属性变化都不会触发重新渲染。
三、类组件的性能优化选择——PureComponent
3.1 PureComponent是什么?
PureComponent是React中类组件(class - based component)的性能优化方案,它和memo有点类似,也是通过减少不必要的渲染来提升性能。PureComponent内部自动实现了shouldComponentUpdate方法(这是React中用于控制组件是否需要更新的一个生命周期方法),并且使用浅比较来比较props和state。
3.2 如何使用PureComponent?
使用PureComponent也不难,看代码:
import React, { PureComponent } from'react';
class MyComponent extends PureComponent {
render() {
console.log('MyComponent is rendering');
return (
<div>
<p>{this.props.message}</p>
</div>
);
}
}
export default MyComponent;
在这段代码中:
- 从
react库中引入PureComponent。 - 定义一个类组件
MyComponent继承自PureComponent。在render方法中,每次渲染会在控制台打印MyComponent is rendering。因为MyComponent继承自PureComponent,所以它会自动进行props和state的浅比较,只有当它们发生变化时才会重新渲染。
3.3 PureComponent的局限性
和memo一样,PureComponent也存在浅比较的局限性。如果props或者state中包含复杂对象或者数组,并且只是对象内部属性发生变化,而对象的引用没有变,PureComponent是检测不到变化的,也就不会重新渲染。例如:
import React, { PureComponent } from'react';
class MyComponent extends PureComponent {
render() {
return (
<div>
<p>{this.props.data.name}</p>
</div>
);
}
}
class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: { name: 'John' }
};
}
handleClick = () => {
// 这里只是修改了对象内部属性,对象引用未变
this.setState((prevState) => {
prevState.data.name = 'Jane';
return { data: prevState.data };
});
};
render() {
return (
<div>
<button onClick={this.handleClick}>Update</button>
<MyComponent data={this.state.data} />
</div>
);
}
}
在这个例子中,点击按钮更新data.name,但由于PureComponent进行浅比较时发现props.data的引用没有变化,所以MyComponent不会重新渲染,这就可能导致页面显示的数据不是最新的。
四、memo与PureComponent的差异对比
4.1 适用组件类型
memo:专门用于函数式组件,是函数式组件性能优化的首选方案。PureComponent:只能用于类组件,是类组件进行性能优化的常用手段。
4.2 比较方式
memo:默认对props进行浅比较,也可以通过传入自定义比较函数实现更灵活的比较逻辑。PureComponent:自动对props和state进行浅比较,开发者不能直接修改比较逻辑,但可以通过一些技巧(如不可变数据结构)来配合使用。
4.3 实现原理
memo:通过高阶函数的方式,包装函数式组件,缓存组件渲染结果。PureComponent:通过继承的方式,重写类组件的shouldComponentUpdate方法,实现基于浅比较的渲染控制。
五、如何根据组件类型选择合适的优化方式?
5.1 函数式组件的选择
如果你的项目使用的是函数式组件,优先选择memo进行性能优化。当组件的props相对简单,且不需要深入比较内部属性时,直接使用memo就能达到很好的效果。如果需要自定义比较逻辑,就传入自定义比较函数。
比如一个展示列表数据的函数式组件:
import React, { memo } from'react';
const ListItem = memo((props) => {
return (
<li>{props.item}</li>
);
});
const List = ({ items }) => {
return (
<ul>
{items.map((item) => (
<ListItem key={item.id} item={item.name} />
))}
</ul>
);
};
export default List;
在这个例子中,ListItem使用memo进行优化,只有当props.item发生变化时,ListItem才会重新渲染,能有效提升列表渲染性能。
5.2 类组件的选择
对于类组件,PureComponent是不错的选择。但要注意浅比较带来的问题,尽量使用不可变数据结构来更新state和props。例如,使用immer库来处理不可变数据:
import React, { PureComponent } from'react';
import produce from 'immer';
class MyComponent extends PureComponent {
constructor(props) {
super(props);
this.state = {
data: { name: 'John' }
};
}
handleClick = () => {
// 使用immer来更新state,确保生成新的对象引用
this.setState(
produce((draft) => {
draft.data.name = 'Jane';
})
);
};
render() {
return (
<div>
<button onClick={this.handleClick}>Update</button>
<p>{this.state.data.name}</p>
</div>
);
}
}
export default MyComponent;
通过这种方式,当state.data.name发生变化时,MyComponent就能正确检测到变化并重新渲染。
5.3 特殊情况
如果你的项目中既有函数式组件又有类组件,且存在一些复杂的场景,比如组件之间的数据传递非常复杂,可能需要结合memo和PureComponent,并配合其他性能优化手段(如useCallback、useMemo等)来实现最佳的性能优化效果。
六、总结
memo和PureComponent都是React性能优化中非常重要的工具。memo是函数式组件的“好帮手”,PureComponent则是类组件的“得力助手”。它们通过浅比较的方式减少不必要的渲染,但也都存在一定的局限性。作为前端工程师,我们需要根据组件类型、数据结构以及具体的业务场景,灵活选择合适的优化方式。掌握了它们,就能在React开发中打造出性能更优、体验更好的前端应用,让你的代码在前端性能优化的道路上一路狂飙!
以上就是关于React中memo与PureComponent性能优化差异及选择的详细介绍。你在实际项目中有用到这些优化技巧吗?有没有遇到过什么问题?欢迎在评论区交流分享!