一、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 为 PureComponentclass 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
都不会被重新渲染了 ????
误区二:在渲染方法中派生 state
考虑一个文章列表,您的个人资料组件将从中显示用户最喜欢的 10 个作品。
render() { const { posts } = this.props // 在渲染函数中生成 topTen,并渲染 const topTen = [...posts].sort((a, b) => b.likes - a.likes).slice(0, 9) return //...}// 这会导致组件每次重新渲染,都会生成新的 topTen,导致不必要的渲染
topTen
每次组件重新渲染时都会有一个全新的引用,即使 posts
没有更改,派生 state
也是相同的。
这个时候,我们应该将 topTen
的判断逻辑提取到 render
函数之外,通过缓存派生 state
来解决此问题。
例如,在组件的状态中设置派生 state
,并仅在 posts
已更新时更新。
componentWillMount() { this.setTopTenPosts(this.props.posts)}componentWillReceiveProps(nextProps) { if (this.props.posts !== nextProps.posts) { this.setTopTenPosts(nextProps.posts) }}// 每次 posts 更新时,更新派生 state,而不是在渲染函数中重新生成setTopTenPosts(posts) { this.setState({ topTen: [...posts].sort((a, b) => b.likes - a.likes).slice(0, 9) })}
总结
在使用 PureComponent
时,请注意:
- 突变一般是不好的,但在使用
PureComponent
时,问题会更加复杂。 - 不要在渲染方法中创建新函数、对象或数组,这不导致项目性能显著降低。
五、PureComponent 源码解析
// 新建了空方法ComponentDummy ,ComponentDummy 的原型 指向 Component 的原型;function ComponentDummy() {}ComponentDummy.prototype = Component.prototype;/** * Convenience component with default shallow equality check for sCU. */function PureComponent(props, context, updater) { this.props = props; this.context = context; // If a component has string refs, we will assign a different object later. this.refs = emptyObject; this.updater = updater || ReactNoopUpdateQueue;} // 解析同 React.Component,详细请看上一章/** * 实现 React.PureComponent 对 React.Component 的原型继承 *//** * 用 ComponentDummy 的原因是为了不直接实例化一个 Component 实例,可以减少一些内存使用 * * 因为,我们这里只需要继承 React.Component 的 原型,直接 PureComponent.prototype = new Component() 的话 * 会继承包括 constructor 在内的其他 Component 属性方法,但是 PureComponent 已经有自己的 constructor 了, * 再继承的话,造成不必要的内存消耗 * 所以会新建ComponentDummy,只继承Component的原型,不包括constructor,以此来节省内存。 */const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());// 修复 pureComponentPrototype 构造函数指向pureComponentPrototype.constructor = PureComponent;// Avoid an extra prototype jump for these methods.// 虽然上面两句已经让PureComponent继承了Component// 但多加一个 Object.assign(),能有效的避免多一次原型链查找Object.assign(pureComponentPrototype, Component.prototype);// 唯一的区别,原型上添加了 isPureReactComponent 属性去表示该 Component 是 PureComponent// 在后续组件渲染的时候,react-dom 会去判断 isPureReactComponent 这个属性,来确定是否浅比较 props、status 实现更新 /** 在 ReactFiberClassComponent.js 中,有对 isPureReactComponent 的判断 if (ctor.prototype && ctor.prototype.isPureReactComponent) { return ( !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) ); } */pureComponentPrototype.isPureReactComponent = true;
这里只是 PureComponent
的声明创建,至于如何实现 shouldComponentUpdate()
,核心代码在:
// ReactFiberClassComponent.jsfunction checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext,) { // ... if (ctor.prototype && ctor.prototype.isPureReactComponent) { // 如果是纯组件,比较新老 props、state // 返回 true,重新渲染, // 即 shallowEqual props 返回 false,或 shallowEqual state 返回 false return ( !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) ); } return true;}
shallowEqual.js
/** * 通过遍历对象上的键并返回 false 来执行相等性 * 在参数列表中,当任意键对应的值不严格相等时,返回 false。 * 当所有键的值严格相等时,返回 true。 */function shallowEqual(objA: mixed, objB: mixed): boolean { // 通过 Object.is 判断 objA、objB 是否相等 if (is(objA, objB)) { return true; } if ( typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null ) { return false; } // 参数列表 const keysA = Object.keys(objA); const keysB = Object.keys(objB); // 参数列表长度不相同 if (keysA.length !== keysB.length) { return false; } // 比较参数列表每一个参数,但仅比较一层 for (let i = 0; i < keysA.length; i++) { if ( !hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]]) ) { return false; } } return true;}
附:Object.is(来自MDN)
Object.is()
判断两个值是否相同。
这种相等性判断逻辑和传统的 ==
运算不同,==
运算符会对它两边的操作数做隐式类型转换(如果它们类型不同),然后才进行相等性比较,(所以才会有类似 "" == false
等于 true
的现象),但 Object.is
不会做这种类型转换。
这与 ===
运算符的判定方式也不一样。===
运算符(和==
运算符)将数字值 -0
和 +0
视为相等,并认为 Number.NaN
不等于 NaN
。
如果下列任何一项成立,则两个值相同:
- 两个值都是
undefined
- 两个值都是
null
- 两个值都是
true
或者都是false
- 两个值是由相同个数的字符按照相同的顺序组成的字符串
- 两个值指向同一个对象
两个值都是数字并且
- 都是正零
+0
- 都是负零
-0
- 都是
NaN
- 都是除零和
NaN
外的其它同一个数字
- 都是正零
走在最后,欢迎关注:前端瓶子君