從 Redux 原始碼談談函數語言程式設計

有道技術團隊發表於2022-04-13

摘要

在 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優雅指南:函數語言程式設計

相關文章