最近对项目中的组件进行了一些改造对组件添加了 PureRenderMixin,在改造过程中遇到一些问题,在这里做一个简单的记录。
示例:
假设有这样一个简单的 React 组件 App,其 state 里面有一个 items 属性为一个数组,render 方法将 items 里面的数据展示出来,并提供一个 按钮,用以添加数据(数据维护中常见的功能)。
// 改造前,注意 _addItem 实现
import React from 'react' import { render } from 'react-dom'
import PureRenderMixin from 'react-addons-pure-render-mixin' class App extends React.Component { constructor(){ this.state = { items: [] }
// 添加 PureRenderMixin 的语句
// this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate
// 通过 React.createClass 新建的组件可以直接在其 mixins: 属性中添加 PureRenderMixin 即可
// mixins: [PureRenderMixin] } render() { return (<div>
<button onClick={ this._addItem }> 添加随机数据 </button>
<ul> { this.state.items.map((item, idx)=> { return <li key={ idx }> {JSON.stringify(item)} </li> }) } </ul>
</div>)
}
_addItem() {
// 直接对 items 进行 push 操作,
let { items = [] } = this.state;
items.push(Math.ceil(Math.random()*1000));
this.setState({
items: items
})
}
}
上面的代码在未添加 PureRenderMixin 时正常运行,点击按钮正常调用 addItem 方法,并按照组件生命周期调用 render 方法组件刷新正常添加了 item 展示在页面上。
但是当添加了 PureRenderMixin 后 无论怎么点击 "添加随机数据",组件都不会调用 render 方法进行相应的刷新展示完整数据。通过工具查看 state.items 属性却是有正常的添加新值的。通过在 render 方法中打印 log 发现是数据变化后没有调用 render 方法重新渲染组件,所以组件展示没有变化。
原因(PureRenderMixin实现):
pureRenderMixin 的实现主要是修改了组件的 shouldComponentUpdate 方法的实现对于组件 state 或 props 变化过后先进行一个 shallowCompare(浅比较)的过程在决定是否需要 render 组件从而优化相关渲染性能。
实现代码主要为:
1 // 源文件地址: https://github.com/facebook/react/blob/master/src/addons/ReactComponentWithPureRenderMixin.js 2 var ReactComponentWithPureRenderMixin = { 3 shouldComponentUpdate: function(nextProps, nextState){ 4 return shallowCompare(this, nextProps, nextState); 5 } 6 }
上面的代码使用了一个 shallowCompare 的方法,实现代码如下:
1 // 源文件地址 https://github.com/facebook/react/blob/master/src/addons/shallowCompare.js 2 function shallowCompare(instance, nextProps, nextState) { 3 return ( 4 !shallowEqual(instance.props, nextProps) || 5 !shallowEqual(instance.state, nextState) 6 ); 7 }
可以看到主要的实现都放到 shallowEqual 这个方法里面,这个方法使用的是一个第三方库用于浅比较两个对象是否相等。
浅的意义在于,不会去迭代的对对象进行深度比较,只取得对象的 key 的值进行比较,对于基础类型直接比较值是否相等,对于引用类型只比较其引用是否相等。
简版的实现如下:
1 function shallowEqual(objA, objB) { 2 if(objA === objB) { 3 return true; 4 } 5 var keyA = Object.keys(objA), 6 keyB = Object.keys(objB); 7 8 if(keyA.length != keyB.length) { 9 return false; 10 } 11 12 for(var idx = 0, len = keyA.length; idx < len; idx++ ) { 13 var key = keyA[idx]; 14 15 if(!objB.hasOwnProperty(key)) { 16 return false; 17 } 18 var valueA = objA[key], 19 valueB = objB[key]; 20 // 无差别比较,引用类型比较引用,基础类型直接比较值 21 if(valueA !== valueB) { 22 return false; 23 } 24 } 25 return true; 26 }
完整 shallowEqual 实现代码地址: https://github.com/dashed/shallowequal/blob/master/src/index.js
将 PureRenderMixin的实现 结合到上面的例子分析:
我们的 _addItem 方法是直接对 state.items 进行 push 操作的,所以执行完 push 操作后 items === state.items 是一直返回 true 的。然后通过 setState 钩子去修改组件的 state 按照组件生命周期接下来组件将会执行 shouldComponentUpdate > componentWillUpdate > render > componentDidUpdate ,由于我们组件添加了 PureRenderMixin 替换了默认的 shouldComponentUpdate 方法,然后按照前面看到的 PureRenderMixin 代码对于 state 的比较会是通过 shallowEqual 方法返回值。而由于新设置的 items 属性和原本的 state.items 是同一对象所以返回结果为 true , 本例中没有 props 传递,也返回 true , 最终 shouldComponentUpdate 运算结果为 false(!true || !true) 所以没有调用 render 方法更新组件。
如何修改:
通过上面的分析我们已经知道问题的原因在于新设置的 items 数组与原来的 state.items 是同一对象保存着同一份引用,所以可以考虑重新构建一个对象使其与原来的 state.items 指向不同的引用即可解决上面的问题。
可选的解决方案: Immutable Data(React爬坑秘籍(一)——提升渲染性能),深拷贝(存在潜在的性能问题), ...(es 6解构,对应于对象的 Object.assign, 数组的 map,与Immutable Data 的思路一样)
这里由于是对数组进行的操作,直接通过 concat 返回一个新的数组对象,然后用这个新的数组对象设置为 items 属性即可。
修改后的 _addItems 方法:
_addItem() { // 直接对 items 进行 push 操作, let { items = [] } = this.state, newItems; newItems = items.concat(Math.ceil(Math.random()* 1000)); // newItems = [...items, Math.ceil(Math.random() * 1000)]; this.setState({ items: newItems }) }
总结:
在 JavaScript 中变量是可以是一个具体的值(简单类型)或执行一个引用(对象,数组等),所以在使用引用类型的时候要格外注意,不然可能导致程序出现一些意料之外的问题。
在使用 PureRenderMixin 或者 shallowEqual 时注意其进行的是浅比较,对于引用类型比较的是引用是否相同。