redux 和 Immutable實(shí)踐

本文基于React^15.6.1 Redux^3.7.1 Immutable^4.0.0-rc.2

Immutable.js

Immutable Data

Immutable 實(shí)現(xiàn)的原理是 Persistent Data Structure(持久化數(shù)據(jù)結(jié)構(gòu))佩番,也就是使用舊數(shù)據(jù)創(chuàng)建新數(shù)據(jù)時(shí)众旗,要保證舊數(shù)據(jù)同時(shí)可用且不變。同時(shí)為了避免 deepCopy 把所有節(jié)點(diǎn)都復(fù)制一遍帶來(lái)的性能損耗答捕,Immutable 使用了 Structural Sharing(結(jié)構(gòu)共享)逝钥,即如果對(duì)象樹中一個(gè)節(jié)點(diǎn)發(fā)生變化,只修改這個(gè)節(jié)點(diǎn)和受它影響的父節(jié)點(diǎn)拱镐,其它節(jié)點(diǎn)則進(jìn)行共享艘款。

React 性能問(wèn)題

React的生命周期函數(shù)shuoldComponentUpdate默認(rèn)是返回true, 這樣的話每次state或者props改變的時(shí)候都會(huì)進(jìn)行render,在render 的過(guò)程當(dāng)就是最消耗性能的過(guò)程.所以在生命周期函數(shù) shuoldComponentUpdate 中實(shí)現(xiàn)性能優(yōu)化.

  1. 可以使用PrueComponent,PrueComponent是繼承至Component

    PrueComponent默認(rèn)進(jìn)行了一次shallowCompare淺比較,所謂淺比較就是只比較第一級(jí)的屬性是否相等

    相關(guān)源碼

    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.
     
     //這里只是簡(jiǎn)單的將ReactComponent的原型復(fù)制到了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);
             }
           }
         }
     
         ***
         ...
     },
    

    再來(lái)看看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只對(duì)object的第一級(jí)屬性進(jìn)行比較
    所以在基本數(shù)據(jù)類型之下我們可以直接繼承PureComponent就能提升性能,比如一個(gè)最為普通的場(chǎng)景

       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>
             );
           }
     }
    
    

    此處在點(diǎn)擊按鈕的時(shí)候直接調(diào)用toggleMenu執(zhí)行其中的setState方法,'open'本來(lái)就是基本數(shù)據(jù)類型,即使不重寫shouldComponentUpdate,PureComponent的shouldComponentUpdate也完全能夠?qū)ζ溥M(jìn)行處理.

    進(jìn)一步我們可以看看state中處理引用數(shù)據(jù)類型的情況
    最熟悉不過(guò)可能就是列表渲染,并更改列表狀態(tài)

    首先我們直接繼承PureComponent并不對(duì)數(shù)據(jù)進(jìn)行Immutable的轉(zhuǎn)化

        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>
             );
         }
     }
    
    

    初始化渲染并沒(méi)有什么問(wèn)題,直接執(zhí)行了10次Itemrender


    當(dāng)點(diǎn)擊操作按鈕的時(shí)候問(wèn)題來(lái)了,么有任何反應(yīng),經(jīng)過(guò)檢測(cè)原來(lái)是繼承了PureComponent,在shouldComponentUpdate返回了false,在看看shallowEqual源碼,確實(shí)返回了false

    這樣的話我們先直接繼承Component,理論上任何一次setState都會(huì)render 10次了

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

    這次果然render了10次

  1. 使用Immutable進(jìn)行優(yōu)化

    此處需要注意, Immutable.js本身的入侵性還是比較強(qiáng),我們?cè)诟脑爝^(guò)程中需要注意與現(xiàn)有代碼的結(jié)合

    這里我們可以遵循幾個(gè)規(guī)則

    1. 在通過(guò)props傳遞的數(shù)據(jù),必須是Immutable
    2. 在組件內(nèi)部的state可以酌情考慮是否需要Immutable
      1. 基本數(shù)據(jù)類型(Bool,Number,String等)可以不進(jìn)行Immutable處理
      2. 引用數(shù)據(jù)類型(Array,Object)建議直接Immutable.fromJS轉(zhuǎn)成Immutable的對(duì)象
    3. ajax返回的數(shù)據(jù)我們可以根據(jù)第2點(diǎn)直接進(jìn)行轉(zhuǎn)化

所以對(duì)代碼進(jìn)行如下改造

 @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;
             //重點(diǎn)當(dāng)ajax數(shù)據(jù)返回之后引用數(shù)據(jù)類型直接轉(zhuǎn)化成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>
         );
     }
 }   

當(dāng)點(diǎn)擊操作按鈕的之后,最終Item的render調(diào)用如下


這里我們使用了一個(gè)裝飾器@pure
具體邏輯可以根據(jù)項(xiàng)目數(shù)據(jù)結(jié)構(gòu)進(jìn)行實(shí)現(xiàn),如下代碼還有可以改進(jìn)空間


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;
    }
}

結(jié)合 redux

現(xiàn)在我們將剛才的組件代碼融入進(jìn)redux的體系當(dāng)中

首先我們使用了redux-immutable,將初始化state進(jìn)行了Immutable的轉(zhuǎn)化

然后我們從組件觸發(fā)理一下思路,結(jié)合上文中Immutable對(duì)象

通過(guò)`redux`的`connect`關(guān)聯(lián)得到的數(shù)據(jù),最終是通過(guò)組件的`props`向下傳導(dǎo)的,所以`connect`所關(guān)聯(lián)的數(shù)據(jù)必須是`Immutable`的,這樣一來(lái)事情就好辦了,我們可以在`reducer`里面進(jìn)行統(tǒng)一處理,所有通過(guò)`redux`處理過(guò)的`state`必須是`Immutable`的,這就能保證所有的組件通過(guò)`connect`得到的屬性必然也是`Immutable`的

實(shí)現(xiàn)如下

//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,詳細(xì)文檔可以查閱[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`引入的,可以查閱其詳細(xì)文檔,這里的主要作用是將導(dǎo)出的reducers轉(zhuǎn)化成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`的對(duì)象,等待傳入組件的`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;

至此我們已經(jīng)將Immutable和redux進(jìn)行了結(jié)合.
總結(jié)如下

  1. 通過(guò)redux-immutable將初始化的state和所有reducer暴露出的對(duì)象轉(zhuǎn)成Immutable對(duì)象
  2. 在所有容器組件與reducer的連接函數(shù)connect中對(duì)組件的props屬性進(jìn)行統(tǒng)一Immutable的限定,保證組件內(nèi)部能直接訪問(wèn)Immutable的props
  3. 在reducer中對(duì)action傳入的數(shù)據(jù)進(jìn)行Immutable話,返回一個(gè)Immutable的state

這樣一來(lái),Immutable就和redux集成到了一起

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市沃琅,隨后出現(xiàn)的幾起案子慢味,更是在濱河造成了極大的恐慌,老刑警劉巖洁段,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刺下,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡郭脂,警方通過(guò)查閱死者的電腦和手機(jī)年碘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)展鸡,“玉大人屿衅,你說(shuō)我怎么就攤上這事∮ū祝” “怎么了涤久?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵涡尘,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我响迂,道長(zhǎng)考抄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任蔗彤,我火速辦了婚禮川梅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘幕与。我一直安慰自己挑势,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布啦鸣。 她就那樣靜靜地躺著潮饱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪诫给。 梳的紋絲不亂的頭發(fā)上香拉,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音中狂,去河邊找鬼凫碌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛胃榕,可吹牛的內(nèi)容都是我干的盛险。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼勋又,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼苦掘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起楔壤,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鹤啡,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蹲嚣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體递瑰,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年隙畜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抖部。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡议惰,死狀恐怖慎颗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤哗总,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站倍试,受9級(jí)特大地震影響讯屈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜县习,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一涮母、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧躁愿,春花似錦叛本、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至逸雹,卻和暖如春营搅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背梆砸。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工转质, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人帖世。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓休蟹,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親日矫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赂弓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容