Redux 學習筆記 – 原始碼閱讀

zhe.zhang發表於2019-01-15

同步自我的 部落格

很久之前就看過一遍 Redux 相關技術棧的原始碼,最近在看書的時候發現有些細節已經忘了,而且發現當時的理解有些偏差,打算寫幾篇學習筆記。這是第一篇,主要記錄一下我對 Redux 、redux-thunk 原始碼的理解。我會講一下大體的架構,和一些核心部分的程式碼解釋,更具體的程式碼解釋可以去看我的 repo,後續會繼續更新 react-redux,以及一些別的 redux 中介軟體的程式碼和學習筆記。

注意:本文不是單純的講 API,如果不瞭解的可以先看一下文件,或者 google 一下 Redux 相關的基礎內容。

整體架構

在我看來,Redux 核心理念很簡單

  1. store 負責儲存資料
  2. 使用者觸發 action
  3. reducer 監聽 action 變化,更新資料,生成新的 store

程式碼量也不大,原始碼結構很簡單:

.src
    |- utils
    |- applyMiddleware.js
    |- bindActionCreators.js
    |- combineReducers.js
    |- compose.js
    |- createStore.js
    |- index.js複製程式碼

其中 utils 只包含一個 warning 相關的函式,這裡就不說了,具體講講別的幾個函式

index.js

這是入口函式,主要是為了暴露 ReduxAPI

這裡有這麼一段程式碼,主要是為了校驗非生產環境下是否使用的是未壓縮的程式碼,壓縮之後,因為函式名會變化,isCrushed.name 就不等於 isCrushed

if (
  process.env.NODE_ENV !== `production` &&
  typeof isCrushed.name === `string` &&
  isCrushed.name !== `isCrushed`
) {
    warning(...)
)}複製程式碼

createStore

這個函式是 Redux 的核心部分了,我們先整體看一下,他用到的思路很簡單,利用一個閉包,維護了自己的私有變數,暴露出給呼叫方使用的 API

// 初始化的 action
export const ActionTypes = {
    INIT: `@@redux/INIT`
}

export default function createStore(reducer, preloadedState, enhancer) {

    // 首先進行各種引數獲取和型別校驗,不具體展開了
    if (typeof preloadedState === `function` && typeof enhancer === `undefined`) {
        enhancer = preloadedState
        preloadedState = undefined
  }
  if (typeof enhancer !== `undefined`) {...}
  if (typeof reducer !== `function`) {...}

  //各種初始化
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  // 儲存一份 nextListeners 快照,後續會講到它的目的
  function ensureCanMutateNextListeners() {
        if (nextListeners === currentListeners) {
            nextListeners = currentListeners.slice()
        }
  }

  function getState(){...}

  function subscribe(){...}

  function dispatch(){...}

  function replaceReducer(){...}

  function observable(){...}

  // 初始化
  dispatch({ type: ActionTypes.INIT })

  return {
        dispatch,
        subscribe,
        getState,
        replaceReducer,
        [$$observable]: observable
  }
}複製程式碼

下面我們具體來說

ActionTypes

這裡的 ActionTypes 主要是宣告瞭一個預設的 action,用於 reducer 的初始化。

ensureCanMutateNextListeners

它的目的主要是儲存一份快照,下面我們就講講 subscribe,以及為什麼需要這個快照

subscribe

目的是為了新增一個監聽函式,當 dispatch action 時會依次呼叫這些監聽函式,程式碼很簡單,就是維護了一個回撥函式陣列

function subscribe(listener) {
    // 異常處理
    ...

    // 標記是否有listener
    let isSubscribed = true

    // subscribe時儲存一份快照
    ensureCanMutateNextListeners()
    nextListeners.push(listener)

      // 返回一個 unsubscribe 函式
    return function unsubscribe() {
        if (!isSubscribed) {
            return
        }

        isSubscribed = false
        // unsubscribe 時再儲存一份快照
        ensureCanMutateNextListeners()
        //移除對應的 listener
        const index = nextListeners.indexOf(listener)
        nextListeners.splice(index, 1)
    }
}複製程式碼

這裡我們看到了 ensureCanMutateNextListeners 這個儲存快照的函式,Redux 的註釋裡也解釋了原因,我這裡直接說說我的理解:由於我們可以在 listeners 裡巢狀使用 subscribeunsubscribe,因此為了不影響正在執行的 listeners 順序,就會在 subscribeunsubscribe 時儲存一份快照,舉個例子:

store.subscribe(function(){
    console.log(`first`);

    store.subscribe(function(){
        console.log(`second`);
    })    
})
store.subscribe(function(){
    console.log(`third`);
})
dispatch(actionA)複製程式碼

這時候的輸出就會是

first
third複製程式碼

在後續的 dispatch 函式中,執行 listeners 之前有這麼一句:

const listeners = currentListeners = nextListeners複製程式碼

它的目的則是確保每次 dispatch 時都可以取到最新的快照,下面我們就來看看 dispatch 內部做了什麼。

dispatch

dispatch 的內部實現非常簡單,就是將當前的 stateaction 傳入 reducer,然後依次執行當前的監聽函式,具體解析大概如下:

function dispatch(action) {
    // 這裡兩段都是異常處理,具體程式碼不貼了
    if (!isPlainObject(action)) {
        ...
    }
    if (typeof action.type === `undefined`) {
        ...
    }

    // 立一個標誌位,reducer 內部不允許再dispatch actions,否則丟擲異常
    if (isDispatching) {
        throw new Error(`Reducers may not dispatch actions.`)
    }

    // 捕獲前一個錯誤,但是會將 isDispatching 置為 false,避免影響後續的 action 執行
    try {
         isDispatching = true
         currentState = currentReducer(currentState, action)
    } finally {
         isDispatching = false
    }

      // 這就是前面說的 dispatch 時會獲取最新的快照
    const listeners = currentListeners = nextListeners

    // 執行當前所有的 listeners
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i]
        listener()
    }

    return action
}複製程式碼

這裡有兩點說一下我的看法:

  1. 為什麼reducer 內部不允許再 dispatch actions?我覺得主要是為了避免死迴圈。
  2. 在迴圈執行 listeners 時有這麼一段
const listener = listeners[i]
listener()複製程式碼

乍一看覺得會為什麼不直接 listeners[i]() 呢,仔細斟酌一下,發現這樣的目的是為了避免 this 指向的變化,如果直接執行 listeners[i](),函式裡的 this 指向的是 listeners,而現在就是指向的 Window

getState

獲取當前的 state,程式碼很簡單,就不貼了。

replaceReducer

更換當前的 reducer,主要用於兩個目的:1. 本地開發時的程式碼熱替換,2:程式碼分割後,可能出現動態更新 reducer的情況

function replaceReducer(nextReducer) {
     if (typeof nextReducer !== `function`) {
        throw new Error(`Expected the nextReducer to be a function.`)
    }

      // 更換 reducer
    currentReducer = nextReducer
    // 這裡會進行一次初始化
    dispatch({ type: ActionTypes.INIT })
}複製程式碼

observable

主要是為 observable 或者 reactive 庫提供的 APIReux 內部並沒有使用這個 API,暫時不解釋了。

combineReducers

先問個問題:為什麼要提供一個 combineReducers

我先貼一個正常的 reducer 程式碼:

function reducer(state,action){
    switch (action.type) {
        case ACTION_LIST:
        ...
        case ACTION_BOOKING:
        ...
    }
}複製程式碼

當程式碼量很小時可能發現不了問題,但是隨著我們的業務程式碼越來越多,我們有了列表頁,詳情頁,填單頁等等,你可能需要處理 state.list.product[0].name,此時問題就很明顯了:由於你的 state 獲取到的是全域性 state,你的取數和修改邏輯會非常麻煩。我們需要一種方案,幫我們取到區域性資料以及拆分 reducers,這時候 combineReducers 就派上用場了。

原始碼核心部分如下:

export default function combineReducers(reducers) {
    // 各種異常處理和資料清洗
    ... 

    return function combination(state = {}, action) {

        const finalReducers = {};
        // 又是各種異常處理,finalReducers 是一個合法的 reducers map
        ...

        let hasChanged = false;
        const nextState = {};
        for (let i = 0; i < finalReducerKeys.length; i++) {
            const key = finalReducerKeys[i];
            const reducer = finalReducers[key];
            // 獲取前一次reducer
            const previousStateForKey = state[key];
            // 獲取當前reducer
            const nextStateForKey = reducer(previousStateForKey, action);

            nextState[key] = nextStateForKey;
            // 判斷是否改變
            hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
        }
        // 如果沒改變,返回前一個state,否則返回新的state
        return hasChanged ? nextState : state;
    }
}複製程式碼

注意這一句,每次都會拿新生成的 state 和前一次的對比,如果引用沒變,就會返回之前的 state,這也就是為什麼值改變後 reducer 要返回一個新物件的原因。

hasChanged = hasChanged || nextStateForKey !== previousStateForKey;複製程式碼

隨著業務量的增大,我們就可以利用巢狀的 combineReducers 拼接我們的資料,但是就筆者的實踐看來,大部分的業務資料都是深巢狀的簡單資料操作,比如我要將 state.booking.people.name 置為測試姓名,因此我們這邊有一些別的解決思路,比如使用高階 reducer,又或者即根據 path 來修改資料,舉個例子:我們會 dispatch(update(`booking.people.name`,`測試姓名`)),然後在 reducer 中根據 booking.people.name 這個 path 更改對應的資料。

compose

接受一組函式,會從右至左組合成一個新的函式,比如compose(f1,f2,f3) 就會生成這麼一個函式:(...args) => f1(f2(f3(...args)))

核心就是這麼一句

return funcs.reduce((a, b) => (...args) => a(b(...args)))複製程式碼

拿一個例子簡單解析一下

[f1,f2,f3].reduce((a, b) => (...args) => a(b(...args)))

step1: 因為 reduce 沒有預設值,reduce的第一個引數就是 f1,第二個引數是 f2,因此第一個迴圈返回的就是 (...args)=>f1(f2(...args)),這裡我們先用compose1 來代表它

step2: 傳入的第一個引數是前一次的返回值 compose1,第二個引數是 f3,可以得到此次的返回是 (...args)=>compose1(f3(...args)),即 (...args)=>f1(f2(f3(...args)))複製程式碼

bindActionCreator

簡單說一下 actionCreator 是什麼

一般我們會這麼呼叫 action

dispatch({type:"Action",value:1})複製程式碼

但是為了保證 action 可以更好的複用,我們就會使用 actionCreator

function actionCreatorTest(value){
    return {
        type:"Action",
        value
    }
}

//呼叫時
dispatch(actionCreatorTest(1))複製程式碼

再進一步,我們每次呼叫 actionCreatorTest 時都需要使用 dispatch,為了再簡化這一步,就可以使用 bindActionCreatoractionCreator 做一次封裝,後續就可以直接呼叫封裝後的函式,而不用顯示的使用 dispatch了。

核心程式碼就是這麼一段:

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}複製程式碼

下面的程式碼主要是對 actionCreators 做一些操作,如果你傳入的是一個 actionCreator 函式,會直接返回一個包裝過後的函式,如果你傳入的一個包含多個 actionCreator 的物件,會對每個 actionCreator 都做一個封裝。

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === `function`) {
    return bindActionCreator(actionCreators, dispatch)
  }

  //型別錯誤
  if (typeof actionCreators !== `object` || actionCreators === null) {
    throw new Error(
      ...
    )
  }

  // 處理多個actionCreators
  var keys = Object.keys(actionCreators)
  var boundActionCreators = {}
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i]
    var actionCreator = actionCreators[key]
    if (typeof actionCreator === `function`) {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}複製程式碼

applyMiddleware

想一下這種場景,比如說你要對每次 dispatch(action) 都做一次日誌記錄,方便記錄使用者行為,又或者你在做某些操作前和操作後需要獲取服務端的資料,這時可能需要對 dispatch 或者 reducer 做一些封裝,redux 應該是想好了這種使用者場景,於是提供了 middleware 的思路。

applyMiddleware 的程式碼也很精煉,具體程式碼如下:

export default function applyMiddleware(...middlewares) {
    return (createStore) => (reducer, preloadedState, enhancer) => {
        const store = createStore(reducer, preloadedState, enhancer)
        let dispatch = store.dispatch
        let chain = []

        const middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action) 
        }

        chain = middlewares.map(middleware => middleware(middlewareAPI))

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

        return {
            ...store,
            dispatch
        }
    }
}複製程式碼

可以看到 applyMiddleware 內部先用 createStorereducer 生成了 store,之後又用 store 生成了一個 middlewareAPI,這裡注意一下 dispatch: (action) => dispatch(action),由於後續我們對 dispatch 做了修改,為了保證所有的 middleware 中能拿到最新的 dispatch,我們用了閉包對它進行了一次包裹。

之後我們執行了

chain = middlewares.map(middleware => middleware(middlewareAPI))複製程式碼

生成了一個 middleware[m1,m2,...]

再往後就是 applyMiddleware 的核心,它將多個 middleWare 串聯起來並依次執行

dispatch = compose(...chain)(store.dispatch)複製程式碼

compose 我們之前有講過,這裡其實就是 dispatch = m1(m2(dispatch))

最後,我們會用新生成的 dispatch 去覆蓋 store 上的 dispatch

但是,在 middleware 內部究竟是如何實現的呢?我們可以結合 redux-thunk 的程式碼一起看看,redux-thunk 主要是為了執行非同步操作,具體的 API 和用法可以看 github,它的原始碼如下:

function createThunkMiddleware(extraArgument) {
    return ({ dispatch, getState }) => next => action => {
        if (typeof action === `function`) {
            return action(dispatch, getState, extraArgument);
        }

        // 用next而不是dispatch,保證可以進入下一個中介軟體
        return next(action);
    };
}複製程式碼

這裡有三層函式

  1. ({ dispatch, getState })=> 這一層對應的就是前面的 middleware(middlewareAPI)
  2. next=> 對應前面 compose 鏈的邏輯,再舉個例子,m1(m2(dispatch)),這裡 dispatchm2nextm2(dispatch) 返回的函式是 m1next,這樣就可以保證執行 next 時可以進入下一個中介軟體
  3. action 這就是使用者輸入的 action

到這裡,整個中介軟體的邏輯就很清楚了,這裡還有一個點要注意,就是在中介軟體的內部,dispatchnext 是要注意區分的,前面說到了,next 是為了進入下一個中介軟體,而由於之前提到的 middlewareAPI 用到了閉包,如果在這裡執行 dispatch 就會從最一開始的中介軟體重新再走一遍,如果 middleWare 一直呼叫 dispatch 就可能導致無限迴圈。

那麼這裡的 dispatch 的目的是什麼呢?就我看來,其實就是取決與你的中介軟體的分發思路。比如你在一個非同步 action 中又呼叫了一個非同步 action,此時你就希望再經過一遍 thunk middleware,因此 thunk 中才會有 action(dispatch, getState, extraArgument),將 dispatch 傳回給呼叫方。

小結

結合這一段時間的學習,讀了第二篇原始碼依然會有收穫,比如它利用函式式和 curry 將程式碼做到了非常精簡,又比如它的中介軟體的設計,又可以聯想到 AOPexpress 的中介軟體。

那麼,redux 是如何與 react 結合的?promisesaga 又是如何實現的?與 thunk 相比有和優劣呢?後面會繼續閱讀原始碼,記錄筆記,如果有興趣也可以 watch 我的 repo 等待後續更新。

相關文章