從 Redux 源碼談談函數式編程

語言: CN / TW / HK

摘要

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

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

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

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

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

```js let store = { foo: 1, bar: 2 };

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

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

狀態更新通過一個純函數(Reducer)完成。純函數(Pure function)的特點是: - 輸出僅與輸入有關; - 引用透明,不依賴外部變量; - 不產生副作用;

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

1. 需要了解的幾個概念

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

Action

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

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

Reducers

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

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

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

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

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

一個典型的 reducer 函數如下:

```js 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() 方法獲取更新後的狀態:

```js 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 函數如下:

```js // 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 :

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

const store = createStore(reducer); ```

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

```js // 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: "測試內容" }); ```

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

js { 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的邏輯被省略了,順便把源碼中的類型註解也都去掉了,方便閲讀:

```javascript // 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 實現如下(發佈訂閲的功能不寫了):

```javascript 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 中的一個難點,雖然代碼不多,但是裏面用到了大量函數式編程技巧,本人也是經過大量源碼調試才徹底搞懂。

首先要能看懂這種寫法:

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

上面的寫法相當於:

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

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

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

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

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

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

js export default function applyMiddleware<Ext, S = any>( ...middlewares: Middleware<any, S, any>[] ): StoreEnhancer<{ dispatch: Ext }> 個人觀點,這樣做的好處就是提供了造輪子的空間

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

```js 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 :

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

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

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

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

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

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

```javascript // 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 的源碼,為便於閲讀,把源碼中的類型註解都去掉了:

```js // 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
}

} } ```

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

js 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 函數,是函數式編程範式中經常用到的一種處理,創建一個從右到左的數據流,右邊函數執行的結果作為參數傳入左邊,最終返回一個以上述數據流執行的函數:

js // 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)) ) }

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

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

js 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 函數本身其實就是以閉包形式引入的,這個閉包可能沒多少人能看得出來:

js // 定義新的 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 更新之後,閉包中的值也相應更新:

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

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

js 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 函數放在異步回調中調用呀:

js 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,例如:

```js // 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 :

```js // 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)

} ```

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

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

// otherComponent.js showNotificationWithTimeout(this.props.dispatch, 'You just logged out.') ```

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

```js // 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 : ```js // 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)
}

} ```

在組件中使用如下:

js // 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 實現原理非常簡單,代碼如下:

```js // 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 方法得到一個高階函數然後向外導出。這個函數就是我們之前分析的中間件結構:

```js ({ 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() 打印可以輸出如下內容:

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

return next(action) } ```

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

```js // 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 參數:

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

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

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

js 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優雅指南:函數式編程