淺析Redux原始碼

西爽發表於2018-05-14

@(Redux)[|用法|原始碼]

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

@[三大原則]

  • 單一資料來源 - 整個應用的state被儲存在一棵object tree中,並且這個object tree只存在於唯一一個store中。所有資料會通過store.getState()方法呼叫獲取.
  • State只讀 - 根據State只讀原則,資料變更會通過store,dispatch(action)方法.
  • 使用純函式修改 -Reducer只是一些純函式,它接收先前的stateaction,並返回新的state.

[TOC]

準備階段

柯里化函式(curry)

	//curry example
	const A  = (a) => {
		return (b) => {
			return a + b
		}
	}
複製程式碼

通俗的來講,可以用一句話概括柯里化函式:返回函式的函式. 優點: 避免了給一個函式傳入大量的引數,將引數的代入分離開,更有利於除錯。降低耦合度和程式碼冗餘,便於複用.

程式碼組合(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的原始碼

	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為引數,其餘函式以上個函式結果為入引數執行。

優點: 通過這樣函式之間的組合,可以大大增加可讀性,效果遠大於巢狀一大堆的函式呼叫,並且我們可以隨意更改函式的呼叫順序

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.

相關文章