淺析Redux源碼

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

Redux 由Dan Abramov在2015年建立的科技術語。是受2014年Facebook的Flux架構以及函數式編程語言Elm啓發。很快,Redux因其簡單易學體積小短期內成爲最熱門的前端架構。前端

@[三大原則]java

  • 單一數據源 - 整個應用的state被儲存在一棵object tree中,而且這個object tree只存在於惟一一個store中。全部數據會經過store.getState()方法調用獲取.
  • State‘只讀’ - 根據State只讀原則,數據變動會經過store,dispatch(action)方法.
  • 使用純函數修改 -Reducer只是一些純函數1,它接收先前的stateaction,並返回新的state.

[TOC]git

準備階段

柯里化函數(curry)

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

通俗的來說,能夠用一句話歸納柯里化函數:返回函數的函數.
優勢: 避免了給一個函數傳入大量的參數,將參數的代入分離開,更有利於調試。下降耦合度和代碼冗餘,便於複用.github

代碼組合(compose)

舉個例子編程

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的源碼redux

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

通俗的講: 從右到左執行函數,最右函數以arguments爲參數,其他函數以上個函數結果爲入參數執行。api

優勢: 經過這樣函數之間的組合,能夠大大增長可讀性,效果遠大於嵌套一大堆的函數調用,而且咱們能夠隨意更改函數的調用順序數組

CombineReducers

做用

隨着整個項目愈來愈大,state狀態樹也會愈來愈龐大,state的層級也會愈來愈深,因爲redux只維護惟一的state,當某個action.type所對應的須要修改state.a.b.c.d.e.f時,個人函數寫起來就很是複雜,我必須在這個函數的頭部驗證state 對象有沒有那個屬性。這是讓開發者很是頭疼的一件事。因而有了CombineReducers。咱們除去源碼校驗函數部分,從最終返回的大的Reducers來看。安全

Note:

  • FinalReducers : 經過=== 'function'校驗後的Reducers.
  • FinalReducerKeys : FinalReducers的全部key

(與入參Objectkey區別:過濾了value不爲function的值)

源碼

// 返回一個function。該方法接收state和action做爲參數
      return function combination(state = {}, action) {
        var hasChanged = false
        var nextState = {}
        // 遍歷全部的key和reducer,分別將reducer對應的key所表明的state,代入到reducer中進行函數調用
        for (var i = 0; i < finalReducerKeys.length; i++) {
          var key = finalReducerKeys[i]
          var reducer = finalReducers[key]
          // CombineReducers入參Object中的Value爲reducer function,從這能夠看出reducer function的name就是返回給store中的state的key。
          var previousStateForKey = state[key]
          // debugger
          var nextStateForKey = reducer(previousStateForKey, action)
          // 若是reducer返回undefined則拋出錯誤
          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
      }

小結

combineReducers實現方法很簡單,它遍歷傳入的reducers,返回一個新的reducer.該函數根據Statekey 去執行相應的子Reducer,並將返回結果合併成一個大的State 對象。

CreateStore

做用

createStore主要用於Store的生成,咱們先整理看下createStore具體作了哪些事兒。(這裏咱們看簡化版代碼)

源碼(簡化版)

const createStore = (reducer, initialState) => {
      // initialState通常設置爲null,或者由服務端給默認值。
      // 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;
    }

小結

源碼角度,一大堆類型判斷先忽略,能夠看到聲明瞭一系列函數,而後執行了dispatch方法,最後暴露了dispatchsubscribe……幾個方法。這裏dispatch了一個init Action是爲了生成初始的State樹。

ThunkMiddleware

做用

首先,說ThunkMiddleware以前,也許有人會問,到底middleware有什麼用?
這就要從action提及。在redux裏,action僅僅是攜帶了數據的普通js對象。action creator返回的值是這個action類型的對象。而後經過store.dispatch()進行分發……

action ---> dispatcher ---> reducers
同步的狀況下一切都很完美……
若是遇到異步狀況,好比點擊一個按鈕,但願1秒以後顯示。咱們可能這麼寫:

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

這會報錯,返回的不是一個action,而是一個function。這個返回值沒法被reducer識別。

你們可能會想到,這時候須要在actionreducer之間架起一座橋樑……
固然這座橋樑就是middleware。接下來咱們先看看最簡單,最精髓的ThunkMiddleware的源碼

源碼

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

很是之精髓。。。咱們先記住上述代碼,引出下面的ApplyMiddleware

ApplyMiddleware

做用

介紹applyMiddleware以前咱們先看下項目中store的使用方法以下:

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

經過使用方法能夠看到有3處柯里化函數的調用,applyMiddleware 函數Redux 最精髓的地方,成功的讓Redux 有了極大的可拓展空間,在action 傳遞的過程當中帶來無數的「反作用」,雖然這每每也是麻煩所在。 這個middleware的洋蔥模型思想是從koa的中間件拿過來的,用圖來表示最直觀。

洋蔥模型

Image text
咱們來看源碼:

源碼

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)
        }
        // 每一個 middleware 都以 middlewareAPI 做爲參數進行注入,返回一個新的鏈。
        // 此時的返回值至關於調用 thunkMiddleware 返回的函數: (next) => (action) => {} ,接收一個next做爲其參數
        chain = middlewares.map(middleware => middleware(middlewareAPI))
        // 並將鏈代入進 compose 組成一個函數的調用鏈
        dispatch = compose(...chain)(store.dispatch)
        return {
          ...store,
          dispatch
        }
      }
    }

applyMiddleware函數第一次調用的時候,返回一個以createStore爲參數的匿名函數,這個函數返回另外一個以reducer,initialState,enhancer爲參數的匿名函數.咱們在使用方法中,分別能夠看到傳入的值。
結合一個簡單的實例來理解中間件以及洋蔥模型

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

當咱們傳入多個相似A,B,C的middlewareapplyMiddleware後,調用

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

結合場景而且執行compose結果爲:

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

從中咱們能夠清晰的看到middleware函數中的next函數相互鏈接,這裏體現了compose FP編程思想中代碼組合的強大做用。再結合洋蔥模型的圖片,不難理解是怎麼樣的一個工做流程。

最後咱們看結果,當咱們觸發一個store.dispath的時候進行分發。則會先進入middlewareA而且打印A start 而後進入next函數,也就是middlewareB同時打印B start,而後觸發next函數,這裏的next函數就是middlewareC,而後打印C start,以後才處理dispath,處理完成後先打印C end,而後B end,最後A end。完成總體流程。

小結

  • Redux applyMiddleware機制的核心在於,函數式編程(FP)compose組合函數,需將全部的中間件串聯起來。
  • 爲了配合compose對單參函數的使用,對每一箇中間件採用currying的設計。同時,利用閉包原理作到每一箇中間件共享Store。(middlewareAPI的注入)

Feedback & Bug Report


Thank you for reading this record.


  1. 純函數,它不依賴於外部環境(例如:全局變量、環境變量)、不改變外部環境(例如:發送請求、改變DOM結構),函數的輸出徹底由函數的輸入決定。好比 slice 和 splice,這兩個函數的做用並沒有二致——可是注意,它們各自的方式卻大不一樣,但無論怎麼說做用仍是同樣的。咱們說 slice 符合純函數的定義是由於對相同的輸入它保證能返回相同的輸出。而 splice 卻會嚼爛調用它的那個數組,而後再吐出來;這就會產生可觀察到的反作用,即這個數組永久地改變了。能夠看到,splice改變了原始數組,而slice沒有。咱們認爲,slice不改變原來數組的方式更加「安全」。改變原始組數,是一種「反作用」。
相關文章
相關標籤/搜索