redux 和 Immutable实践

本文基于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 中实现性能优化.

  1. 可以使用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 (
               <div>
                  <AppBar
                   title={null}
                   iconElementRight={<UserHeader lang={localLang}/>}
                   onLeftIconButtonTouchTap={this.toggleMenu}
                 />
               </div>
             );
           }
     }
    
    

    此处在点击按钮的时候直接调用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(
                     <GridTile
                         className={cx('grid-box')}
                         >
                         <img src={tile.images[0].thumb} />
                         {
                             tile.operated ? null:
                             <div className={cx('decide-box')}>
                         
                             <RaisedButton
                                 label="PASS"
                                 primary={true}
                                 onTouchTap = {this.operatePositive}
                                 icon={<FontIcon className="material-icons">done</FontIcon>}
                             />
                             <RaisedButton
                                 label="BLOCK"
                                 onTouchTap = {this.operateNegative}
                                 icon={<FontIcon className="material-icons">block</FontIcon>}
                             />
                         </div>
                         }
                         
                     </GridTile>
                 )
             }
         }
         
     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(
                 <GridList
                     cellHeight={'auto'}
                     cols={4}
                     padding={1}
                     className={cx('root')}
                 >
                <Subheader>
                    {
                        list.length ?  <div className={cx('count-table')}>
                             <Chip>{count.blocked || 0} blocked</Chip><Chip>{count.passed || 0} passed</Chip><Chip>{count.unusable || 0} remaining</Chip>
                         </div> : null
                    }
                 </Subheader>
                 {list.map((tile,index) => (
                    <Item tile={tile} key={index} operate={this.operate}></Item>
                 ))}
                 </GridList>
             );
         }
     }
    
    

    初始化渲染并没有什么问题,直接执行了10次Itemrender


    当点击操作按钮的时候问题来了,么有任何反应,经过检测原来是继承了PureComponent,在shouldComponentUpdate返回了false,在看看shallowEqual源码,确实返回了false

    这样的话我们先直接继承Component,理论上任何一次setState都会render 10次了

    class Item extends Component{
        ***
        ***
    }
    

    这次果然render了10次

  1. 使用Immutable进行优化

    此处需要注意, Immutable.js本身的入侵性还是比较强,我们在改造过程中需要注意与现有代码的结合

    这里我们可以遵循几个规则

    1. 在通过props传递的数据,必须是Immutable
    2. 在组件内部的state可以酌情考虑是否需要Immutable
      1. 基本数据类型(Bool,Number,String等)可以不进行Immutable处理
      2. 引用数据类型(Array,Object)建议直接Immutable.fromJS转成Immutable的对象
    3. ajax返回的数据我们可以根据第2点直接进行转化

所以对代码进行如下改造

 @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(
             <GridTile
                 className={cx('grid-box')}
                 >
                 <img src={tile.getIn(['images',0,'thumb'])} />
                 <div className={cx('decide-box')}>
                     <RaisedButton
                         label="PASS"
                         primary={true}
                         onTouchTap = {this.operatePositive}
                         icon={<FontIcon className="material-icons">done</FontIcon>}
                     />
                     <RaisedButton
                         label="BLOCK"
                         onTouchTap = {this.operateNegative}
                         icon={<FontIcon className="material-icons">block</FontIcon>}
                     />
                 </div>
             </GridTile>
         )
     }
 }

     
 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(
             <GridList
                 className={cx('root')}
             >
              {list.map((tile) => <Item key={tile.get('_id')} tile={tile} operate={this.operate}/>)}
             </GridList>
         );
     }
 }   

当点击操作按钮的之后,最终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') ? <App /> :  
           <MuiThemeProvider muiTheme={NDbaseTheme}>
                <RefreshIndicator
                    className="page-indicator"
                    size={60}
                    left={0}
                    top={0}
                    status="loading"
                />
           </MuiThemeProvider>
        );
    }
}
export default AuthorApp;

至此我们已经将Immutable和redux进行了结合.
总结如下

  1. 通过redux-immutable将初始化的state和所有reducer暴露出的对象转成Immutable对象
  2. 在所有容器组件与reducer的连接函数connect中对组件的props属性进行统一Immutable的限定,保证组件内部能直接访问Immutable的props
  3. 在reducer中对action传入的数据进行Immutable话,返回一个Immutable的state

这样一来,Immutable就和redux集成到了一起

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容