使用 PureComponent 是优化 React 性能的一种常用手段, 相较于 Component, PureComponent 会在 render 之前自动执行一次 shouldComponentUpdate() 函数, 根据返回的 bool 值判断是否进行 render。其中有个重点是 PureComponent 在 shouldComponentUpdate() 的时候会进行 shallowEqual(浅比较)。
PureComponent 的浅比较策略如下:
对 prevState/nextState 以及 prevProps/nextProps 这两组数据进行浅比较:
1.对象第一层数据未发生改变, render 方法不会触发; 2.对象第一层数据发生改变(包括第一层数据引用的改变), render 方法会触发;
照着上述思路我们来实现 PureComponent 的逻辑
function PureComponent(props) {this.props = props || {}this.state = {}isShouldComponentUpdate.call(this) // 为每个 PureComponent 绑定 shouldComponentUpdate 方法}PureComponent.prototype.setState = function(updater, cb) {isShouldComponentUpdate.call(this) // 调用 setState 时, 让 this 指向子类的实例, 目的取到子类的 this.stateasyncRender(updater, this, cb)}function isShouldComponentUpdate() {const cpState = this.stateconst cpProps = this.propsthis.shouldComponentUpdate = function (nextProps, nextState) {if (!shallowEqual(cpState, nextState) || !shallowEqual(cpProps, nextProps)) {return true // 只要 state 或 props 浅比较不等的话, 就进行渲染} else {return false // 浅比较相等的话, 不渲染}}}// 浅比较逻辑const shallowEqual = function(oldState, nextState) {const oldKeys = Object.keys(oldState)const newKeys = Object.keys(nextState)if (oldKeys.length !== newKeys.length) {return false}let flag = truefor (let i = 0; i < oldKeys.length; i++) {if (!nextState.hasOwnProperty(oldKeys[i])) {flag = falsebreak}if (nextState[oldKeys[i]] !== oldState[oldKeys[i]]) {flag = falsebreak}}return flag}
测试用例用 在 React 上提的一个 issue 中的案例, 我们期望点击增加按钮后, 页面上显示的值能够加 1。
class B extends PureComponent {constructor(props) {super(props)this.state = {count: 0}this.click = this.click.bind(this)}click() {this.setState({count: ++this.state.count,})}render() {return (<div><button onClick={this.click}>增加</button><div>{this.state.count}</div></div>)}}
然而, 我们点击上述代码, 页面上显示的 0 分毫不动!!!
揭秘如下:
click() {const t = ++this.state.countconsole.log(t === this.state.count) // truethis.setState({count: t,})}
当点击增加按钮, 控制台显示 t === this.state.count
为 true, 也就说明了 setState 前后的状态是统一的, 所以 shallowEqual(浅比较) 返回的是 true, 致使 shouldComponentUpdate 返回了 false, 页面因此没有渲染。
类似的, 如下写法也是达不到目标的, 留给读者思考了。
click() {this.setState({count: this.state.count++,})}
那么如何达到我们期望的目标呢。揭秘如下:
click() {this.setState({count: this.state.count + 1})}
感悟: 小小的一行代码里蕴藏着无数的 bug。