淺析Redux源碼

@(Redux)[|用法|源碼]

Redux 由Dan Abramov在2015年創(chuàng)建的科技術(shù)語。是受2014年Facebook的Flux架構(gòu)以及函數(shù)式編程語言Elm啟發(fā)。很快,Redux因其簡單易學(xué)體積小短時(shí)間內(nèi)成為最熱門的前端架構(gòu)。

@[三大原則]

  • 單一數(shù)據(jù)源 - 整個(gè)應(yīng)用的state被儲(chǔ)存在一棵object tree中,并且這個(gè)object tree只存在于唯一一個(gè)store中。所有數(shù)據(jù)會(huì)通過store.getState()方法調(diào)用獲取.
  • **State‘只讀’ ** - 根據(jù)State只讀原則,數(shù)據(jù)變更會(huì)通過store,dispatch(action)方法.
  • 使用純函數(shù)修改 -Reducer只是一些純函數(shù)[1],它接收先前的stateaction,并返回新的state.

[TOC]

準(zhǔn)備階段

柯里化函數(shù)(curry)

    //curry example
    const A  = (a) => {
        return (b) => {
            return a + b
        }
    }

通俗的來講,可以用一句話概括柯里化函數(shù):返回函數(shù)的函數(shù).
優(yōu)點(diǎn): 避免了給一個(gè)函數(shù)傳入大量的參數(shù),將參數(shù)的代入分離開,更有利于調(diào)試。降低耦合度和代碼冗余,便于復(fù)用.

代碼組合(compose)

舉個(gè)例子

    let init = (...args) => args.reduce((ele1, ele2) => ele1 + ele2, 0)
    let step2 = (val) => val + 2
    let step3 = (val) => val + 3
    let step4 = (val) => val + 4
    let steps = [step4, step3, step2, init]
    let composeFunc = compose(...steps)
    console.log(composeFunc(1, 2, 3))
    // 1+2+3+2+3+4 = 15

接下來看下FP思想的compose的源碼

    const compose = function (...args) {
      let length = args.length
      let count = length - 1
      let result
      let this_ = this
      // 遞歸
      return function f1(...arg1) {
        result = args[count].apply(this, arg1)
        if (count <= 0) {
          count = length - 1
          return result
        }
        count--
        return f1.call(null, result)
      }
    }

通俗的講: 從右到左執(zhí)行函數(shù),最右函數(shù)以arguments為參數(shù),其余函數(shù)以上個(gè)函數(shù)結(jié)果為入?yún)?shù)執(zhí)行。

優(yōu)點(diǎn): 通過這樣函數(shù)之間的組合,可以大大增加可讀性,效果遠(yuǎn)大于嵌套一大堆的函數(shù)調(diào)用,并且我們可以隨意更改函數(shù)的調(diào)用順序

CombineReducers

作用

隨著整個(gè)項(xiàng)目越來越大,state狀態(tài)樹也會(huì)越來越龐大,state的層級也會(huì)越來越深,由于redux只維護(hù)唯一的state,當(dāng)某個(gè)action.type所對應(yīng)的需要修改state.a.b.c.d.e.f時(shí),我的函數(shù)寫起來就非常復(fù)雜,我必須在這個(gè)函數(shù)的頭部驗(yàn)證state 對象有沒有那個(gè)屬性。這是讓開發(fā)者非常頭疼的一件事。于是有了CombineReducers。我們除去源碼校驗(yàn)函數(shù)部分,從最終返回的大的Reducers來看。

Note:

  • FinalReducers : 通過=== 'function'校驗(yàn)后的Reducers.
  • FinalReducerKeys : FinalReducers的所有key
    (與入?yún)?code>Object的key區(qū)別:過濾了value不為function的值)

源碼

      // 返回一個(gè)function。該方法接收state和action作為參數(shù)
      return function combination(state = {}, action) {
        var hasChanged = false
        var nextState = {}
        // 遍歷所有的key和reducer,分別將reducer對應(yīng)的key所代表的state,代入到reducer中進(jìn)行函數(shù)調(diào)用
        for (var i = 0; i < finalReducerKeys.length; i++) {
          var key = finalReducerKeys[i]
          var reducer = finalReducers[key]
          // CombineReducers入?yún)bject中的Value為reducer function,從這可以看出reducer function的name就是返回給store中的state的key。
          var previousStateForKey = state[key]
          // debugger
          var nextStateForKey = reducer(previousStateForKey, action)
          // 如果reducer返回undefined則拋出錯(cuò)誤
          if (typeof nextStateForKey === 'undefined') {
            var errorMessage = getUndefinedStateErrorMessage(key, action)
            throw new Error(errorMessage)
          }
          // 將reducer返回的值填入nextState
          nextState[key] = nextStateForKey
          // 如果任一state有更新則hasChanged為true
          hasChanged = hasChanged || nextStateForKey !== previousStateForKey
        }
        return hasChanged ? nextState : state
      }

小結(jié)

combineReducers實(shí)現(xiàn)方法很簡單,它遍歷傳入的reducers,返回一個(gè)新的reducer.該函數(shù)根據(jù)Statekey 去執(zhí)行相應(yīng)的子Reducer,并將返回結(jié)果合并成一個(gè)大的State 對象。

CreateStore

作用

createStore主要用于Store的生成,我們先整理看下createStore具體做了哪些事兒。(這里我們看簡化版代碼)

源碼(簡化版)

const createStore = (reducer, initialState) => {
      // initialState一般設(shè)置為null,或者由服務(wù)端給默認(rèn)值。
      // internal variables
      const store = {};
      store.state = initialState;
      store.listeners = [];
      // api-subscribe
      store.subscribe = (listener) => {
        store.listeners.push(listener);
      };
      // api-dispatch
      store.dispatch = (action) => {
        store.state = reducer(store.state, action);
        store.listeners.forEach(listener => listener());
      };
      // api-getState
      store.getState = () => store.state;
      
      return store;
    }

小結(jié)

源碼角度,一大堆類型判斷先忽略,可以看到聲明了一系列函數(shù),然后執(zhí)行了dispatch方法,最后暴露了dispatch、subscribe……幾個(gè)方法。這里dispatch了一個(gè)init Action是為了生成初始的State樹。

ThunkMiddleware

作用

首先,說ThunkMiddleware之前,也許有人會(huì)問,到底middleware有什么用?
這就要從action說起。在redux里,action僅僅是攜帶了數(shù)據(jù)的普通js對象。action creator返回的值是這個(gè)action類型的對象。然后通過store.dispatch()進(jìn)行分發(fā)……

action ---> dispatcher ---> reducers
同步的情況下一切都很完美……
如果遇到異步情況,比如點(diǎn)擊一個(gè)按鈕,希望1秒之后顯示。我們可能這么寫:

function (dispatch) {
        setTimeout(function () {
            dispatch({
                type: 'show'
            })
        }, 1000)
    }

這會(huì)報(bào)錯(cuò),返回的不是一個(gè)action,而是一個(gè)function。這個(gè)返回值無法被reducer識別。

大家可能會(huì)想到,這時(shí)候需要在actionreducer之間架起一座橋梁……
當(dāng)然這座橋梁就是middleware。接下來我們先看看最簡單,最精髓的ThunkMiddleware的源碼

源碼

const thunkMiddleware = ({ dispatch, getState }) => {
      return next => action => {
        typeof action === 'function' ?
          action(dispatch, getState) :
          next(action)
      }
    }

非常之精髓。。。我們先記住上述代碼,引出下面的ApplyMiddleware

ApplyMiddleware

作用

介紹applyMiddleware之前我們先看下項(xiàng)目中store的使用方法如下:

  let step = [ReduxThunk, middleware, ReduxLogger]
  let store = applyMiddleware(...step)(createStore)(reducer)
  return store

通過使用方法可以看到有3處柯里化函數(shù)的調(diào)用,applyMiddleware 函數(shù)Redux 最精髓的地方,成功的讓Redux 有了極大的可拓展空間,在action 傳遞的過程中帶來無數(shù)的“副作用”,雖然這往往也是麻煩所在。 這個(gè)middleware的洋蔥模型思想是從koa的中間件拿過來的,用圖來表示最直觀。

洋蔥模型

洋蔥模型.png

我們來看源碼:

源碼

    const applyMiddleware = (...middlewares) => {
      return (createStore) => (reducer, initialState, enhancer) => {
        var store = createStore(reducer, initialState, enhancer)
        var dispatch
        var chain = []
        var middlewareAPI = {
          getState: store.getState,
          dispatch: (action) => dispatch(action)
        }
        // 每個(gè) middleware 都以 middlewareAPI 作為參數(shù)進(jìn)行注入,返回一個(gè)新的鏈。
        // 此時(shí)的返回值相當(dāng)于調(diào)用 thunkMiddleware 返回的函數(shù): (next) => (action) => {} ,接收一個(gè)next作為其參數(shù)
        chain = middlewares.map(middleware => middleware(middlewareAPI))
        // 并將鏈代入進(jìn) compose 組成一個(gè)函數(shù)的調(diào)用鏈
        dispatch = compose(...chain)(store.dispatch)
        return {
          ...store,
          dispatch
        }
      }
    }

applyMiddleware函數(shù)第一次調(diào)用的時(shí)候,返回一個(gè)以createStore為參數(shù)的匿名函數(shù),這個(gè)函數(shù)返回另一個(gè)以reducer,initialState,enhancer為參數(shù)的匿名函數(shù).我們在使用方法中,分別可以看到傳入的值。
結(jié)合一個(gè)簡單的實(shí)例來理解中間件以及洋蔥模型

    // 傳入middlewareA
    const middlewareA = ({ dispatch, getState }) => {
      return next => action => {
        console.warn('A middleware start')
        next(action)
        console.warn('A middleware end')
      }
    }
    // 傳入多個(gè)middlewareB
    const middlewareB = ({ dispatch, getState }) => {
      return next => action => {
        console.warn('B middleware start')
        next(action)
        console.warn('B middleware end')
      }
    }
    // 傳入多個(gè)middlewareC
    const middlewareC = ({ dispatch, getState }) => {
      return next => action => {
        console.warn('C middleware start')
        next(action)
        console.warn('C middleware end')
      }
    }

當(dāng)我們傳入多個(gè)類似A,B,C的middlewareapplyMiddleware后,調(diào)用

dispatch = compose(...chain)(store.dispatch)

結(jié)合場景并且執(zhí)行compose結(jié)果為:

dispatch = middlewareA(middlewareB(middlewareC(store.dispatch)))

從中我們可以清晰的看到middleware函數(shù)中的next函數(shù)相互連接,這里體現(xiàn)了compose FP編程思想中代碼組合的強(qiáng)大作用。再結(jié)合洋蔥模型的圖片,不難理解是怎么樣的一個(gè)工作流程。

最后我們看結(jié)果,當(dāng)我們觸發(fā)一個(gè)store.dispath的時(shí)候進(jìn)行分發(fā)。則會(huì)先進(jìn)入middlewareA并且打印A start然后進(jìn)入next函數(shù),也就是middlewareB同時(shí)打印B start,然后觸發(fā)next函數(shù),這里的next函數(shù)就是middlewareC,然后打印C start,之后才處理dispath,處理完成后先打印C end,然后B end,最后A end。完成整體流程。

小結(jié)

  • Redux applyMiddleware機(jī)制的核心在于,函數(shù)式編程(FP)compose組合函數(shù),需將所有的中間件串聯(lián)起來。
  • 為了配合compose對單參函數(shù)的使用,對每個(gè)中間件采用currying的設(shè)計(jì)。同時(shí),利用閉包原理做到每個(gè)中間件共享Store。(middlewareAPI的注入)

Feedback & Bug Report


Thank you for reading this record.


  1. 純函數(shù),它不依賴于外部環(huán)境(例如:全局變量、環(huán)境變量)、不改變外部環(huán)境(例如:發(fā)送請求、改變DOM結(jié)構(gòu)),函數(shù)的輸出完全由函數(shù)的輸入決定。 比如 slice 和 splice,這兩個(gè)函數(shù)的作用并無二致——但是注意,它們各自的方式卻大不同,但不管怎么說作用還是一樣的。我們說 slice 符合純函數(shù)的定義是因?yàn)閷ο嗤妮斎胨WC能返回相同的輸出。而 splice 卻會(huì)嚼爛調(diào)用它的那個(gè)數(shù)組,然后再吐出來;這就會(huì)產(chǎn)生可觀察到的副作用,即這個(gè)數(shù)組永久地改變了??梢钥吹剑瑂plice改變了原始數(shù)組,而slice沒有。我們認(rèn)為,slice不改變原來數(shù)組的方式更加“安全”。改變原始組數(shù),是一種“副作用”。. ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • http://gaearon.github.io/redux/index.html ,文檔在 http://rac...
    jacobbubu閱讀 80,415評論 35 198
  • 學(xué)習(xí)必備要點(diǎn): 首先弄明白,Redux在使用React開發(fā)應(yīng)用時(shí),起到什么作用——狀態(tài)集中管理 弄清楚Redux是...
    賀賀v5閱讀 9,069評論 10 58
  • 前言 本文 有配套視頻,可以酌情觀看。 文中內(nèi)容因各人理解不同,可能會(huì)有所偏差,歡迎朋友們聯(lián)系我討論。 文中所有內(nèi)...
    珍此良辰閱讀 12,189評論 23 111
  • 為什么dispatch需要middleware 上圖表達(dá)的是 redux 中一個(gè)簡單的同步數(shù)據(jù)流動(dòng)的場景,點(diǎn)擊 b...
    一個(gè)胖子的我閱讀 2,124評論 1 9
  • 遇見小清新--華欣 五月初,來曼谷已經(jīng)兩個(gè)月,還沒出遠(yuǎn)門旅行,沒錢是硬傷,工作的事情也沒有搞定,說多了都是淚,只有...
    Echo_一可閱讀 660評論 4 4

友情鏈接更多精彩內(nèi)容