PureComponent继承自Component。PureComponent几乎和Component完全相同。
唯一不同之处在于Component的shouldComponentUpdate的默认实现是返回true。而PureComponent通过props和state的浅比较实现shouldComponentUpdate,某些情况下使用PureComponent可以减少不必要的渲染,提升性能。
之所以,Components的props和state每次发生变化时都会触发render。是因为Component的shouldComponentUpdate方法默认返回了true。而PureComponent的shouldComponentUpdate并非简单的返回了true。
当PureComponent组件更新时,如果组件的 props 和 state 都没发生改变, render 方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。具体就是 React 自动帮我们做了一层浅比较shallowEqual(即指针比较),如下是PureComponent的shouldComponentUpdate的源码:
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps)
|| !shallowEqual(inst.state, nextState);
}
关于浅比较的更多内容可以参考https://www.imweb.io/topic/598973c2c72aa8db35d2e291
当把之前和下一个的props和state作比较,浅比较将检查原始值是否有相同的值(例如:1 == 1或者ture==true);而对于数组和对象类型,将会比较引用是否相同。
1.可变数据变化前后不能使用同一个引用。 2.不变数据变化前后需使用同一个引用。
1> 使用PureComponent不要在props和state中改变对象和数组这种引用类型。即可变数据不能使用同一个引用。如果你在你的父组件中改变对象,你的“pure”子组件不将更新。虽然值已经被改变,但是子组件比较的是之前props的引用是否相同,所以不会检测到不同。因此,你可以通过使用es6的assign方法或者数组的扩展运算符或者使用第三方库,强制返回一个新的对象。
2> 不要在render的函数中绑定值。即不变数据变化前后需使用同一个引用。
假设你有一个项目列表,每个项目都传递一个唯一的参数到父方法。为了绑定参数,你可能会这么做:
<CommentItem likeComment={() => this.likeComment(user.id)} />
这个问题会导致每次父组件render方法被调用时,一个新的函数被创建,已将其传入likeComment。这会有一个改变每个子组件props的副作用,它将会造成他们全部重新渲染,即使数据本身没有发生变化。
为了解决这个问题,只需要将父组件的原型方法的引用传递给子组件。子组件的likeComment属性将总是有相同的引用,这样就不会造成不必要的重新渲染。
<CommentItem likeComment={this.likeComment} userID={user.id} />
然后再子组件中创建一个引用了传入属性的类方法:
class CommentItem extends PureComponent { ... handleLike() { this.props.likeComment(this.props.userID) } ... }
3> 不要在render方法里派生数据。即不变数据变化前后使用需同一个引用。
考虑一下你的配置组件将从一系列文章中展示用户最喜欢的十篇文章。
render() { const { posts } = this.props const topTen = posts.sort((a, b) => b.likes - a.likes).slice(0, 9) return //... }
每次组件重新渲染时topTen都将有一个新的引用,即使posts没有改变并且派生数据也是相同的。这将造成列表不必要的重新渲染。
你可以通过缓存你的派生数据来解决这个问题。例如,设置派生数据在你的组件state中,仅当posts更新时它才更新。
componentWillMount() { this.setTopTenPosts(this.props.posts) } componentWillReceiveProps(nextProps) { if (this.props.posts !== nextProps.posts) { this.setTopTenPosts(nextProps) } } setTopTenPosts(posts) { this.setState({ topTen: posts.sort((a, b) => b.likes - a.likes).slice(0, 9) }) }
如果你正在使用Redux,可以考虑使用reselect来创建"selectors"来组合和缓存派生数据。
4> 子组件数据更新,使用Immutable.js库解决数据不变问题。
有时候数组或对象内部依旧持有的是数组或对象,数据引用变化,虽然指针变了,但是内层数据实际上没变化,此时也会触发render。这个时候可以使用immutable-js函数库。
5> 复杂状态与简单状态不要共用一个组件
这点可能和 PureComponent 没多少关系,但做的不好可能会浪费很多性能,比如一个页面上面一部分是一个复杂的列表,下面是一个输入框,抽象代码:
change = (e) => {
this.setState({ value: e.target.value });
}
render() {
return (<div>
<ul>
{this.state.items.map((i, k) => <li key={k}> {...}</li>)}
</ul>
<input value={this.state.value} onChange={this.change} />
</div>)
}
表单和列表其实是没有什么关联的,表单的值也可能经常变动,但它的会给列表也带来必然的 diff操作,这是没必要的,最好是给列表抽出成一个单独的 PureComponent 组件,这样 state.items不变的话,列表就不会重新 render 了。
如果 PureComponent 里有 shouldComponentUpdate 函数的话,直接使用 shouldComponentUpdate 的结果作为是否更新的依据,没有 shouldComponentUpdate 函数的话,才会去判断是不是 PureComponent ,是的话再去做 shallowEqual 浅比较。
PureComponent
时问题会变得复杂。尽量让数据不可变,可以使用Immutable.js。render
方法中创建一个新的函数、对象或者是数组,那么你的做法(可能)是错误的。shallowEqual会比较 Object.keys(state | props) 的长度是否一致,每一个 key 是否两者都有,并且是否是一个引用,也就是只比较了第一层的值,确实很浅,所以深层的嵌套数据是对比不出来的。
const hasOwn = Object.prototype.hasOwnProperty
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}
export default function shallowEqual(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 (!hasOwn.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])) {
return false
}
}
return true
}
PureComponent
https://segmentfault.com/a/1190000014979065 https://wulv.site/2017-05-31/react-purecomponent.html https://juejin.im/post/5b614d9bf265da0fa759e84b
浅比较 https://www.imweb.io/topic/598973c2c72aa8db35d2e291
展示组件和容器组件 https://juejin.im/post/5a52fe32f265da3e317e008b https://juejin.im/post/5af3d66ef265da0b7b35ec1d