從應用到原始碼-深入淺出Redux

語言: CN / TW / HK

theme: awesome-green

引言

大家好,這是一篇沒有任何注水的 Redux 從入門到精通的原始碼解讀文章。

文章中的每一行程式碼都是筆者深思熟慮敲下的,歡迎對 Redux 感興趣的同學共同討論。

文章篇幅較長,建議收藏逐步閱讀。希望文章中的內容可以對大家有所啟發。

createStore

基礎概念

談起 redux 首當其衝必須從最開始的 createStore 入口方法談起,我們先來看看 createStore 的用法。

語法: createStore(reducer, [preloadedState], enhancer)

createStore 通過傳入三個引數建立當前應用中的唯一 store 例項物件,注意它是全域性唯一單例。

後續我們可以通過 createStore 返回的 dispatch、subscribe、getState 等方法對於 store 中存放的資料進行增刪改查。

我們來看看所謂傳入的三個引數:

reducer

reducer 作為第一個引數,它必須是一個函式。

相信有過 redux 開發經驗的同學,對於 reducer 並不陌生。比如一個常見的 reducer 就像下面這個樣子:

```js import { Reducer } from "redux"

interface NameReducerState { name: string }

interface NameReducerAction { type: typeof CHANGE_NAME;

}

const initialState = { name: '' }

const CHANGE_NAME = 'change' export const changeAction = (payload: string) => ({ type: CHANGE_NAME, payload })

const name: Reducer = (state = initialState, action) => { switch (action.type) { case CHANGE_NAME: return { name: action.payload } default: return state } }

export default name ```

上邊的 name 函式就是一個 reducer 函式,這個函式接受兩個引數分別為

  • state 這個引數表示當前 reducer 中舊的狀態。

  • action 它表示本次處理的動作,它必須擁有 type 屬性。在reducer函式執行時會匹配 action.type 執行相關邏輯(當然,在 action 物件中也可以傳遞一些額外的屬性作為本次reducer執行時的引數)。

需要額外注意的是,在 redux 中要求每個 reducer 函式中匹配到對應的 action 時需要返回一個全新的物件(兩個物件擁有完全不同的記憶體空間地址)。

preloadedState

preloadedState 是一個可選引數,它表示通過 createStore 建立 store 時傳遞給 store 中的 state 的初始值。

簡單來說,預設情況下通過 createStore 不傳入 preloadedState 時,當前 store 中的 state 值會是通過傳入的 reducer 中第一個引數 initizalState 的預設值來建立的。

比如這樣:

```js function reducer(state = { number: 1 }, action) { switch (action.type) { case 'add': return { number: state.number + 1 } default: return state } }

// 不傳入preloadedState const store = createStore(reducer)

console.log(store.getState()) // { number: 1 }

// ----此處分割線----

function reducer(state = { number: 1 }, action) { switch (action.type) { case 'add': return { number: state.number + 1 } default: return state } }

const store = createStore(reducer, { number: 100 })

console.log(store.getState()) // { number: 100 } ```

相信通過上邊兩個例子大家已經明顯能感受到 preloadedState 代表的含義,通過在進行服務端同構應用時這個引數會起到很大的作用。

當然,這個引數與 combineReducers 或多或少存在一些關係。我們會在稍後談論到。

enhancer

enhancer 直譯過來意味增強劑,其實也就是 middleware 的作用。

我們可以利用 enhancer 引數擴充套件 redux 對於 store 介面的改變,讓 redux 支援更多各種各樣的功能。

當前,關於 enhancer 在文章的後續我們會詳細深入。本質上它仍然通過一組高階函式(HOC)來拓展現有 redux 中 store 功能的輔助中介軟體函式。

實現

思路梳理

在上邊我們簡單介紹了 createStore 基礎的用法以及對應的含義,那麼此時我們直接上手來實現一下對應的 createStore 函式吧。

輸入

首先,createStore 方法會接受三個引數。上邊我們分析過分別為 reducer、preloadedState 以及 enhancer 。

關於 enhancer 我們現在暫時先拋開它,後續我們會詳細詳細就這個點來展開。

輸出

createStore 返回的 store 中會返回以下幾個方法:

  • dispatch

dispatch 是一個方法,修改 store 中的 state 值的唯一途徑就是通過呼叫 dispatch 方法匹配 dispatch 傳入的 type 欄位從而執行對應匹配 reducer 函式中的邏輯修改 state 。

比如:

```js function reducer(state = { number: 1 }, action) { switch (action.type) { case 'add': return { number: state.number + 1 } default: return state } }

const store = createStore(reducer, { number: 100 })

console.log(store.getState()) // { number: 100 }

// ---後續程式碼會省略上述建立store邏輯-----

// 派發dispatch 修改store store.dispatch({ type: 'add' })

console.log(store.getState()) // { number: 101 } ```

  • subscribe

subscribe(listener) 方法會接受一個函式作為引數,每當通過 dispatch 派發 Action 修改 store 中的 state 狀態時,subscribe 方法中傳入的 callback 會依次執行。

並且在傳入的 listener callback 中可以通過 store.getState() 獲得修改後最新的 state 。

需要注意的是 subscriptions 在每次進行 dispatch 之前都會針對於舊的 subscriptions 儲存一份快照。

這也就意味著當 subscriptions 中某個 subscription 正在執行時去掉 or 訂閱新的 subscription 對於當前 dispatch 並不會有任何影響。

  • getState

getState 方法正如它定義的名字一般,它會返回應用當前的 state 樹。

  • replaceReducer

replaceReducer 方法接受一個 reducer 作為引數,它會替換 store 當前用來計算 state 的 reducer。

思路

未命名檔案.png

整體思路我畫了一張草圖來給大家提供一些思路,核心其實就是在 createStore 中通過閉包的形式訪問內部的 state 從而進行一系列操作。

當然,也許現在對於這張圖你會感到疑惑。沒關係,稍後我們自己實現完基礎的 redux 後在回頭來看我相信你會清晰的。

實現

現在我們來稍微實現一般基礎版的 redux :

首先我們我們在 src/core 中新建一個 createStore.ts 檔案,根據剛才提到的思路我們來填充這個方法:

createStore.ts 基礎邏輯

```js

/* * 建立Store * @param reducer 傳入的reducer * @param loadedState 初始化的state * @returns / function createStore(reducer,loadedState) { // reducer 必須是一個函式 if (typeof reducer !== 'function') { throw new Error( Expected the root reducer to be a function. ) }

// 初始化的state let currentState = loadedState // 初始化的reducer let currentReducer = reducer // 初始化的listeners let currentListeners = [] // listeners 的快照副本 let nextListeners = currentListeners // 是否正在dispatch let isDispatching = false

/* * 派發action觸發reducer / function dispatch(action) {

}

/* * 訂閱store中action的變化 / function subscribe() {

}

/* * 獲取State / function getState() { return currentState }

/* * 替換store中的reducer * @param reducer 需要替換的reducer / function replaceReducer(reducer) {

}

return { dispatch, replaceReducer, getState, subscribe } }

export default createStore ```

我們構造了基礎的 createStore 邏輯,僅僅是填充了 getState 方法。

因為這個方法非常簡單,它就是獲得當前 store 內部中的 currentState 。

dispatch 方法

接下來我們來看看對應的 dispatch 方法:

我們提到過 dispatch 方法會接受一個 action 的引數,通過匹配 action.type 來匹配 reducer 中的分支從而返回一個全新的 State 。

```js // ...

/* * 派發action觸發reducer / function dispatch(action) { // action 必須是一個純物件 if (!isPlainObject(action)) { throw new Error(You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples. ) }

// action 必須存在一個 type 屬性
if (typeof action.type === 'undefined') {
  throw new Error(
    'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.'
  )
}

// 如果當前正在dispatching狀態 報錯
if (isDispatching) {
  throw new Error('Reducers may not dispatch actions.')
}

try {
  isDispatching = true
  // 呼叫reducer傳入的currentState和action
  // reducer的返回值賦給currentState
  currentState = currentReducer(currentState, action)
} finally {
  isDispatching = false
}

// 當reducer執行完畢後 通知訂閱的listeners 依次進行執行
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
  const listener = listeners[i]
  listener()
}

// dispatch完成返回action
return action

} // ...

```

其實可以看到 dispatch 的邏輯非常清晰的,首先 dispatch 函式中進行了引數校驗。

傳入的action必須是一個物件,並且必須具有 type 屬性,同時當前 store 中的 isDispatching 必須為 false 。

當滿足邊界條件後,首先會將 isDispatching 重置為 true 的狀態。

之後呼叫傳入的 currentReducer 函式,傳入舊的 state 以及傳入的 action 執行 reducer ,將 reducer 中返回的結果重新賦值給 currentState。

其實 dispatch 的邏輯非常簡單,完全就是利用閉包的效果。傳入 store 內部維護的 currentState 以及傳入的 action 作為引數呼叫 createStore 時傳入的 reducer 函式獲得返回值更新 currentState 。

同時在 action 執行完畢後,遍歷 nextListeners 中訂閱的函式,依次執行 nextListeners 中的函式。

subscribe 方法

上邊我們提到了,在通過 action 觸發 reducer 執行完成後,會依次呼叫 nextListeners 中的方法。

那麼 nextListeners 中的方法是哪裡來的呢? 恰恰是通過 createStore 返回的 subscribe 進行訂閱的。

它的邏輯非常的簡單,相信不少同學已經可以猜出來它是如何實現的。同樣也是利用閉包的特性配合釋出訂閱模式,通過 subscribe 方法傳入 listener 進行訂閱,在每次 action 派發結束後依次呼叫訂閱的 listener。

``js /** * 訂閱store中action的變化 */ function subscribe(listener: () => void) { // listener 必須是一個函式 if (typeof listener !== 'function') { throw new Error(Expected the listener to be a function. Instead.` ) } // 如果當前正在執行 reducer 過程中,不允許進行額外的訂閱 if (isDispatching) { throw new Error() }

// 標記當前listener已經被訂閱了
let isSubscribed = true

// 確保listeners正確性
ensureCanMutateNextListeners()
// 為下一次的listeners中新增傳入的listener
nextListeners.push(listener)

// 返回取消訂閱的函式
return function unsubscribe() {
  // 如果當前listener已經被取消(未訂閱狀態,那麼之際返回)
  if (!isSubscribed) {
    return
  }
  // 當前如果是reducer正在執行的過程,取消訂閱直接報錯
  // 換言之,如果在reducer函式執行中呼叫取消訂閱,那麼直接報錯
  if (isDispatching) {
      // 直接報錯
      throw new Error()
  }

  // 標記當前已經取消訂閱
  isSubscribed = false

  // 同樣確保listeners正確性
  ensureCanMutateNextListeners()
  // 邏輯很簡單了利用 indexOf + splice 刪除當前訂閱的listener
  const index = nextListeners.indexOf(listener)
  nextListeners.splice(index, 1)
  currentListeners = null
}

} ```

上述的邏輯總體比較簡單,本質上 subscribe 方法通過操作 nextListeners 陣列從而控制訂閱的 listeners 。

不過,細心的同學可能會發現對應的 ensureCanMutateNextListeners 並沒有實現。我們來看看這個方法究竟是在做了什麼:

js function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } 上邊在進行分析時,我們提到過:

需要注意的是 subscriptions 在每次進行 dispatch 之前都會針對於舊的 subscriptions 儲存一份快照。

這也就意味著當 subscriptions 中某個 subscription 正在執行時去掉 or 訂閱新的 subscription 對於當前 dispatch 並不會有任何影響。

這裡的 ensureCanMutateNextListeners 恰恰是為了實現這兩句中的額外補充邏輯。

在之前我們實現的 dispatch 方法,在 dispatch 觸發的 reducer 函式執行完畢後會派發對應的 listeners 依次進行執行。

此時,如果在訂閱的 listeners 列表中的 listener 函式再次進行了 store.subscribe 或者呼叫了已被訂閱的 listener 函式的取消訂閱方法的話。那麼此時並不會立即生效。

所謂不會立即生效的原因就是在這裡,在進行 subscribe 時首先會判斷 nextListeners === currentListeners 是否相等。

如果相等的話,那麼就會對於 nextListeners 進行重新賦值,會將當前 currentListeners 這個陣列進行一次淺拷貝。

注意由於 JavaScript 引用型別的關係,此時 nextListeners 已經是一個全新的物件,指向了一個新的記憶體空間。

而在 dispatch 函式中的 listeners 執行時 :

```js // dispatch 函式

// 當reducer執行完畢後 通知訂閱的listeners 依次進行執行 const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } ```

此時的 listeners 由於引用型別的關係,指標仍然指向舊的(被淺拷貝的原始物件)。所以後續無論是針對於新的 nextListeners 進行新增還是取消訂閱,並不會在本輪 dispatch 後的 listeners 中立即生效,而是會在下一次 dispatch 時才會生效。

replaceReducer

接來下我們來看看對應的 replaceReducer 方法,在編寫 replaceReducer 方法前我們先來思考一個另外的邏輯。

不知道有沒有細心的朋友發現了沒有,我們一起來看看這段程式碼: ```js

function reducer(state = { number: 1 }, action) { switch (action.type) { case 'add': return { number: state.number + 1 } default: return state } }

const store = createStore(reducer)

console.log(store.getState()) ```

此時如果我們沒有傳遞 loadedState 的話,那麼,當我們直接呼叫 store.getState() 按照我們的程式碼應該返回的 currentState 是 undeinfed 沒錯吧。

顯然這並不是期望的結果,當呼叫 createStore 時未傳入 loadedState 時我們希望 currentState 的值是傳入 reducer 函式中第一個引數的預設引數(也就是{number:1})。

那麼此時應該如何去處理這個呢,其實答案非常簡單。Redux 在 createStore 的函式結尾派發了一次type 為 隨機的 action 。

```js function createStore() { // ....

// 派發了一次type為隨機的 action ActionTypes.REPLACE其實就是一個隨機的字串
dispatch({ type: ActionTypes.REPLACE })
return {
    dispatch,
    replaceReducer,
    getState,
    subscribe
}

} ```

同學們可以回憶一下,通常我們在編寫 reducer 函式中是否對於匹配的 action.type 存在當任何型別都不匹配 action.type 時,預設會返回傳入的 state :

js function reducer(state = { number: 1 }, action) { switch (action.type) { case 'add': return { number: state.number + 1 } default: return state } }

在 createStore 函式中,首次派發了一個 action 並且它的型別不會匹配 reducer 中任何的 actionType。

那麼此時呼叫 reducer ,state 的值會變成預設引數進行初始化。同時在 reducer 執行完成會將返回值賦值給 currentState 。

這樣是不是就達到了當沒有傳入 loadedState 引數時,初始化 currentState 為 reducer 中 state 的預設引數的效果了嗎。

當然,如果傳入了 loadedState 的話。那麼由於第一次派發 action 時任何東西都不會匹配並且傳入 reducer 的第一個引數 state 是存在值的(loadedState)。

那麼此時,currentState 仍然為 loadedState 。

搞清楚了這個點之後,我們再回到 replaceReducer 的實現上:

``js /** * 替換store中的reducer * @param reducer 需要替換的reducer */ function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error(Expected the nextReducer to be a function. Instead, received: '}` ) }

currentReducer = nextReducer

dispatch({ type: '@redux/INIT$`' })

return {
  dispatch,
  replaceReducer,
  getState,
  subscribe
}

} ```

replaceReducer 函式我並沒有進行逐行註釋。其實它的邏輯也非常簡單,就是單純替換 currentReducer 為 nextReducer。

同時派發了一個初始化的 action 。

上述完整的程式碼倉庫你可以在這裡看到:程式碼倉庫地址

這個地址我會不定時根據文章更新一些原始碼解讀的相關內容,有興趣的小夥伴可以 star 關注。

原始碼分析

其實這裡 createStore 的原始碼就已經沒有任何原始碼分析的必要了。

大家可以看到本身 createStore 做的事情非常簡單,通過閉包儲存一系列變數返回對應 API 提供給使用方去呼叫。

完整的原始碼地址你可以在這裡查閱到,我想說的是其實上述實現的程式碼已經可以說一比一還原了 redux 中 createStore 的原始碼了。

不過,唯一一些的不同點就是關於一些型別定義 ts 型別的補充,以及 createStore 的返回值原始碼中會額外多出一個 [$$observable], 日常中對於它的應用可以說是少之又少所以這裡忽略了這個方法,當然有興趣的同學可以自行查閱。

bindActionCreators

基礎概念

通常我們在使用 React 的過程中會遇到這麼一種情況,父元件中需要將 action creator 往下傳遞下到另一個元件上。

但是通常我們並不希望子元件中可以察覺到 Redux 的存在,我們更希望子元件中的邏輯更加純粹並不需要通過dispatch 或 Redux store 傳給它 。

也許接觸 redux 不多的同學,不太清楚什麼是 action creator 。沒關係,這非常簡單。

```js const ADD = 'ADD'

// 建立一個ActionCreator const addAction = () => ({ type: ADD })

function reducer(state = { number: 1 }, action) { switch (action.type) { case ADD: return { number: state.number + 1 } default: return state } }

const store = createStore(reducer)

// 通過actionCreator派發一個action store.dispatch(addAction()) ```

我們將上述的程式碼經過了簡單的修改(其實本質上是一模一樣的,只是額外進行了一層包裝)。

這裡的 addAction 函式就被稱為 actionCreator 。所謂的 actionCreator 本質上就是一個函式,通過呼叫這個函式返回對應的 action 提供給 dispatch 進行呼叫。

可以明顯的看到,如果存在父子元件需要互相傳遞 actionCreator 時,父傳遞給子 actionCreator 那麼子仍需要通過 store.dispatch 進行呼叫。

這樣在子元件中仍然需要關聯 Redux 中的 dispatch 方法進行處理,這顯然是不太合理的。

Redux 提供了 bindActionCreators API來幫助我們解決這個問題。

bindActionCreators(actionCreators, dispatch)

引數

bindActionCreators 接受兩個必選引數作為入參:

  • actionCreators : 一個 action creator,或者一個 value 是 action creator 的物件。
  • dispatch : 一個由 Store 例項提供的 dispatch 函式。

返回值

它會返回一個與原物件類似的物件,只不過這個物件的 value 都是會直接 dispatch 原 action creator 返回的結果的函式。如果傳入一個單獨的函式作為 actionCreators,那麼返回的結果也是一個單獨的函式。

用法

它的用法非常簡單,結合上邊的程式碼我們來看看如何使用 bindActionCreators:

```js import { createStore, bindActionCreators } from 'redux'

// ... 上述原本的 DEMO 示例

// 傳入的addAction是一個原始的 actionAcreate 函式,同時傳入store.dispatch const action = bindActionCreators(addAction, store.dispatch)

// 呼叫返回的函式相當於 => store.dispatch(addAction()) action() ```

```js // 同時也支援第一個引數傳入一個物件 const action = bindActionCreators({ add: addAction }, store.dispatch)

// 通過 action.add 呼叫相當於 => store.dispatch(addAction()) action.add() ```

實現

上述我們聊了聊 bindActionCreators 的基礎概念和用法,經過了 createStore 的實現後這裡我希望同學們可以停下閱讀來思考一下換做是你會如何實現 bindActionCreators 。


具體的思路圖這裡我就不進行描繪了,因為這個 API 其實非常簡單。

```js function bindActionCreators(actionCreators, dispatch) { // 如果傳入的是函式 if (typeof actionCreators === 'function') { return bindActionCreator(actionCreators, dispatch) }

// 保證傳入的除了函式以外只能是一個Object if (typeof actionCreators !== 'object' || actionCreators === null) { throw new Error( bindActionCreators expected an object or a function) }

// 定義最終返回的物件 const boundActionCreators = {}

// 迭代actionCreators物件 for (const key in actionCreators) { const actionCreator = actionCreators[key] // 如果value是函式,那麼為boundActionCreators[key]賦值 if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } }

/* * 單個 actionCreator 執行的邏輯 * @param actionCreator * @param dispatch * @returns / function bindActionCreator( actionCreator, dispatch ) { return function (this: any, ...args: any[]) { return dispatch(actionCreator.apply(this, args)) } }

export default bindActionCreators ```

可以看到 bindActionCreators 函式實現的邏輯非常簡單。

如果傳入的 actionCreator 是一個函式,那麼它會返回利用 bindActionCreator 的新函式。新函式內部同樣利用閉包呼叫了 dispatch(actionCreator.apply(this, args)) 從而達到 派發 action(actionCreator(args)) 的效果。

如果傳入的是物件,那麼將會返回一個物件。對於物件中的 key 對應的每個 value 會利用 bindActionCreator 函式去處理。

上述完整的程式碼倉庫你可以在這裡看到:程式碼倉庫地址

原始碼解讀

同樣,原始 Redux 中的原始碼地址你可以在這裡看到

同樣,針對於 bindActionCreator 這個函式上述我們實現的邏輯其實是和原始碼中是一模一樣的。我就不過多深入原始碼解讀了。

combineReducers

隨著前端應用越來越複雜,使用單個 Reducer 的方式管理全域性 store 會讓整個應用狀態樹顯得非常臃腫且難以閱讀。

此時,Redux API 提供了 combineReducers 來為我們解決這個問題。

概念

combineReducers(reducers)

combineReducers 輔助函式的作用是,把一個由多個不同 reducer 函式作為 value 的 object,合併成一個最終的 reducer 函式,然後就可以對這個合成後的 reducer 呼叫 createStore 方法。

合併後的 reducer 可以呼叫各個子 reducer,並把它們返回的結果合併成一個 state 物件。 由 combineReducers() 返回的 state 物件,會將傳入的每個 reducer 返回的 state 按其傳遞給 combineReducers() 時對應的 key 進行命名

老樣子,我們先來看看 combineReducers 怎麼用:

```js import { combineReducers, createStore } from 'redux'

// 子reduer function counter(state = {number:1},action) { switch (action.type) { case 'add': return { number: state.number + 1 } default: return state; } }

// 子reducer function name(state = { name:'wang.haoyu'},action) { switch (action.type) { case 'changeName': return {name: action.payload}
default: return state; } }

// combineReducers 合併子reduer為總reducer const rootReducer = combineReducers({ counter, name })

const store = createStore(rootReducer)

// { name: { name:'wang.haoyu' }, counter: { number:1 } } store.getState()

store.dispatch({type: 'add'})

store.dispatch({type: 'changeName', payload: '19qingfeng'})

// { name: { name:'wang.haoyu' }, counter: { number:1 } } store.getState() ```

上述程式碼簡單列舉了 combinReducers 的使用方法,它的用法也非常簡單本質上就是合併傳入的 reducer Object 為一個 rootReducer 。

實現

思路分析

迴歸樹.png

針對於上邊的 Demo 程式碼我繪製了一張簡單的流程圖。

本質上 combinReducers 還是通過傳入的 reducerObject 建立了一層巢狀的 object 。

之後在 dispatch 過程中依次去尋找所有的 reducer 進行邏輯呼叫,最終 getState 返回一個名為 rootState 的頂級物件。

需要留意的一點是在通過 dispatch 觸發 action 時多個 reducer 之間我刻意使用了流通這個關鍵字是有原因的,我們會在稍微詳細解釋到。

程式碼實現

分析完大概的實現思路後,我們來一步一步來嘗試實現它吧。

首先,我們清楚通過 combineReducers(reducerObject) 最終會返回的是可以傳遞給 createStore 使用的函式。

那麼不難想到 combineReducers 的基礎結構如下:

```js

/* * combineReducers 接受一個 reducers 結合的物件 * @param reducers * @returns 返回combination函式 它是一個組合而來的reducer函式 / function combineReducers(reducers) { // do something return function combination(state, action) { // do something } }

export default combineReducers ```

之後我們來一步一步實現所謂的 combineReducers 邏輯。

```js /* * combineReducers 接受一個 reducers 結合的物件 * @param reducers 傳入的 reducers 是一個 Object 型別,同時 Object 中 key 為對應的 reducer 名稱,value 為對應的 reducer * @returns 返回combination函式 它是一個組合而來的reducer函式 / function combineReducers(reducers) {

// 獲得 reducers 所有 key 組成的列表 const finalReducers = Object.keys(reducers)

return function combination(state, action) { // 定義hasChanged變量表示本次派發action是否修改了state let hasChanged = false

// 定義本次reducer執行 返回的整體store
const nextState = {}

// 迭代reducers中的每個reducer
for (let key of finalReducers) {
  // 獲得reducers中當前的reducer
  const reducer = finalReducers[key]
  // 獲取當前reducers中對應的state
  const previousStateForKey = state[key]
  // 執行 reducer 傳入舊的 state 以及 action 獲得執行後的 state
  const nextStateForKey = reducer(previousStateForKey, action)
  // 更新
  nextState[key] = nextStateForKey
  // 判斷是否改變 如果該reducer中返回了全新的state 那麼重製hasChanged狀態為true
  hasChanged = hasChanged || nextStateForKey === previousStateForKey
}

// 同時最後在根據 finalReducers 的長度進行一次判斷(是否有新增reducer執行為state添加了新的key&value)
hasChanged =
  hasChanged || finalReducers.length !== Object.keys(state).length

// 通過 hasChanged 標記位 判斷是否改變並且返回對應的state
return hasChanged ? nextState : state

} } ```

上述的程式碼,我在每一行中都進行了詳細的註釋。

本質上仍然是通過內部儲存傳入的 reducers 變數,返回一個整體組裝而成的 reducer 函式。

當每次呼叫 dispatch(action) 時,會觸發返回的 combination 函式,而 combination 函式由於閉包會拿到記錄的 reducers 物件。

所以當 combination 被呼叫時非常簡單,它擁有 store 中傳入的整體 state 狀態,同時也可以通過閉包拿到對應的 reducers 集合。自然內部只需要遍歷 reducers 中每一個 reducer 並且傳入對應的 state 獲得它的返回值更新對應 rootState 即可。

這裡需要額外注意的是上邊我們強調所謂的流通

細心的同學也許會發現一個問題,當我們利用 combineReducers 合併了多個 reducer 後。

當我們派發任意一個 action 時,即使當前派發的 action 已經匹配到了對應的 reducer 並且執行完畢後。

此時剩餘的 reducer 函式並不會意味會被中止,相反剩餘 reducer 仍然也會傳入本次 action 進行繼續匹配。

這也就意味著如果不同的 reducer 中存在相同的 action.type 的匹配那麼派發 action 時所以匹配到型別的 reducer 都會被計算。

也許,你不是很明白上邊那段話。沒關係,我們來結合一個簡單的例子來看看。

```js function counter(state = { number: 1 }, action) { switch (action.type) { case 'add': return { number: state.number + 1 } default: return state } }

function name(state = { name: 'wang.haoyu' }, action) { switch (action.type) { case 'add': return { number: '19QIngfeng' } default: return state } } const store = createStore(combineReducers({ name, counter }))

// 派發一個同名的 action, counter Reducer 和 name Reducer 中都存在這個 actionType store.dispatch({ type: 'add' })

store.getState() // { name: { counter: '19Qingfeng' }, counter: { number:2 }} ```

通過上述的例子其實可以很好的解釋 Redux 中流通這個概念。

簡單來說,可以總結為通過 combineReducers 合併多個 reducer 後,觸發任意 action 無論如何所有 reducer 函式都會被執行。

你可以在這裡看到我們實現的 combineReducers 程式碼

原始碼解讀

上述其實我們已經實現了 redux 中 combineReducers 中的所有核心邏輯,原始碼中對於 combineReducers 的邏輯無非是比我們實現的版本增加了一些邊界條件的處理。

真實的原始碼位置你可以在這裡看到

首先我們從頭開始來看:

image.png

原始碼中 combinReducers 首先對於傳入的 reducers 進行了過濾,僅保留了 reducers 中 value 為函式符合條件的物件,儲存為 finalReducerKeys 。

之後對於處理後的 reducers 呼叫了 assertReducerShape(finalReducers) ,這個方法本質上也是針對於每個 reducer 進行校驗而言,要求組成 reducers 的每個 reducer 不可返回 undefined :

image.png

在結束上述兩部校驗之後,combineReducers 會獲得匹配結果的 finalReducerKeys 表示傳入的 reducers 物件 keys 組成的集合。

image.png

關於getUnexpectedStateShapeWarningMessage方法本質上仍是在進行邊界情況的校驗,這裡我就不展開帶大家一行一行看這個方法了,有興趣的同學可以自行查閱。

再剩餘的邏輯其實和之前我們實現的是一模一樣的,至此 combineReducers 的實現以及對應的原始碼也就告一段落了。

Redux 中介軟體

為什麼需要中介軟體

其實上邊我們針對於 redux 的完整生命流程基本已經討論完畢了。

不過,在上述的 API 程式碼中,我們能利用的 reducer 也僅僅只是 redux 的基礎功能,簡單舉個例子。

按照上述的使用過程,當觸發某些事件時派發 action 交給 store 之後 store 通過 action 和 舊的 state 觸發內部 reducer 最終修改 stroe.state 。

元件內部訂閱 store.state 的改變,之後在進行 rerender 看上去都是那麼一切自然。

可是,假使我們需要在 store 中處理派發一些非同步 Action 又該怎麼辦呢?顯然上述的過程完全是一個同步的過程。

上述源生 redux 的整體流程就好像這樣:

image.png

看上去非常簡單的一個過程,顯然它是不能滿足我們上述提到的需求。

Redux 提供了中介軟體的機制來幫助我們修改 dispatch 的邏輯,從而滿足各種不同的應用場景。

中介軟體是什麼

上述我們提到過 Redux 中提供了中介軟體的機制來擴充套件更多的應用場景,那麼什麼是中介軟體呢?換句話說,所謂的中介軟體究竟有什麼作用。

比如剛才的場景下,某些業務場景下我們需要派發一個非同步 Action ,此時我們需要支援傳入的 action 是一個函式,這樣在函式內部可以自由的進行非同步派發 action :

```js import { createStore } from 'redux'

const ADD = 'add'

const reducer = (state = { number: 1 }, action) => { switch (action.type) { case ADD: return { number: state.number + 1 } default: return state } }

const store = createStore(reducer)

// 儲存 store.dispatch 方法 const prevDispatch = store.dispatch

// 修改store的dispatch方法讓它支援傳入的action是函式型別 store.dispatch = (action) => { if (typeof action === 'function') { // 傳入的是函式的話,傳入prevDispatch呼叫傳入的函式 action(prevDispatch) } else { prevDispatch(action) } } ```

大家留意上述的程式碼,雖然上述程式碼粗暴的直接修改了 store.dispatch 的指向,但是 redux 中介軟體其實本質思想和它是一致的都是通過閉包訪問外部自由變數的形式去包裹原始的 action ,從而返回一個新的 action 。

此時,當我們再次呼叫 store.dispatch 時你就會發現:

```js

// 定義 actionType 注意它是一個函式 const actionType = (dispatch) => { setTimeout(() => { dispatch({ type: ADD }) }, 1000) }

// 派發函式型別的action // 此時我們支援了非同步的action派發 1s後state.number會變為 2 store.dispatch(actionType) ```

上述程式碼完全是一段可以跑起來的虛擬碼,之所以拿出來這段程式碼和大家舉例更多的是想通過一個簡單的例子來為你闡述 Redux 中介軟體究竟是在做什麼。

實現一款中介軟體

瞭解了 Redux Middleware 究竟在做什麼之後,我們來看看究竟應該如何實現一款 Middleware。

這裡我們以一款正兒八經的非同步 middleware 為基礎先來看看如何實現 Redux Middleware。

一個 Redux Middleware 必須擁有以下條件:

  • middleware 是一個函式,它接受 Store 的 dispatch 和 getState 函式作為命名引數

  • 並且每個 middleware 會接受一個名為 next 的形參作為引數,它表示下一個 middleware 的 dispatch 方法,並且返回一個接受 Action 的函式。

  • 返回的最後一個函式,這個函式可以直接呼叫 next(action),我們可以通過呼叫 next(action) 進入下一個中介軟體的邏輯,注意當已經進入呼叫鏈中最後一個 middleware 時,它會接受真實的 store 的 dispatch 方法作為 next 引數,並藉此結束呼叫鏈。

綜合上邊三點,所謂一個middleware大概的結構如下所示:

```js

/* * 非同步中介軟體 * @param param { getState,dispatch } 每個 middleware 接受 Store 的 dispatch 和 getState 函式作為命名引數 * @returns 返回一個函式 / function thunkMiddleware({ getState, dispatch }) { // 返回的 next 引數會在下一個middleware中當中當作dispatch來觸發action return function (next) { // 接受真實傳入的action return function (action) { // do something } } } ``` 一個middleware大體的結構如下所示,還是稍微比較繞的。

一個 Redux Middleware 是一個函式,它會返回一個接受 next 引數的函式,next 形參表示上一個 middleware 處理後的 dispatch 方法(就好比我們最開始的虛擬碼,此時的 next 就是我們修改後的 store.dispatch 方法)。

同時,middleware 返回的函式仍會返回一個函式。該函式才是真正的中介軟體邏輯,它接受外部 dispatch(action) 中的 action 作為引數。

大多數同學對於這些可能感覺到難以理解,沒關係此時我們可以僅考慮一箇中間件。在單箇中間件的情況下,你完全可以將 next 引數當作原本的 dispatch 方法。

```js

/* * 非同步中介軟體 * @param param { getState,dispatch } 每個 middleware 接受 Store 的 dispatch 和 getState 函式作為命名引數 * @returns 返回一個函式 / function thunkMiddleware({ getState, dispatch }) { // 返回的 next 引數會在下一個middleware中當中當作dispatch來觸發action return function (next) { // 接受真實傳入的action return function (action) { // 傳入的是函式 if (typeof action === 'function') { // 呼叫函式,將next(單箇中間件情況下它完全等同於store.dispatch)傳遞給action函式作為引數 // 修改dispatch函式的返回值為傳入函式的返回值 return action(dispatch, getState) } // 傳入的非函式 返回action // 我們之前在createStore中實現過dispatch方法~他會返回傳入的action return next(action) } } }

```

注意 thunkMiddleware 中接受的 dispatch 是已經經過所有 middleware 修改後的 dispatch 而非原始的 store.dispatch 。

上邊我們按照步驟實現了一個簡單的 Redux-Thunk 中介軟體,它支援我們傳入的 action 型別為一個函式。此時我們就可以在 Redux 中完美的使用非同步 Action 。

來看看如何使用它:

```js import { applyMiddleware, createStore } from 'redux' import thunkMiddleware from './middleware'

const ADD = 'add' const reducer = (state = { number: 1 }, action) => { switch (action.type) { case ADD: return { number: state.number + 1 } default: return state } }

const store = createStore(reducer, applyMiddleware(thunkMiddleware))

// 定義 actionType 注意它是一個函式 const actionType = () => { return (dispatch) => { setTimeout(() => { dispatch({ type: ADD }) }, 1000) } }

// 派發函式型別的action store.dispatch(actionType())

setTimeout(() => { console.log(store.getState(), '3') }, 3000) ```

注意 createStore 內部進行了函式引數的過載判斷,這裡我們第二個引數傳入 applyMiddleware(thunkMiddleware) 相當於第二個引數 preloaded 傳入 undefined 第三個引數傳入 applyMiddleware(thunkMiddleware) 。

上邊的程式碼,我們使用了 Redux 提供的 applyMiddleware API 來使用 Thunk 中介軟體。

我們對於 Action 的型別支援傳入一個函式,這個函式會接受 dispatch、getState 作為引數從而可以達到實現非同步 dispatch 的邏輯。

其實這裡不少同學也許仍然還有有很多疑惑,比如中介軟體的工作機制以及它是如何在 Redux 內部進行串聯的。彆著急,這裡你僅僅需要搞清楚一箇中間件長什麼樣子就可以了。

applyMiddleware

上邊我們在 Redux 中使用中介軟體的時候在 createStore 中傳入了第三個引數,並且使用 applyMiddleware 包裹了它。

首先我們先來看看所謂的 applyMiddleware 究竟是什麼。

概念

applyMiddleware(...middleware) 是 Reudx 提供給我們使用自定義 middleware 的拓展推薦 API 。

它提供給了我們利用 middleware 的能力來包裝 store 的 dispatch 方法從而實現任何我們想要達到的目的。

同時在 applyMiddleware 內部提供了一種可組合的機制,多個 middleware 可以通過 applyMiddleware 組合到一起使用。

引數

  • ...middleware (arguments): 遵循 Redux middleware API 的函式。每個 middleware 接受 Store 的 dispatch 和 getState 函式作為命名引數,並返回一個函式。該函式會被傳入被稱為 next 的下一個 middleware 的 dispatch 方法,並返回一個接收 action 的新函式,這個函式可以直接呼叫 next(action),或者在其他需要的時刻呼叫,甚至根本不去呼叫它。呼叫鏈中最後一個 middleware 會接受真實的 store 的 dispatch 方法作為 next 引數,並藉此結束呼叫鏈。所以,middleware 的函式簽名是 ({ getState, dispatch }) => next => action

那麼一長段話,其實簡單來說就是它接受多個 Middleware ,每個 middleware 需要和我們上邊提到的結構一致。

返回值

applyMiddleware 會返回函式。它會返回一個應用了 middleware 後的 store enhancer。

applyMiddleware 總結

applyMiddleware 本質上即使對於 Redux 提供了 middleware 的支援能力,並且同時支援傳入多個 middleware ,applyMiddleware 內部會對於傳入的 middleware 進行組合。

同時,可以看到 applyMiddleware 通常需要配合 createStore 一起使用。在 createStore 中傳遞了 applyMiddleware 後即可開啟 middleware 的支援。

稍微看到它的原始碼你就會明白它究竟在做什麼,特別簡單。

applyMiddleware 原始碼

image.png

applyMiddleware 原始碼中的每一行我都已經為它進行了詳細的註釋,可以清晰的看到 applyMiddleWare 通過傳入的引數最終返回的是一個全新的 store 。

此處的 compose 函式就是在做函式組合的事情,之後我們會詳細解讀它。

在來看看所謂 createStore 中接受的 applyMiddleWare 引數:

image.png

注意此處的當我們在 createStore 中傳入了 enhancer 時,他會進行

enhancer(createStore)(reducer,preloadedState)

明顯看到我們傳入的 applyMiddleWare 即是所謂的 enhancer ,相當於 createStore 返回的是:

applyMiddleWare(createStore)(reducer,preloadedState)

這不恰好是 createStore 返回的是通過 applyMiddleWare 返回新的 store 嗎,不過返回的 store 中的 dispatch 方法是通過各個中介軟體進行了改寫。

compose

終於到了所謂的 compose 函數了,接觸過函數語言程式設計的小夥伴或多或少都聽過 compose 函式的鼎鼎大名。

在 Redux 中集成了所謂的 compose 方法,它的作用非常簡單從右到左來組合多個函式

上邊我們看到在 applyMiddleWare 原始碼中使用了 compose 方法來組合多箇中間件的邏輯。

接下來我們就來揭開它的面紗。

所謂 compose 其實和 Redux 關係並不是很大,只是 Redux 中利用了這個方法來方便的組合中介軟體而已。

換句話說所謂 compose 組合的應用場景並不僅僅侷限於這裡,它本身就是函數語言程式設計中的概念。

開始 compose 之前我們先來定義三個簡單的中介軟體:

```js const promise = ({ getState, dispatch }) => (next) => action => { console.log('promise 中介軟體') next(action) }

const thunk = ({ getState, dispatch }) => (next) => action => { console.log('thunk 中介軟體') next(action) }

const logger = ({ getState, dispatch }) => (next) => action => { console.log('logger 中介軟體') next(action) } ```

上述程式碼中,我們定義了三個非常簡單的 Redux 中介軟體。

上邊我們提到過在 applyMiddleWare 內部對於中介軟體的處理流程:

image.png

可以看到在進行 compose 組裝之前首先呼叫了 middlewares.map(middleware => middleware(middlewareAPI))呼叫了中介軟體函式。

所以由此可得所謂的 compose 函式首先傳入的 chain 我們可以簡化成為:

```js // 直接省略最外層函式,compose處理時最外層函式已經不存在了 const promise = (next) => action => { console.log('promise 中介軟體') next(action) }

const thunk = (next) => action => { console.log('thunk 中介軟體') next(action) }

const logger = (next) => action => { console.log('logger 中介軟體') next(action) } ```

上述我們簡化了對應中介軟體需要 compose 的程式碼,注意當我們呼叫 compose 時比如:

compose(logger,thunk,promise) 中介軟體的組合順序是從右往左,換言之在真正派發 dispatch 時中介軟體的執行順序應該是相反的,也就是從左往右先執行 logger、thunk最後為promise 最後再到真實 store 中的 disaptch。

js function compose(...fns) { return (args) => { // 逆序執行 for (let i = fns.length - 1; i >= 0; i--) { const fn = fns[i] args = fn(args) } return args; }; }

原始碼中的 compose 是使用 reduce API 實現,這裡為了方便大家理解我該寫成了 for 迴圈。

可以看到 compose 的程式碼還是非常簡單的,不過這之中稍微有些繞。

首先在使用 compose 函式時:

js const composeFn = compose(logger, thunk, promise);

我們呼叫了 compose 函式傳入 三個中介軟體函式,compose 函式返回一個函式,這對你來說非常簡單對吧。

之後,我們會再次呼叫 compose 函式返回的 composeFn 並且傳入 store 的 dispatch 方法:

js // 傳入真實的dispatch方法返回處理後的dispatch方法 const dispatch = composeFn(store.dispatch);

注意,重點就在這裡了。由於閉包的原因,呼叫 compose 返回的 composeFn 可以訪問到在傳入的中介軟體函式 fns 。

此時在 composeFn 內部對於 [logger,thunk,promise] 使用 for 迴圈進行了逆序呼叫。

首先我們呼叫composeFn(store.dispatch) for 迴圈中首先會拿到 promise 中介軟體的函式也就是所謂的:

當我們呼叫 composeFn(store.dispatch) 會發生如下幾件事:

  • 首次呼叫 composeFn(store.dispatch) 傳入 args 為 store.disaptch ,注意這裡的 args 表示真實的 store.disaptch 方法。

  • 此時函式開始執行,for 迴圈首先尋找到 promise middleware ,第一次迴圈中呼叫 promise middleware 函式傳入 store.dispatch 作為引數(args = store.dispatch),呼叫結束 for 迴圈內部修改 args 指向args = fn(args)

經過 for 迴圈第一次迭代,此時 args 從 store.disaptch 變成了 promise 中返回的函式(這裡我們稱為promiseAction 函式)

js (action) => { console.log('promise 中介軟體'); // 注意這裡的next就相當於 store.disaptch // 由於閉包的原因 我們可以在這個函式呼叫時訪問到 next next(action); }

  • 此時 for 迴圈進入第二次迴圈中,fn 變成 thunk 中介軟體函式,此時呼叫 thunk middleware 函式。並且傳入的 args 引數(此時args指向上一次 promise middleware 處理後的返回函式):

js // 呼叫該函式 next 即使 args ,指向上一次處理後返回的函式 const thunk = (next) => (action) => { console.log('thunk 中介軟體'); next(action); };

第二次執行完畢後再次修改 args 的指向,讓它指向本次 middleware 返回的函式。

  • 第三次迴圈中,本質上重複了第二次的過程。修改 args 為本次 loggerMiddleware 返回的函式。

  • 迴圈結束,最終返回拼接後的 args 函式(此時store.disaptch會被重新賦值為返回的args函式)。

當我們呼叫 store.dispatch 函式時,又會經歷以下步驟:

  • 當我們呼叫 store.dispatch(action) 時,首先拿到返回的 args 函式,相當於呼叫 args(action)。

  • 由於返回的 args 是 logger 函式(逆序第一個fn)內部的函式,自然優先執行 logger 返回的函式,也就是會執行:

js (action) => { console.log('logger 中介軟體'); next(action); }

  • 此時 logger 函式中的 next 相當於上一個中介軟體的 args ,相當於執行: js (action) => { console.log('thunk 中介軟體'); next(action); };

  • 此時又回執行 thunk 中介軟體的邏輯,同理 logger 中的 next 是上一個中介軟體傳遞過來的 args,也就是 promise 中介軟體返回的函式,那麼又會執行這個函式:

js (action) => { console.log('promise 中介軟體'); next(action); };

  • 當執行完 promise 中介軟體後,此時再次呼叫 next(action)。此時這裡的 next 函式相當於第一次呼叫 composeFn 傳入的 store.dispatch 也就是 composeFn(store.dispatch) 。

  • 最終沿著裡鏈路,進行一路尋找到真實的 store.dispatch 進行派發 action 。

上述的描述過程可能仍然不是那麼容易理解,我會把完整程式碼放在下面。畢竟 compose 是一個比較抽象的過程,建議不是很清晰這一過程的同學可能自行 debugger 進行除錯一下。

```js

function compose(...fns) { return (args) => { // 逆序執行 for (let i = fns.length - 1; i >= 0; i--) { const fn = fns[i]; args = fn(args); } return args; }; }

const promise = (next) => (action) => { console.log('promise 中介軟體'); next(action); };

const thunk = (next) => (action) => { console.log('thunk 中介軟體'); next(action); };

const logger = (next) => (action) => { console.log('logger 中介軟體'); next(action); };

const composeFn = compose(logger, thunk, promise); // 這裡傳入的 () => console.log('dispatch') 是一個模擬store.dispatch的方法 const dispatch = composeFn(() => console.log('dispatch')); dispatch('123'); // log: logger 中介軟體 thunk 中介軟體 promise中介軟體 dispatch ```

最後我們再來一起看看 Redux 中 compose 的原始碼:

image.png

其實在你搞清楚上述我們實現的 compose 後在看看原始碼中的 compose 會好理解很多。

原始碼中是利用 reducer 形成一層一層閉包引用引數的關係,從而實現中介軟體的邏輯呼叫的。

比如以上述的示例 Demo 為例,原始碼中的 dispatch 方法最終會變成:

```js const promise = (next) => (action) => { console.log('promise 中介軟體'); next(action); };

const thunk = (next) => (action) => { console.log('thunk 中介軟體'); next(action); };

const logger = (next) => (action) => { console.log('logger 中介軟體'); next(action); };

const dispatch = logger(thunk(promise(disaptch)))

dispach({ type: 'add' }) // 呼叫時 第一步首先執行 logger 函式的邏輯,同時 logger 中的引數 next 為 thunk(promise(dispatch)) // 當呼叫 next(action) 時,相當於呼叫 thunk(promise(disaptch))(action) // 依次繼續執行 thunk middleware 中的邏輯,同時執行到 thunk 中的 next 時,傳遞了 action 相當於呼叫 promise(disaptch)(action) // 繼續進入 promise 中進行執行,由於 promise 中的 next 指向的是 store.disaptch 所以最終 promise 執行會執行 store.dispatch(action) , over。 ```

關於 compose 個人建議同學們有空可以複製上邊的程式碼多 debugger 幾次,自然也就搞清楚了。

結尾

文章篇幅比較長,但是總結來看 Redux 系列的所有 API 我都已經帶大家過了一遍。

之後,如果有時間的話我也會和大家分享一些 Redux 周邊生態的用法和原始碼,比如一些 react-readux、dva、immutabl 等等相關。

當然,因為之前主要技術方向不是 React 所有對於 React 周邊生態也是在逐步上手的過程。希望這篇文章可以幫助到大家,大家加油!