從 Redux 源碼談談函數式編程

語言: CN / TW / HK

摘要

在 React 的世界中,狀態管理方案不下幾百種,但其中最經典的,莫過於 Redux 。如果你想學習函數式編程,那麼 Redux 源碼就是最好的學習材料。考慮到不少小夥伴用的是 Vue ,本人爭取讓這篇文章寫得通俗易懂,讓沒有接觸過 React 技術棧的同學也能掌握 Redux 。

在 React 的世界中,狀態管理方案不下幾百種,但其中最經典的,莫過於 Redux 。如果你想學習函數式編程,那麼 Redux 源碼就是最好的學習材料。考慮到不少小夥伴用的是 Vue ,本人爭取讓這篇文章寫得通俗易懂,讓沒有接觸過 React 技術棧的同學也能掌握 Redux 。

在 React 的世界中,狀態管理方案不下幾百種,但其中最經典的,莫過於 Redux 。如果你想學習函數式編程,那麼 Redux 源碼就是最好的學習材料。考慮到不少小夥伴用的是 Vue ,本人爭取讓這篇文章寫得通俗易懂,讓沒有接觸過 React 技術棧的同學也能掌握 Redux 。

Redux 屬於典型的“百行代碼,千行文檔”,其中核心代碼非常少,但是思想不簡單,可以總結為下面兩點:

  • 全局狀態唯一且不可變(Immutable) ,不可變的意思是當需要修改狀態的時候,用一個新的來替換,而不是直接在原數據上做更改:
 let store = { foo: 1, bar: 2 };

 // 當需要更新某個狀態的時候
 // 創建一個新的對象,然後把原來的替換掉
 store = { ...store, foo: 111 };

這點與 Vue 恰好相反,在 Vue 中必須直接在原對象上修改,才能被響應式機制監聽到,從而觸發 setter 通知依賴更新。

狀態更新通過一個純函數(Reducer)完成。純函數(Pure function)的特點是:

  • 輸出僅與輸入有關;
  • 引用透明,不依賴外部變量;
  • 不產生副作用;

因此對於一個純函數,相同的輸入一定會產生相同的輸出,非常穩定。使用純函數進行全局狀態的修改,使得全局狀態可以被預測。

1. 需要了解的幾個概念

在使用 Redux 及閲讀源碼之前需要了解下面幾個概念:

Action

action 是一個普通 JavaScript 對象,用來描述如何修改狀態,其中需要包含 type 屬性。一個典型的 action 如下所示:

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}

Reducers

reducer 是一個純函數,其函數簽名如下:

/**
 * @param {State} state 當前狀態
 * @param {Action} action 描述如何更新狀態
 * @returns 更新後的狀態
 */
function reducer(state: State, action: Action): State

reducer 函數的名字來源於數組的 reduce 方法,因為它們類似數組 reduce 方法傳遞的回調函數,也就是上一個返回的值會作為下一次調用的參數傳入。

reducer函數的編寫需要嚴格遵頊以下規則:

  • 檢查reducer是否關心當前的action
  • 如果是,就創建一份狀態的副本,使用新的值更新副本中的狀態,然後返回這個副本
  • 否則就返回當前狀態

一個典型的 reducer 函數如下:

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  if (action.type === 'counter/incremented') {
    return {
      ...state,
      value: state.value + 1
    }
  }
  return state
}

Store

通過調用 createStore 創建的 Redux 應用實例,可以通過 getState() 方法獲取到當前狀態。

Dispatch

store 實例暴露的方法。更新狀態的唯一方法就是通過 dispatch 提交 action 。store 將會調用 reducer 執行狀態更新,然後可以通過 getState() 方法獲取更新後的狀態:

store.dispatch({ type: 'counter/incremented' })

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

storeEnhancer

createStore 的高階函數封裝,用於增強 store 的能力。Redux 的 applyMiddleware 是官方提供的一個 enhancer 。

middleware

dispatch 的高階函數封裝,由 applyMiddleware 把原 dispatch替換為包含 middleware 鏈式調用的實現。Redux-thunk 是官方提供的 middleware,用於支持異步 action 。

2. 基本使用

學習源碼之前,我們先來看下 Redux 的基本使用,便於更好地理解源碼。

首先我們編寫一個 Reducer 函數如下:

// reducer.js
const initState = {
  userInfo: null,
  isLoading: false
};

export default function reducer(state = initState, action) {
  switch (action.type) {
    case 'FETCH_USER_SUCCEEDED':
      return {
        ...state,
        userInfo: action.payload,
        isLoading: false
      };
    case 'FETCH_USER_INFO':
      return { ...state, isLoading: true };
    default:
      return state;
  }
}

在上面代碼中:

  • reducer首次調用的時候會傳入initState作為初始狀態,然後switch...case最後的default用來獲取初始狀態
  • 在switch...case中還定義了兩個action.type用來指定如何更新狀態

接下來我們創建 store :

// index.js
import { createStore } from "redux";
import reducer from "./reducer";

const store = createStore(reducer);

store 實例會暴露兩個方法 getState 和 dispatch ,其中 getState 用於獲取狀態,dispatch 用於提交 action 修改狀態,同時還有一個 subscribe 用於訂閲store的變化:

// index.js

// 每次更新狀態後訂閲 store 變化
store.subscribe(() => console.log(store.getState()));

// 獲取初始狀態
store.getState();

// 提交 action 更新狀態
store.dispatch({ type: "FETCH_USER_INFO" });
store.dispatch({ type: "FETCH_USER_SUCCEEDED", payload: "測試內容" });

我們運行一下上面的代碼,控制枱會先後打印:

{ userInfo: null, isLoading: false } // 初始狀態
{ userInfo: null, isLoading: true } // 第一次更新
{ userInfo: "測試內容", isLoading: false } // 第二次更新

3. Redux Core 源碼分析

上面的例子雖然很簡單,但是已經包含 Redux 的核心功能了。接下來我們來看下源碼是如何實現的。

createStore

可以説 Redux 設計的所有核心思想都在 createStore 裏面了。 createStore 的實現其實非常簡單,整體就是一個閉包環境,裏面緩存了 currentReducer 和 currentState ,並且定義了getState、subscribe、dispatch 等方法。

createStore 的核心源碼如下,由於這邊還沒用到 storeEnhancer ,開頭有些if...else的邏輯被省略了,順便把源碼中的類型註解也都去掉了,方便閲讀:

// src/createStore.ts
function createStore(reducer, preloadState = undefined) {
  let currentReducer = reducer;
  let currentState = preloadState;
  let listeners = [];

  const getState = () => {
    return currentState;
  }

  const subscribe = (listener) => {
    listeners.push(listener);
  }

  const dispatch = (action) => {
    currentState = currentReducer(currentState, action);

    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }

    return action;
  }
  
  dispatch({ type: "INIT" });

  return {
    getState,
    subscribe,
    dispatch
  }
}

createStore 的調用鏈路如下:

  • 首先調用 createStore 方法,傳入 reducer 和 preloadState 。preloadState 代表初始狀態,假如不傳那麼 reducer 必須要指定初始值;
  • 將 reducer 和 preloadState 分別賦值給 currentReducer 和 currentState 用於創建閉包;
  • 創建 listeners 數組,這其實就是基於發佈訂閲模式,listeners 就是發佈訂閲模式的事件中心,也是通過閉包緩存;
  • 創建 getState 、subscribe 、dispatch 等函數;
  • 調用 dispatch 函數,提交一個 INIT 的 action 用來生成初始state,在 Redux 源碼中,這裏的 type 是一個隨機數;
  • 最後返回一個包含 getState 、subscribe 、dispatch 函數的對象,即 store 實例;

那麼很顯然,外界無法訪問到閉包的值,只能通過getState函數訪問。

為了訂閲狀態更新,可以使用 subscribe 函數向事件中心 push 監聽函數(注意 listener 是允許副作用存在的)。

當需要更新狀態時,調用 dispatch 提交 action 。在 dispatch 函數中調用 currentReducer(也就是 reducer 函數),並傳入 currentState 和 action ,然後生成一個新的狀態,傳給 currentState 。在狀態更新完成後,將訂閲的監聽函數執行一遍(實際上只要調用 dispatch ,即使沒有對 state 做任何修改,也會觸發監聽函數)。

如果有熟悉面向對象編程的小夥伴可能會説,createStore裏面做的事情可以封裝到一個類裏面。確實可以,本人用 TypeScript 實現如下(發佈訂閲的功能不寫了):

type State = Object;
type Action = {
  type: string;
  payload?: Object;
}
type Reducer = (state: State, action: Action) => State;

// 定義 IRedux 接口
interface IRedux {
  getState(): State;
  dispatch(action: Action): Action;
}

// 實現 IRedux 接口
class Redux implements IRedux {
  // 成員變量設為私有
  // 相當於閉包作用
  private currentReducer: Reducer;
  private currentState?: State;

  constructor(reducer: Reducer, preloadState?: State) {
    this.currentReducer = reducer;
    this.currentState = preloadState;
    this.dispatch({ type: "INIT" });
  }
  
  public getState(): State {
    return this.currentState;
  }

  public dispatch(action: Action): Action {
    this.currentState = this.currentReducer(
      this.currentState,
      action
    );
    return action;
  }
}

// 通過工廠模式創建實例
function createStore(reducer: Reducer, preloadState?: State) {
  return new Redux(reducer, preloadState);
}

你看,多有意思,函數式編程和麪向對象編程竟然殊途同歸了。

applyMiddleware

applyMiddleware 是 Redux 中的一個難點,雖然代碼不多,但是裏面用到了大量函數式編程技巧,本人也是經過大量源碼調試才徹底搞懂。

首先要能看懂這種寫法:

const middleware =
  (store) =>
    (next) =>
      (action) => {
        // ...
      }

上面的寫法相當於:

const middleware = function(store) {
  return function(next) {
    return function(action) {
      // ...
    }
  }
}

其次需要知道,這種其實就是函數柯里化,也就是可以分步接受參數。如果內層函數存在變量引用,那麼每次調用都會生成閉包。

説到閉包,有些同學馬上就想到內存泄漏。但實際上閉包在平時項目開發中非常常見,很多時候我們不經意間就創建了閉包,但往往都被我們忽略了。

閉包一大作用就是緩存值,這和聲明一個變量在賦值的效果是類似的。而閉包的難點就在於,變量是顯式聲明,而閉包往往是隱式的,什麼時候創建閉包,什麼時候更新了閉包的值,很容易被忽略。

可以這麼説,函數式編程就是圍繞閉包展開的。在下面的源碼分析中,會看到大量閉包的例子。

applyMiddleware 是 Redux 官方實現的 storeEnhancer ,實現了一套插件機制,增加 store 的能力,例如實現異步 Action ,實現 logger 日誌打印,實現狀態持久化等等。

export default function applyMiddleware<Ext, S = any>(
  ...middlewares: Middleware<any, S, any>[]
): StoreEnhancer<{ dispatch: Ext }>

個人觀點,這樣做的好處就是提供了造輪子的空間

applyMiddleware 接受一個或多個 middleware 實例,然後再傳給createStore:

import { applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk"; // 使用 thunk 中間件
import reducer from "./reducer";

const store = createStore(reducer, applyMiddleware(thunk));

createStore 入參中只接受一個 storeEnhancer ,如果需要傳入多個,可以使用 Redux Utils 中的 compose 函數將它們組合起來。

compose 函數在後面會介紹

看上面的用法,可以猜測 applyMiddleware 肯定也是個高階函數。之前説到 createStore 前面有些if..else邏輯因為沒用到 storeEnhancer 所以被省略了。這邊我們一起來看下。

首先看 createStore 的函數簽名,實際上是可以接受 1-3 個參數。其中 reducer 是必須要傳遞的。當第二個參數為函數類型,會識別為 storeEnhancer。如果第二個參數不是函數類型,則會識別為 preloadedState ,此時還可以再傳遞一個函數類型的 storeEnhancer :

function createStore(reducer: Reducer, preloadedState?: PreloadedState | StoreEnhancer, enhancer?: StoreEnhancer): Store

可以看到源碼中參數校驗的邏輯:

// src/createStore.ts:71
if (
  (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
  (typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
  // 傳遞兩個函數類型參數的時候,拋出異常
  // 也就是隻接受一個 storeEnhancer
  throw new Error();
}

當第二個參數為函數類型,將它作為 storeEhancer 處理:

// src/createStore.ts:82
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
  enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
  preloadedState = undefined
}

接下來是一個比較難的邏輯:

// src/createStore.ts:87
if (typeof enhancer !== 'undefined') {
  // 如果使用了 enhancer
  if (typeof enhancer !== 'function') {
    // 如果 enhancer 不是函數就拋出異常
    throw new Error();
  }

  // 直接返回調用 enhancer 之後的結果,並沒有往下繼續創建 store
  // enhancer 肯定是一個高階函數
  // 先傳入了 createStore,又傳入 reducer 和 preloadedState
  // 説明很有可能在 enhancer 內部再次調用 createStore
  return enhancer(createStore)(
    reducer,
    preloadedState
  )
}

下面我們來看一下 applyMiddleware 的源碼,為便於閲讀,把源碼中的類型註解都去掉了:

// src/applyMiddleware.ts
import compose from './compose';

function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState);
    let dispatch = () => {
      throw new Error();
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    dispatch = compose(...chain)(store.dispatch);

    return {
      ...store,
      dispatch
    }
  }
}

可以看到這裏代碼並不多,但是出現了一個函數嵌套函數的情形:

const applyMiddleware = (...middlewares) =>
  (createStore) =>
    (reducer, preloadedState) => {
      // ...
    }

分析一下源碼中的調用鏈路:

  • 調用 applyMiddleware 時,傳入中間件實例,返回 enhancer 。從剩餘參數的用法看出,支持傳入多個 middleware ;
  • 由createStore調用 enhancer ,分兩次傳入 createStore 和 reducer 、preloadedState ;
  • 內部再次調用 createStore ,這次由於沒有傳 enhancer ,所以直接走創建 store 的流程;
  • 創建一個經過修飾的 dispatch 方法,覆蓋默認 dispatch ;
  • 構造 middlewareAPI ,對 middleware 注入 middlewareAPI ;
  • 將 middleware 實例組合為一個函數,再向 middleware 傳遞默認的 store.dispatch 方法;
  • 最後返回一個新的 store 實例,此時 store 的 dispatch 方法經過了 middleware 修飾;

這裏涉及到 compose 函數,是函數式編程範式中經常用到的一種處理,創建一個從右到左的數據流,右邊函數執行的結果作為參數傳入左邊,最終返回一個以上述數據流執行的函數:

// src/compose.ts:46
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return (arg) => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce(
    (a, b) =>
      (...args) =>
        a(b(...args))
  )
}

思考題:如果希望把執行順序改為從左往右,需要怎麼改?

通過這邊的代碼,我們不難推斷出一箇中間件的結構:

function middleware({ dispatch, getState }) {
  // 接收 middlewareAPI
  return function(next) {
    // 接收默認的 store.dispatch 方法
    return function(action) {
      // 接收組件調用 dispatch 傳入的 action
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      return result;
    }
  }
}

看到這裏,我想大多數讀者都會有兩個問題:

  1. 通過 middlewareAPI 獲取的 dispatch 函數和 store 實例最終暴露的 dispatch 函數都是經過修飾的嗎;
  2. 為了防止在創建 middleware 的時候調用 dispatch ,applyMiddleware 給新的 dispatch 初始化為一個空函數,且調用會拋出異常,那麼這個函數究竟在何時被替換掉的;

大家可以先試着思考一下。

説實話,本人在閲讀源碼的時候也被這兩個問題困擾,大多數技術文章也都沒有給出解釋。沒辦法,只能通過調試源碼來找答案。經過不斷調試,終於搞清楚了,middlewareAPI 的 dispatch 函數本身其實就是以閉包形式引入的,這個閉包可能沒多少人能看得出來:

// 定義新的 dispatch 方法
// 此時是一個空函數,調用會拋出異常
let dispatch = () => {
  throw new Error();
}
// 定義 middlewareAPI
// 注意這裏的 dispatch 是通過閉包形式引入的
const middlewareAPI = {
  getState: store.getState,
  dispatch: (action, ...args) => dispatch(action, ...args)
}
// 對 middleware 注入 middlewareAPI
// 此時在 middleware 中調用 dispatch 會拋出異常
const chain = middlewares.map(middleware => middleware(middlewareAPI));

然後下面這段代碼其實做了兩件事,一方面將 middleware 組合為一個函數,注入默認 dispatch 函數,另一方面將新的 dispatch 初始的空函數替換為正常可執行的函數。同時由於 middlewareAPI 的 dispatch 是以閉包形式引入的,當 dispatch 更新之後,閉包中的值也相應更新:

// 將 dispatch 替換為正常的 dispatch 方法
// 注意閉包中的值也會相應更新,middleware 可以訪問到更新後的方法
dispatch = compose(...chain)(store.dispatch);

也就是説,createStore 生成的實例暴露的 dispatch 和 middleware 獲取的都是修飾後的 dispatch ,並且應該是長這樣:

function(action) {
  // 注意這裏存在閉包
  // 可以獲取到中間件初始化傳入的 dispatch、getState 和 next
  // 如果你打斷點,可以在 scope 中看到閉包的變量
  // 同時注意這裏的 dispatch 就是這個函數本身
  console.info('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
}

4. 處理異步 Action

由於 reducer 需要嚴格控制為純函數,因此不能在裏面進行異步操作,也不能進行網絡請求。有些同學可能會説,雖然 reducer 裏面不能放異步代碼,但是可以把 dispatch 函數放在異步回調中調用呀:

setTimeout(() => {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

在 React 組件中通常用 connect 把 dispatch 映射到組件的props 中,類似 Vuex 中的 mapAction 用法。

確實可以!Redux 作者 Dan Abramov 在 Stackoverflow 上面有一個非常好的回答,其中就贊同了這種用法:

https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559

本人將 Dan Abramov 的核心觀點總結如下。

  • Redux 確實提供了一些處理異步 Action 的替代方法,但應該只在當你意識到你編寫了大量模板代碼的時候再去使用。否則就用最簡單的方案(如無必要,勿增實體);
  • 當多個組件需要用到同一個 action.type 時,為避免 action.type 拼寫錯誤,需要抽離公共的 actionCreator,例如:
  // actionCreator.js
  export function showNotification(text) {
    return { type: 'SHOW_NOTIFICATION', text }
  }
  export function hideNotification() {
    return { type: 'HIDE_NOTIFICATION' }
  }

  // component.js
  import { showNotification, hideNotification } from '../actionCreator'

  this.props.dispatch(showNotification('You just logged in.'))
  setTimeout(() => {
    this.props.dispatch(hideNotification())
  }, 5000)
  • 上面的邏輯在簡單場景下完全可行,但是隨着業務複雜度增加會出現幾個問題:
  1. 通常狀態更新有好幾個步驟,而且存在邏輯上的先後順序,例如通知的展示和隱藏,導致模板代碼較多;
  2. 提交的 action 沒有狀態,如出現競爭條件可能導致狀態更新出 bug ;
  • 出於上面的問題,需要抽離異步的 actionCreator ,把涉及狀態更新的操作封裝進去,便於複用,同時為每次 dispatch 生成唯一 id :
  // actions.js
  function showNotification(id, text) {
    return { type: 'SHOW_NOTIFICATION', id, text }
  }
  function hideNotification(id) {
    return { type: 'HIDE_NOTIFICATION', id }
  }

  let nextNotificationId = 0
  export function showNotificationWithTimeout(dispatch, text) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }

然後在頁面組件中這樣使用,解決了模板代碼和狀態更新衝突問題:

  // component.js
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

  // otherComponent.js
  showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')
  • 細心的同學應該注意到,這邊傳遞了 dispatch 。這是因為,正常來説只有組件中能訪問到 dispatch ,為了能讓外部封裝的函數也能訪問,我們需要將 dispatch 作為參數傳過去;
  • 這時有些同學會提出質疑,如果將 store 作為全局單例,不就可以直接訪問了:
  // store.js
  export default createStore(reducer)

  // actions.js
  import store from './store'

  // ...

  let nextNotificationId = 0
  export function showNotificationWithTimeout(text) {
    const id = nextNotificationId++
    store.dispatch(showNotification(id, text))

    setTimeout(() => {
      store.dispatch(hideNotification(id))
    }, 5000)
  }

  // component.js
  showNotificationWithTimeout('You just logged in.')

  // otherComponent.js
  showNotificationWithTimeout('You just logged out.')
  • 上面這樣從操作上來説確實可行,但是 Redux 團隊並不贊同單例的寫法。他們的理由是,如果 store 變為單例,會導致服務端渲染的實現變得困難,同時測試也不方便,如要改用 mock store 需要修改所有 import ;
  • 基於上面的原因,Redux 團隊還是建議使用函數參數將 dispatch 傳遞出去,儘管這樣很麻煩。那麼有沒有一種解決方案呢?有的,使用 Redux-thunk 就解決了這個問題;
  • 實際上,Redux-thunk 的作用是教 Redux 識別函數類型的特殊 Action ;
  • 中間件啟用後,當 dispatch 的 Action 為函數類型,Redux-thunk 就會給這個函數傳入 dispatch 作為參數,需要注意最終 reducer 拿到的仍然是普通 JavaScript 對象作為 Action :
  // actions.js

  function showNotification(id, text) {
    return { type: 'SHOW_NOTIFICATION', id, text }
  }
  function hideNotification(id) {
    return { type: 'HIDE_NOTIFICATION', id }
  }

  let nextNotificationId = 0
  export function showNotificationWithTimeout(text) {
    return function (dispatch) {
      const id = nextNotificationId++
      dispatch(showNotification(id, text))

      setTimeout(() => {
        dispatch(hideNotification(id))
      }, 5000)
    }
  }

在組件中使用如下:

  // component.js
  this.props.dispatch(showNotificationWithTimeout('You just logged in.'))

好了,Dan Abramov 的觀點就總結到這裏。

看到這裏大家應該清楚 Redux-thunk 的作用了,Redux-thunk 本身並沒有提供異步解決方案,實現異步就是使用最簡單的方法,把 dispatch 函數放在異步回調中。很多時候我們會封裝異步的 actionCreator ,在異步操作中每次都需要把 dispatch 傳遞出來很麻煩,Redux-thunk 對 dispatch 函數進行高階封裝,允許接受函數類型的 Action ,同時給這個 Action 傳入 dispatch 和 getState 作為參數,這樣就不用每次手動傳遞。

在看源碼之前,大家可以結合 applyMiddleware 源碼,想一下 Redux-thunk 內部實現。

其實 Redux-thunk 實現原理非常簡單,代碼如下:

// src/index.ts:15
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) =>
    next =>
      action => {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument)
        }

        return next(action)
      }
}

在 Redux-thunk 內部,首先會調用 createThunkMiddleware 方法得到一個高階函數然後向外導出。這個函數就是我們之前分析的中間件結構:

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

      return next(action)
    }

首先在初始化階段,applyMiddleware 會為 thunk 先後注入 middlewareAPI (對應 dispatch 和 getState 形參)和 store.dispatch (即原本的 dispatch 方法,對應 next 形參)。

在初始化完成之後,store 實例的 dispatch 會被替換為一個經過修飾的 dispatch 方法(middlewareAPI 中的 dispatch 由於是閉包引用,也會被替換),用 dispatch.toString() 打印可以輸出如下內容:

// 注意這裏可以訪問到閉包中的 dispatch、getState 和 next
// 初始化完成後的 dispatch 實際上就是下面這個函數本身
action => {
  if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument)
  }

  return next(action)
}

接下來的事情就很簡單了,當我們提交一個函數類型的 Action :

// actions.js
const setUserInfo = data => ({
  type: "SET_USER_INFO",
  payload: data
})

export const getUserInfoAction = userId => {
  return dispatch => {
    getUserInfo(userId)
      .then(res => {
        dispatch(setUserInfo(res));
      })
  }
}

// component.js
import { getUserInfoAction } from "./actionCreator";

this.props.dispatch(getUserInfoAction("666"));

當提交的 action 為函數類型的時候,就調用這個函數,然後傳入 dispatch 、getState 、extraArgument 參數:

if (typeof action === 'function') {
  return action(dispatch, getState, extraArgument)
}

(從這裏可以看出,除了 dispatch 之外,在函數類型的 Action 內部還可以訪問 getState 和 extraArgument)

當異步操作完成,調用 Redux-thunk 傳遞的 dispatch 方法提交對象類型 Action 時,還是進入這個被修飾的 dispatch 方法,只不過在判斷類型的時候,進入了另一個分支:

return next(action);

這裏的 next 就是 Redux 原本的 dispatch 方法,會將對象類型的 Action 提交給 reducer 方法,最終執行狀態更新。

5. 總結

Redux 是一種非常經典的狀態管理解決方案。它遵循函數式編程的原則,狀態只讀且不可變,只有通過純函數才能更新狀態。

但是 Redux 同樣也存在着不少問題。首先,對於新手來説,上手成本較高,使用之前需要先了解函數式編程的概念和設計思想。其次,Redux 在實際開發中非常繁瑣,即使實現一個很簡單的功能,可能也需要同時修改 4-5 個文件,降低了開發效率。作為對比,Vuex 的上手成本非常低,對於新手非常友好,使用也非常簡單,既不需要異步中間件,也不需要額外的 UI binding ,在 Redux 中通過插件提供的功能,全部內置開箱即用。

對此,Redux 官方提供了一個封裝方案 Redux Toolkit,社區也提供了很多封裝方案,例如 Dva 、Rematch 等等,旨在簡化 Redux 的使用,API 的封裝上很多地方就是參考了 Vuex 。甚至還出現了酷似 Vue 響應式、使用可變數據(Mutable)的 Mobx 狀態管理方案。此外,React 官方團隊也在近期推出了 Recoil 狀態管理庫。

參考

https://redux.js.org

https://github.com/reduxjs/redux

https://github.com/reduxjs/redux-thunk

https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559

coding優雅指南:函數式編程