本文基于React^15.6.1
Redux^3.7.1
Immutable^4.0.0-rc.2
Immutable.js
Immutable Data
Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
React 性能问题
React的生命周期函数shuoldComponentUpdate
默认是返回true
, 这样的话每次state
或者props
改变的时候都会进行render
,在render 的过程当就是最消耗性能的过程.所以在生命周期函数 shuoldComponentUpdate
中实现性能优化.
-
可以使用
PrueComponent
,PrueComponent
是继承至Component
PrueComponent
默认进行了一次shallowCompare浅比较,所谓浅比较就是只比较第一级的属性是否相等相关源码
node_modules/react/lib/ReactBaseClasses.js
function ReactPureComponent(props, context, updater) { // Duplicated from ReactComponent. this.props = props; this.context = context; this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the // renderer. this.updater = updater || ReactNoopUpdateQueue; } function ComponentDummy() {} ComponentDummy.prototype = ReactComponent.prototype; ReactPureComponent.prototype = new ComponentDummy(); ReactPureComponent.prototype.constructor = ReactPureComponent; // Avoid an extra prototype jump for these methods. //这里只是简单的将ReactComponent的原型复制到了ReactPureComponent的原型 _assign(ReactPureComponent.prototype, ReactComponent.prototype); ReactPureComponent.prototype.isPureReactComponent = true; module.exports = { Component: ReactComponent, PureComponent: ReactPureComponent };
node_modules/react-dom/lib/ReactCompositeComponent.js
updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) { *** ... var nextState = this._processPendingState(nextProps, nextContext); var shouldUpdate = true; if (!this._pendingForceUpdate) { if (inst.shouldComponentUpdate) { if (process.env.NODE_ENV !== 'production') { shouldUpdate = measureLifeCyclePerf(function () { return inst.shouldComponentUpdate(nextProps, nextState, nextContext); }, this._debugID, 'shouldComponentUpdate'); } else { shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext); } } else { if (this._compositeType === CompositeTypes.PureClass) { shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState); } } } *** ... },
再来看看
shallowEqual
node_modules/fbjs/lib/shallowEqual.js
var shallowEqual = require('fbjs/lib/shallowEqual'); *** // line 23 function is(x, y) { // SameValue algorithm if (x === y) { // Steps 1-5, 7-10 // Steps 6.b-6.e: +0 != -0 // Added the nonzero y check to make Flow happy, but it is redundant return x !== 0 || y !== 0 || 1 / x === 1 / y; } else { // Step 6.a: NaN == NaN return x !== x && y !== y; } } // line 41 function shallowEqual(objA, objB) { if (is(objA, objB)) { return true; } if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false; } var keysA = Object.keys(objA); var keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } // Test for A's keys different from B. for (var i = 0; i < keysA.length; i++) { if (!hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { return false; } } return true; }
可以看到shallowEqual只对object的第一级属性进行比较
所以在基本数据类型之下我们可以直接继承PureComponent
就能提升性能,比如一个最为普通的场景import React, { PureComponent } from 'react'; *** class MainPage extends PureComponent{ constructor(props,context){ super(props); this.props = props; this.state = { open: false }; this.toggleMenu = this.toggleMenu.bind(this); } toggleMenu() { this.setState({ open: !this.state.open }); } componentWillUnmount(){ console.log('componentWillUnmount-----mainpage') } render(){ let {match,location,localLang} = this.props; return (
} onLeftIconButtonTouchTap={this.toggleMenu} /> 此处在点击按钮的时候直接调用
toggleMenu
执行其中的setState
方法,'open'本来就是基本数据类型,即使不重写shouldComponentUpdate,PureComponent的shouldComponentUpdate也完全能够对其进行处理.进一步我们可以看看state中处理引用数据类型的情况
最熟悉不过可能就是列表渲染,并更改列表状态首先我们直接继承
PureComponent
并不对数据进行Immutable
的转化class Item extends PureComponent{ constructor(props){ super(props); this.operatePositive = this.operatePositive.bind(this) this.operateNegative = this.operatePositive.bind(this) } static propTypes = { tile: PropTypes.object.isRequired, operate: PropTypes.func.isRequired } operatePositive(){ let id = this.props.tile._id; this.props.operate(true,id) } operateNegative(){ let id = this.props.tile._id; this.props.operate(false,id) } render(){ console.log('render item') let {tile} = this.props; return(
{ tile.operated ? null: ) } } class Check extends PureComponent{ static propTypes = { lang: PropTypes.string.isRequired } constructor(props){ super(props) this.state = { list: [], count:{ blocked:0, passed:0, unusable:0 } } this.operate = this.operate.bind(this) } operate(usable,itemID){ console.log('----operate----') let list = this.state.list.map(item=>{ if(item.get('_id') == itemID){ return item.update('operated',false,(val)=>!val) } return item }) console.log(is(this.state.list,list)) this.setState({ list }) } getList(isInitial){ if(this.noMore) return false; let { lang } = this.props; let paramObj = { pageSize: 10 }; if(isInitial){ paramObj.properties = 'count'; } $get(`/api/multimedia/check/photos/${lang}`, paramObj) .then((res)=>{ let {data} = res; let obj = { list: data }; if(data.length < paramObj.pageSize){ this.noMore = true; } this.setState(obj) }) .catch(err=>{ console.log(err) }) } componentWillMount(){ this.getList('initial'); } componentWillUnmount(){ console.log('-----componentWillUnmount----') } render(){ let {list,count} = this.state; return(done} /> block} /> { list.length ? {list.map((tile,index) => ({count.blocked || 0} blocked {count.passed || 0} passed {count.unusable || 0} remaining - ))}
初始化渲染并没有什么问题,直接执行了10次
Item
的render
当点击操作按钮的时候问题来了,么有任何反应,经过检测原来是继承了PureComponent
,在shouldComponentUpdate
返回了false,在看看shallowEqual
源码,确实返回了false这样的话我们先直接继承
Component
,理论上任何一次setState
都会render 10次了class Item extends Component{ *** *** }
这次果然
render
了10次
-
使用
Immutable
进行优化此处需要注意,
Immutable.js
本身的入侵性还是比较强,我们在改造过程中需要注意与现有代码的结合这里我们可以遵循几个规则
- 在通过props传递的数据,必须是
Immutable
的 - 在组件内部的state可以酌情考虑是否需要
Immutable
- 基本数据类型(
Bool
,Number
,String
等)可以不进行Immutable
处理 - 引用数据类型(
Array
,Object
)建议直接Immutable.fromJS
转成Immutable
的对象
- 基本数据类型(
- ajax返回的数据我们可以根据第2点直接进行转化
- 在通过props传递的数据,必须是
所以对代码进行如下改造
@pure
class Item extends PureComponent{
constructor(props){
super(props);
this.operatePositive = this.operatePositive.bind(this)
this.operateNegative = this.operatePositive.bind(this)
}
static propTypes = {
tile: PropTypes.object.isRequired,
operate: PropTypes.func.isRequired
}
operatePositive(){
let id = this.props.tile.get('_id');
this.props.operate(true,id)
}
operateNegative(){
let id = this.props.tile.get('_id');
this.props.operate(false,id)
}
render(){
console.log('render item')
let {tile} = this.props;
return(
done}
/>
block}
/>
)
}
}
class Check extends PureComponent{
static propTypes = {
lang: PropTypes.string.isRequired
}
constructor(props){
super(props)
this.state = {
list: List([])
}
this.operate = this.operate.bind(this)
}
operate(usable,itemID){
let list = this.state.list.map(item=>{
if(item._id == itemID){
item.operated = true;
}
return item
})
console.log('----operate----')
this.setState({
list
})
}
getList(isInitial){
if(this.noMore) return false;
let { lang } = this.props;
let paramObj = {
pageSize: 10
};
$get(`/api/multimedia/check/photos/${lang}`, paramObj)
.then((res)=>{
let {data} = res;
//重点当ajax数据返回之后引用数据类型直接转化成Immutable的
let obj = {
list: fromJS(data)
};
this.setState(obj)
})
.catch(err=>{
console.log(err)
})
}
componentWillMount(){
this.getList('initial');
}
componentWillUnmount(){
console.log('-----componentWillUnmount----')
}
render(){
let {list,count} = this.state;
return(
{list.map((tile) => - )}
);
}
}
当点击操作按钮的之后,最终Item
的render调用如下
这里我们使用了一个装饰器
@pure
具体逻辑可以根据项目数据结构进行实现,如下代码还有可以改进空间
export const pure = (component)=>{
component.prototype.shouldComponentUpdate = function(nextProps,nextState){
let thisProps = this.props;
let thisState = this.state;
// console.log(thisState,nextState)
// if (Object.keys(thisProps).length !== Object.keys(nextProps).length ||
// Object.keys(thisState).length !== Object.keys(nextState).length) {
// return true;
// }
if(thisProps != nextProps){
for(const key in nextProps){
if(isImmutable(thisProps[key])){
if(!is(thisProps[key],nextProps[key])){
return true
}
}else{
if(thisProps[key]!= nextProps[key]){
return true;
}
}
}
}else if(thisState != nextState){
for(const key in nextState){
if(isImmutable(thisState[key])){
if(!is(thisState[key],nextState[key])){
return true
}
}else{
if(thisState[key]!= nextState[key]){
return true;
}
}
}
}
return false;
}
}
结合 redux
现在我们将刚才的组件代码融入进redux
的体系当中
首先我们使用了redux-immutable,将初始化state进行了Immutable的转化
然后我们从组件触发理一下思路,结合上文中Immutable对象
通过`redux`的`connect`关联得到的数据,最终是通过组件的`props`向下传导的,所以`connect`所关联的数据必须是`Immutable`的,这样一来事情就好办了,我们可以在`reducer`里面进行统一处理,所有通过`redux`处理过的`state`必须是`Immutable`的,这就能保证所有的组件通过`connect`得到的属性必然也是`Immutable`的
实现如下
//store.js
import { createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import Immutable from 'immutable';
import rootReducer from '../reducers';
let middleWareArr = [thunk];
//初始化store,注意需要建立Immutable的初始化state,详细文档可以查阅[redux-immutable](https://github.com/gajus/redux-immutable)
const initialState = Immutable.Map();
let store = createStore(rootReducer, initialState, applyMiddleware(...middleWareArr));
export default store;
// reducer.js
import { Map, List, fromJS, isImmutable } from 'immutable';
// 注意这里的`combineReducers` 是从`redux-immutable`引入的,可以查阅其详细文档,这里的主要作用是将导出的reducers转化成Immutable的
import { combineReducers } from 'redux-immutable';
import * as ActionTypes from '~actions/user';
let authorState = Map({
checked: false
})
let author = (state = authorState, action)=>{
switch(action.type){
case ActionTypes.CHANGE_AUTHOR_STATUS:
state = state.set('checked',true)
break;
}
return state;
}
export default combineReducers({
author
});
//app.js
const mapStateToProps = (state)=>{
//这里传入的state是`Immutable`的对象,等待传入组件的`props`直接获取即可
let author = state.getIn(['User','author'])
return {
author
}
}
@connect(mapStateToProps)
@prue
class AuthorApp extends PureComponent{
static propTypes = {
dispatch: PropTypes.func.isRequired,
author: PropTypes.object.isRequired
}
render(){
let { author } = this.props;
//author.getIn('checked') 这里是获得真正需要的js属性了
return (
author.getIn('checked') ? :
);
}
}
export default AuthorApp;
至此我们已经将Immutable和redux进行了结合.
总结如下
- 通过redux-immutable将初始化的state和所有reducer暴露出的对象转成Immutable对象
- 在所有容器组件与reducer的连接函数
connect
中对组件的props属性进行统一Immutable
的限定,保证组件内部能直接访问Immutable
的props - 在reducer中对action传入的数据进行Immutable话,返回一个Immutable的state
这样一来,Immutable就和redux集成到了一起