React 源码漂流(三)之 PureComponent
一、PureComponent
PureComponent
最早在 React v15.3 版本中发布,主要是为了优化 React 应用而产生。
class Counter extends React.PureComponent { constructor(props) { super(props); this.state = {count: 1}; } render() { return ( <button color={this.props.color} onClick={() => this.setState(state => ({count: state.count + 1}))}> Count: {this.state.count} </button> ); } }
在这段代码中, React.PureComponent
会浅比较 props.color
或 state.count
是否改变,来决定是否重新渲染组件。
- 实现
React.PureComponent
和React.Component
类似,都是定义一个组件类。不同是React.Component
没有实现shouldComponentUpdate()
,而React.PureComponent
通过 props 和 state 的 浅比较 实现了。 - 使用场景
当
React.Component
的 props 和 state 均为基本类型,使用React.PureComponent
会节省应用的性能 可能出现的问题及解决方案
当props 或 state 为 复杂的数据结构 (例如:嵌套对象和数组)时,因为
React.PureComponent
仅仅是 浅比较 ,可能会渲染出 错误的结果 。这时有 两种解决方案 :- 当 知道 有深度数据结构更新时,可以直接调用 forceUpdate 强制更新
- 考虑使用 immutable objects (不可突变的对象),实现快速的比较对象
- 注意
React.PureComponent
中的shouldComponentUpdate()
将跳过所有子组件树的 prop 更新(具体原因参考 Hooks 与 React 生命周期:即:更新阶段,由父至子去判断是否需要重新渲染),所以使用 React.PureComponent 的组件,它的所有 子组件也必须都为 React.PureComponent 。
二、PureComponent 与 Stateless Functional Component
对于 React 开发人员来说,知道何时在代码中使用 Component,PureComponent 和 Stateless Functional Component 非常重要。
首先,让我们看一下无状态组件。
无状态组件
输入输出数据完全由 props
决定,而且不会产生任何副作用。
const Button = props => <button onClick={props.onClick}> {props.text} </button>
无状态组件可以通过减少继承 Component
而来的生命周期函数而达到性能优化的效果。从本质上来说,无状态组件就是一个单纯的 render
函数,所以无状态组件的缺点也是显而易见的。因为它没有 shouldComponentUpdate
生命周期函数,所以每次 state
更新,它都会重新绘制 render
函数。
React 16.8 之后,React 引入 Hooks 。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
何时使用 PureComponent
?
PureComponent
提高了性能,因为它减少了应用程序中的渲染操作次数,这对于复杂的 UI 来说是一个巨大的胜利,因此建议尽可能使用。此外,还有一些情况需要使用 Component
的生命周期方法,在这种情况下,我们不能使用无状态组件。
何时使用无状态组件?
无状态组件易于实施且快速实施。它们适用于非常小的 UI 视图,其中重新渲染成本无关紧要。它们提供更清晰的代码和更少的文件来处理。
三、PureComponent 与 React.memo
React.memo
为高阶组件。它实现的效果与 React.PureComponent
相似,不同的是:
React.memo
用于函数组件React.PureComponent
适用于 class 组件React.PureComponent
只是浅比较props
、state
,React.memo
也是浅比较,但它可以自定义比较函数
React.memo
function MyComponent(props) { /* 使用 props 渲染 */ } // 比较函数 function areEqual(prevProps, nextProps) { /* 如果把 nextProps 传入 render 方法的返回结果与 将 prevProps 传入 render 方法的返回结果一致则返回 true, 否则返回 false 返回 true,复用最近一次渲染 返回 false,重新渲染 */ } export default React.memo(MyComponent, areEqual);
React.memo
通过记忆组件渲染结果的方式实现 ,提高组件的性能- 只会对
props
浅比较,如果相同,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。 - 可以将自定义的比较函数作为第二个参数,实现自定义比较
- 此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,这会产生 bug。
- 与 class 组件中
shouldComponentUpdate()
方法不同的是,如果 props 相等,areEqual
会返回true
;如果 props 不相等,则返回false
。这与shouldComponentUpdate
方法的返回值相反。
四、使用 PureComponent 常见误区
误区一:在渲染方法中创建函数
如果你在 render
方法里创建函数,那么使用 props
会抵消使用 React.PureComponent
带来的优势。因为每次渲染运行时,都会分配一个新函数,如果你有子组件,即使数据没有改变,它们也会重新渲染,因为浅比较 props
的时候总会得到 false
。
例如:
// FriendsItem 在父组件引用样式 <FriendsItem key={friend.id} name={friend.name} id={friend.id} onDeleteClick={() => this.deleteFriends(friend.id)} /> // 在父组件中绑定 // 父组件在 props 中传递了一个箭头函数。箭头函数在每次 render 时都会重新分配(和使用 bind 的方式相同)
其中,FriendsItem
为 PureComponent
:
// 其中 FriendsItem 为 PureComponent class FriendsItem extends React.PureComponent { render() { const { name, onDeleteClick } = this.props console.log(`FriendsItem:${name} 渲染`) return ( <div> <span>{name}</span> <button onClick={onDeleteClick}>删除</button> </div> ) } } // 每次点击删除操作时,未删除的 FriendsItem 都将被重新渲染
这种在 FriendsItem
直接调用 () => this.deleteFriends(friend.id)
,看起来操作更简单,逻辑更清晰,但它有一个有一个最大的弊端,甚至打破了像 shouldComponentUpdate
和 PureComponent
这样的性能优化。
这是因为:父组件在 render
声明了一个函数onDeleteClick
,每次父组件渲染都会重新生成新的函数。因此,每次父组件重新渲染,都会给每个子组件 FriendsItem
传递不同的 props
,导致每个子组件都会重新渲染, 即使 FriendsItem
为 PureComponent
。
避免在 render 方法里创建函数并使用它。它会打破了像 shouldComponentUpdate 和 PureComponent 这样的性能优化。
要解决这个问题,只需要将原本在父组件上的绑定放到子组件上即可。FriendsItem
将始终具有相同的 props
,并且永远不会导致不必要的重新渲染。
// FriendsItem 在父组件引用样式 <FriendsItem key={friend.id} id={friend.id} name={friend.name} onClick={this.deleteFriends} />
FriendsItem
:
class FriendsItem extends React.PureComponent { onDeleteClick = () => { this.props.onClick(this.props.id) } // 在子组件中绑定 render() { const { name } = this.props console.log(`FriendsItem:${name} 渲染`) return ( <div> <span>{name}</span> <button onClick={this.onDeleteClick}>删除</button> </div> ) } } // 每次点击删除操作时,FriendsItem 都不会被重新渲染
通过此更改,当单击删除操作时,其他 FriendsItem
都不会被重新渲染了