美文网首页
redux 和 Immutable实践

redux 和 Immutable实践

作者: 公里柒 | 来源:发表于2017-08-08 17:31 被阅读0次

    本文基于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集成到了一起

    相关文章

      网友评论

          本文标题:redux 和 Immutable实践

          本文链接:https://www.haomeiwen.com/subject/nhfxrxtx.html