Redux源碼解析

寫在開始

本篇主要結(jié)合react-native 使用redux的過程,說明使用redux的方法和原理,揭秘Redux單向數(shù)據(jù)流更新的機制原理甥捺,適合有一定Redux基礎(chǔ)的同學(xué)冠胯。

Redux 工作圖

Redux原理圖


上圖是redux的工作原理圖,具體每一步驟的實現(xiàn)請見下面的詳細(xì)分析。

Redux需要懂的七大元素

  • combineReducers()
  • createStore()
  • <Provider/>
  • connect()
  • mapStateToProps()
  • mapDispatchToProps()
  • action

一. combineReducers

將應(yīng)用整體state劃分為不同的reducer揖庄,最終合并為rootReducer ===>combineReducers()
1.1reducer

reducer 就是一個方法肮街,接收舊的 state和當(dāng)前操作 action先慷,返回新的 state饮笛。需要注意的是reducer是純函數(shù),永遠(yuǎn)不要在reducer里做這些操作:

  • 修改傳入?yún)?shù)熟掂;
  • 執(zhí)行有副作用的操作缎浇,如 API 請求和路由跳轉(zhuǎn);
  • 調(diào)用非純函數(shù)赴肚,如Date.now()Math.random()素跺。
1.2combineReducers()用法
const rootReducer = combineReducers({
    localCounter:localCounter,
    serverCounter:serverCounter
})
1.3combineReducers()作用
  • 將子reducer用key-value的形式組成rootReducer,value是該子reducer的實現(xiàn)方法誉券。
  • 返回值:一個總reducer指厌,內(nèi)部包含所有子reducer
1.4combineReducers()關(guān)鍵源碼
function combineReducers(reducers) {
  var reducerKeys = Object.keys(reducers);
  var finalReducers = {};
  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i];
    ...
    if (typeof reducers[key] === 'function') {
      //reduer數(shù)組
      finalReducers[key] = reducers[key];
    }
  }
   //reducer的key
  var finalReducerKeys = Object.keys(finalReducers);
  ...
  //返回一個function,該方法接收state和action作為參數(shù)踊跟,其實返回值就是rootReducer
  //遍歷reducers數(shù)組踩验,將action傳入每個reducer方法中得到的新狀態(tài),與舊狀態(tài)對比是否變化商玫,若變化則返回新狀態(tài)箕憾,若沒有變化則返回舊狀態(tài)
  return function combination() {
   ...
    var hasChanged = false;
    var nextState = {};
    //遍歷reducer數(shù)組,執(zhí)行每個reducer的方法
    for (var i = 0; i < finalReducerKeys.length; i++) {
      var key = finalReducerKeys[i];
      var reducer = finalReducers[key];
      var previousStateForKey = state[key];
      //傳入舊state拳昌,action袭异,得到新state
      var nextStateForKey = reducer(previousStateForKey, action);
      ...
      nextState[key] = nextStateForKey;
      //判斷狀態(tài)是否發(fā)生了改變
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    //返回值處理后的state
    return hasChanged ? nextState : state;
  };
}

二. createStore

然后根據(jù) rootReducer創(chuàng)建store===>createStore()
2.1 store

是redux的核心,存儲APP的所有狀態(tài)炬藤,只能有一個御铃。改變狀態(tài)的唯一方法是調(diào)用store.dispatch方法

2.2 createStore()作用
  • 兩種創(chuàng)建方式:
  • createStore(rootReducer,initialState);
  • createStore(rootReducer,initialState,applyMiddleware(thunkMiddleware));

A thunk is a function that wraps an expression to delay its evaluation.簡單來說一個 thunk 就是一個封裝表達(dá)式的函數(shù),封裝的目的是延遲執(zhí)行表達(dá)式

可以使用第三方庫來增強store沈矿,通常會使用redux-thunk庫來支持異步方法的dispatch上真。thunk最終起的作用,對dispatch調(diào)用的action進行檢查羹膳,如果action在第一次調(diào)用之后返回的是function睡互,則將(dispatch, getState)作為參數(shù)注入到action返回的方法中,執(zhí)行異步邏輯(相當(dāng)于開始一個新的action)陵像,若有返回對象則進行分發(fā)湃缎。

  • 返回值

{Store} A Redux store that lets you read the state, dispatch actions
and subscribe to changes.

  • (1) dispatch(action): 用于action的分發(fā),改變store里面的state
  • (2) subscribe(listener): 注冊listener蠢壹,store里面state發(fā)生改變后,執(zhí)行該listener九巡。返回unsubscrib()方法图贸,用于注銷當(dāng)前listenerRedux采用了觀察者模式,store內(nèi)部維護listener數(shù)組疏日,用于存儲所有通過store.subscribe注冊的listener
  • (3) getState(): 讀取store里面的state
  • (4) replaceReducer(): 替換reducer偿洁,改變state修改的邏輯
2.3 源碼分析

function createStore(reducer, preloadedState, enhancer) {
  ...

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.');
    }
    //返回增強store
    return enhancer(createStore)(reducer, preloadedState);
  }

  var currentState = preloadedState;
  var currentListeners = [];
  var nextListeners = currentListeners;
  var isDispatching = false;

  /**
   * 返回當(dāng)前狀態(tài)
   */
  function getState() {
    return currentState;
  }

  /**
   * 注冊`listener`,維護一個listener的數(shù)組
   * `store`里面`state`發(fā)生改變后沟优,執(zhí)行該`listener`
   * 觀察者模式實現(xiàn)的關(guān)鍵
   */
  function subscribe(listener) {
    ...
    nextListeners.push(listener);

    //返回一個注銷listener的方法
    return function unsubscribe() {
      ...
      var index = nextListeners.indexOf(listener);
      nextListeners.splice(index, 1);
    };
  }

  /**
   * Dispatches an action. It is the only way to trigger a state change.
   */
   var currentReducer = reducer;
  function dispatch(action) {
    //類型校驗...
    try {
      isDispatching = true;
      //執(zhí)行rootReducer涕滋,得到新的state
      currentState = currentReducer(currentState, action);
    } finally {
      isDispatching = false;
    }

    var listeners = currentListeners = nextListeners;
    //循環(huán)遍歷,執(zhí)行l(wèi)istener挠阁,通知數(shù)據(jù)改變了宾肺,listeners具體是什么?看容器組件
    for (var i = 0; i < listeners.length; i++) {
      listeners[i]();
    }

    return action;
  }

  /**
   * Replaces the reducer currently used by the store to calculate the state.
   */
  function replaceReducer(nextReducer) {
    currentReducer = nextReducer;
    dispatch({ type: ActionTypes.INIT });
  }

  //返回值
  return _ref2 = {
    dispatch: dispatch,
    subscribe: subscribe,
    getState: getState,
    replaceReducer: replaceReducer
  }, _ref2[_symbolObservable2['default']] = observable, _ref2;
}

三. Provider

store傳遞給應(yīng)用中的View===> <Provider/>
3.1 <Provider/>是redux提供的組件
<Provider store={store}>
  <APP />
</Provider>
3.2 作用:將store傳遞給其子組件

將store設(shè)置到子組件的context中侵俗,這樣應(yīng)用的所有子組件就默認(rèn)都拿到了store

3.3 源碼
var Provider = function (_Component) {
  _inherits(Provider, _Component);

  //用于指定子組件可直接訪問的上下文數(shù)據(jù)锨用,所以子組件可以直接訪問store了
  Provider.prototype.getChildContext = function getChildContext() {
    return { store: this.store, storeSubscription: null };
  };

  function Provider(props, context) {
    _classCallCheck(this, Provider);

    var _this = _possibleConstructorReturn(this, _Component.call(this, props, context));

    _this.store = props.store;
    return _this;
  }

  Provider.prototype.render = function render() {
    return _react.Children.only(this.props.children);
  };

  return Provider;
}(_react.Component);

exports.default = Provider;

四. connect

如何將react中的UI組件與redux的狀態(tài)、事件關(guān)聯(lián)起來====>connect()方法
4.0 UI組件和容器組件

React-Redux 將所有組件分成兩大類:UI 組件(presentational component)和容器組件(container component)隘谣。

UI組件:

①只負(fù)責(zé) UI 的呈現(xiàn)增拥,不帶有任何業(yè)務(wù)邏輯
②沒有狀態(tài)(即不使用this.state這個變量)
③所有數(shù)據(jù)都由參數(shù)(this.props)提供
④不使用任何 Redux 的 API

容器組件:

①負(fù)責(zé)管理數(shù)據(jù)和業(yè)務(wù)邏輯,不負(fù)責(zé) UI 的呈現(xiàn)
②帶有內(nèi)部狀態(tài)
③使用 Redux 的 API

4.1connect()生成容器組件
  • 通過<Provider/>傳遞store是給容器redux的容器組件
    用于從 UI 組件生成容器組件寻歧。connect的意思掌栅,就是將這兩種組件連起來。
    connect方法接受兩個參數(shù):mapStateToProps和mapDispatchToProps码泛。它們定義了 UI 組件的業(yè)務(wù)邏輯
let ConnectCounter = connect(
    mapStateToProps,
    mapDispatchToProps
)(Counter)
4.2 作用

connect是一個高階函數(shù)猾封,首先傳入mapStateToPropsmapDispatchToProps弟晚,然后返回一個生產(chǎn)Component的函數(shù)wrapWithConnect()忘衍,然后再將真正的Component作為參數(shù)傳入wrapWithConnect(MyComponent),這樣就生產(chǎn)出一個經(jīng)過包裹的Connect組件(也就是容器組件)卿城。

容器組件具有如下特點:

  • (1)通過this.context獲取祖先Component的store枚钓,也就是通過<Provider/>傳遞過來的store。
  • (2)props包括stateProps瑟押、dispatchProps搀捷、parentProps,合并在一起得到nextState,作為props傳給真正的Component多望,這樣在真正組件中就能通過this.props獲取到各種數(shù)據(jù)和方法嫩舟。
  • (3)componentDidMount調(diào)用store.subscribe(listener)注冊監(jiān)聽方法,對store的變化進行訂閱怀偷,當(dāng)store變化的時候家厌,更新渲染view。
  • (4)componentWillUnmount時注銷訂閱
4.3源碼分析

注意訂閱的實現(xiàn)

var Connect = function (_Component) {
            _inherits(Connect, _Component);

            /*
            * 構(gòu)造函數(shù)中椎工,構(gòu)造一個訂閱對象饭于,屬性有this.store,方法this.onStateChange.bind(this)
            */
            function Connect(props, context) {
                ...
                //獲取store蜀踏。
                //從父組件或context中獲取store。這里使用的是從context中獲取
                //storeKey = _ref$storeKey === undefined ? 'store' : _ref$storeKey,
                _this.store = props[storeKey] || context[storeKey];

                ...

                //初始化訂閱邏輯
                _this.initSubscription();
                return _this;
            }

            //初始化訂閱方法
            Connect.prototype.initSubscription = function initSubscription() {
                if (!shouldHandleStateChanges) return;

                var parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey];

                //wym: 調(diào)用的是Subscription.js中方法,向store內(nèi)部注冊一個listener---this.onStateChange.bind(this)
                this.subscription = new _Subscription2.default(this.store, parentSub, this.onStateChange.bind(this));

            
                this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription);
            };

            //當(dāng)數(shù)據(jù)狀態(tài)發(fā)生改變時
            Connect.prototype.onStateChange = function onStateChange() {
                this.selector.run(this.props);

                if (!this.selector.shouldComponentUpdate) {
                    this.notifyNestedSubs();
                } else {
                    this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate;
                    //設(shè)置state掰吕,view會自動重新渲染
                    this.setState(dummyState);
                }
            };

            Connect.prototype.getChildContext = function getChildContext() {
                ...
            };

            /*
            * 組件狀態(tài)完成時果覆,向store注冊監(jiān)聽方法
            */
            Connect.prototype.componentDidMount = function componentDidMount() {
                if (!shouldHandleStateChanges) return;

                //實際調(diào)用this.store.subscribe(this.onStateChange);
                //向store注冊監(jiān)聽方法
                this.subscription.trySubscribe();

                this.selector.run(this.props);
                if (this.selector.shouldComponentUpdate) this.forceUpdate();
            };



            Connect.prototype.componentWillUnmount = function componentWillUnmount() {
                //注銷訂閱
                if (this.subscription) this.subscription.tryUnsubscribe();
                this.subscription = null;
                ...
            };


            Connect.prototype.render = function render() {
                var selector = this.selector;
                selector.shouldComponentUpdate = false;

                if (selector.error) {
                    throw selector.error;
                } else {
                    return (0, _react.createElement)(WrappedComponent, this.addExtraProps(selector.props));
                }
            };

            return Connect;
        }(_react.Component);

五.mapStateToProps()

建立一個從(外部的)state對象到(UI 組件的)props對象的映射關(guān)系。mapStateToProps會訂閱 Store殖熟,每當(dāng)state更新的時候局待,就會自動執(zhí)行,重新計算 UI 組件的參數(shù)菱属,從而觸發(fā) UI 組件的重新渲染

六. mapDispatchToProps

  • 用來建立 UI 組件的方法到store.dispatch方法的映射钳榨,它定義了哪些用戶的操作應(yīng)該當(dāng)作 Action,傳給 Store照皆。
  • 它可以是一個函數(shù)重绷,也可以是一個對象。(只是不同的寫法膜毁,作用一樣)

七. Action

Action 是把數(shù)據(jù)從應(yīng)用傳到 store 的有效載荷昭卓。它是 store 數(shù)據(jù)的唯一來源。一般來說你會通過 store.dispatch() 將 action 傳到 store瘟滨。
分為:

  • 同步 action 候醒,返回的是一個對象,要求是純凈的函數(shù)杂瘸。

純凈:沒有特殊情況倒淫、沒有副作用,沒有 API 請求败玉、沒有變量修改敌土,單純執(zhí)行計算。

  • 異步action运翼,返回的是一個方法返干,這個函數(shù)會被Redux Thunk middleware執(zhí)行。這個函數(shù)并不需要保持純凈血淌;它還可以帶有副作用矩欠,包括執(zhí)行異步 API 請求。

Redux中的觀察者模式

redux之所以能夠當(dāng)state變化后悠夯,更新綁定的視圖癌淮,是因為內(nèi)部實現(xiàn)的觀察者模式

觀察者模式的實現(xiàn)

1. store提供了注冊監(jiān)聽的方法===>subscribe(listener)
  • store內(nèi)部維護listener數(shù)組,用于存儲所有通過store.subscrib注冊的listener沦补,store里面state發(fā)生改變(即為調(diào)用store.dispatch())后乳蓄,依次執(zhí)行數(shù)組中的listener
  • store.subscrib返回unsubscrib方法夕膀,用于注銷當(dāng)前l(fā)istener虚倒。

調(diào)用store.dispatch()的時候做了兩件事:
(1)更新當(dāng)前state: currentState = currentReducer(currentState, action);
(2)依次執(zhí)行數(shù)組中的listener匣摘。

2. Connect組件中,向store中注冊監(jiān)聽方法
  • ①構(gòu)造方法中:初始化訂閱邏輯裹刮,將listener:this.onStateChange.bind(this)傳遞給Subscription.js。
  • ②componentDidMount方法中庞瘸,調(diào)用store.subscribe(listener)注冊監(jiān)聽方法:onStateChange()捧弃。

onStateChange方法中,當(dāng)判斷需要更新數(shù)據(jù)時擦囊,調(diào)用的是this.setState(state);
===》根據(jù)react機制违霞,則會重新渲染view

  • ③在componentWillUnmount方法中,注銷訂閱

Redux原理圖

根據(jù)以上的源碼分析瞬场,redux的工作流可用下圖進行概括买鸽。


redux創(chuàng)建過程概括

  • 將一個APP的狀態(tài)分解成不同的reducer,最后創(chuàng)建store(有整個數(shù)據(jù)state贯被,有分發(fā)action的方法眼五,有注冊listener的方法)
  • 將store通過<Provider/>組件傳遞給容器組件
  • 容器組件通過UI組件,mapStateToProps, mapDispatchToProps通過connect()轉(zhuǎn)化而來
  • 將UI交互事件彤灶,外部輸入事件寫成action看幼,用來觸發(fā)reducer

redux更新數(shù)據(jù)過程概括

  • 在UI組件上有交互操作,或外部輸入(網(wǎng)絡(luò)請求)時幌陕,====>寫成Action
  • store.dispatch(action)诵姜,結(jié)果:
    (1)合成新的state,新的state通過mapStateToProps()傳遞給UI組件搏熄,
    (2)執(zhí)行通過store.subscribe()注冊的listener
  • listener具體邏輯:調(diào)用setState()設(shè)置信息數(shù)值棚唆,觸發(fā)View的自動render
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市心例,隨后出現(xiàn)的幾起案子宵凌,更是在濱河造成了極大的恐慌,老刑警劉巖契邀,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摆寄,死亡現(xiàn)場離奇詭異,居然都是意外死亡坯门,警方通過查閱死者的電腦和手機微饥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來古戴,“玉大人欠橘,你說我怎么就攤上這事∠帜眨” “怎么了肃续?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵黍檩,是天一觀的道長。 經(jīng)常有香客問我始锚,道長刽酱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任瞧捌,我火速辦了婚禮棵里,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘姐呐。我一直安慰自己殿怜,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布曙砂。 她就那樣靜靜地躺著头谜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪鸠澈。 梳的紋絲不亂的頭發(fā)上柱告,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音款侵,去河邊找鬼末荐。 笑死,一個胖子當(dāng)著我的面吹牛新锈,可吹牛的內(nèi)容都是我干的甲脏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼妹笆,長吁一口氣:“原來是場噩夢啊……” “哼块请!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拳缠,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤墩新,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后窟坐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體海渊,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年哲鸳,在試婚紗的時候發(fā)現(xiàn)自己被綠了臣疑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡徙菠,死狀恐怖讯沈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情婿奔,我是刑警寧澤缺狠,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布问慎,位于F島的核電站,受9級特大地震影響挤茄,放射性物質(zhì)發(fā)生泄漏如叼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一穷劈、第九天 我趴在偏房一處隱蔽的房頂上張望薇正。 院中可真熱鬧,春花似錦囚衔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至审轮,卻和暖如春肥哎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背疾渣。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工篡诽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人榴捡。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓杈女,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吊圾。 傳聞我的和親對象是個殘疾皇子达椰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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

  • 一、什么情況需要redux项乒? 1啰劲、用戶的使用方式復(fù)雜 2、不同身份的用戶有不同的使用方式(比如普通用戶和管...
    初晨的筆記閱讀 2,030評論 0 11
  • 做React需要會什么? react的功能其實很單一频鉴,主要負(fù)責(zé)渲染的功能栓辜,現(xiàn)有的框架,比如angular是一個大而...
    蒼都閱讀 14,759評論 1 139
  • 前言 本文 有配套視頻砚殿,可以酌情觀看啃憎。 文中內(nèi)容因各人理解不同,可能會有所偏差似炎,歡迎朋友們聯(lián)系我討論辛萍。 文中所有內(nèi)...
    珍此良辰閱讀 11,906評論 23 111
  • 上周六參加了一個公司的面試悯姊,因為是很長時間以來的第一次面試,發(fā)揮的并不是很好贩毕,有兩個下來一想就明白的問題在當(dāng)時卻卡...
    夏爾先生閱讀 6,417評論 0 15
  • http://gaearon.github.io/redux/index.html 悯许,文檔在 http://rac...
    jacobbubu閱讀 79,964評論 35 198