作者從2016年開始接觸 React+Redux,通過閱讀Redux原始碼,瞭解了其實現原理。
Redux程式碼量不多,結構也很清晰,函數語言程式設計思想貫穿著整個Redux原始碼,如純函式,高階函式,Curry,Compose。
本文首先會介紹函數語言程式設計的思想,再逐步介紹Redux中介軟體的實現。
看完本文,希望可以幫助你瞭解中介軟體實現的原理。
1) 基本概念
Redux是可預測的狀態管理框架。它很好的解決多互動,多資料來源的訴求。
Redux設計理念有三個原則: 1. 單一資料來源 2. State只讀 3. 使用純函式變更state值。
基本概念 | 原則 | 解釋 |
---|---|---|
Store | (1) 單一資料來源 (2) State只讀 | Store可以看做是資料儲存的一個容器。在這個容器裡面,只會維護唯一的一個State Tree。
Store會給定4種基礎操作方法:dispatch(action), getState(), replaceReducer(nextReducer), subscribe(listener) 根據單一資料來源原則,所有資料會通過store.getState()方法呼叫獲取。 根據State只讀原則,資料變更會通過store,dispatch(action)方法。 |
Action | (3) 使用純函式變更state值 | Action可以理解為變更資料的資訊載體。type是變更資料的唯一標誌,payload是用來攜帶需要變更的資料。
格式為:const action = { type: 'xxx', payload: 'yyy' }; |
Reducer | (3) 使用純函式變更state值 | Reducer是個純函式。負責根據獲取action.type的內容,計算state數值。
reducer: prevState => action => newState。 |
正常的一個同步資料流為:view層觸發actionCreator,actionCreator通過store.dispatch(action)方法, 變更reducer。
但是面對多種多樣的業務場景,同步資料流方式顯然無法滿足。對於改變reducer的非同步資料操作,就需要用到中介軟體的概念。如圖所示。
2) 函數語言程式設計
函數語言程式設計貫穿著Redux的核心。這裡會簡單介紹幾個基本概念。如果你已經瞭解了函數語言程式設計的核心技術,例如 高階函式,compose, currying,遞迴,可以直接繞過這裡。
我簡單理解的函數語言程式設計思想是: 通過函式的拆解,抽象,組合的方式去程式設計。複雜問題可以拆解成小粒度函式,最終利用組合函式的呼叫達成目的。
2.1) 高階函式
Higher order functions can take functions as parameters and return functions as return values.
接受函式作為引數傳入,並能返回封裝後函式。
2.2) Compose
Composes functions from right to left.
組合函式,將函式串聯起來執行。就像domino一樣,推倒第一個函式,其他函式也跟著執行。
首先我們看一個簡單的例子。
// 實現公式: f(x) = (x + 100) * 2 - 100
const add = a => a + 100;
const multiple = m => m * 2;
const subtract = s => s - 100;
// 深度巢狀函式模式 deeply nested function,將所有函式串聯執行起來。
subtract(multiple(add(200)));
複製程式碼
上述例子執行結果為:500
compose 其實是通過reduce()方法,實現將所有函式的串聯。不直接使用深度巢狀函式模式,增強了程式碼可讀性。不要把它想的很難。
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)))
}
複製程式碼
compose(subtract, multiple, add)(200);複製程式碼
2.3) Currying
Currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument
翻譯過來是:把接受多個引數 的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數且返回結果的新函式的技術。
直接擼程式碼解釋
// 實現公式: f(x, y, z) = (x + 100) * y - z;
const fn = (x, y, z) => (x + 100) * y - z;
fn(200, 2, 100);
// Curring實現 使用一層層包裹的單參匿名函式,來實現多引數函式的方法
const fn = x => y => z => (x + 100) * y - z;
fn(200)(2)(100);複製程式碼
*Currying只允許接受單引數。
3) Redux applyMiddleware.js
Redux中reducer更關注的是資料邏輯轉化,所以Redux中介軟體是為了增強dispatch方法出現的。如我們上面圖,所描述的流程。中介軟體呼叫鏈,會在dispatch(action)方法之前呼叫。
所以Redux中介軟體實現核心目標是:改造dispatch方法。
redux對中介軟體的實現,程式碼是很精簡。整體都不超過20行。
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
}
}
}
複製程式碼
接下來,一步步的解析Redux在中介軟體實現的過程。
applyMiddleware.js 方法有三個主要的步驟,如下:
- 將所有的中介軟體組合在一起, 並保證最後一個執行的是dispatch(action)方法。
- 像Koa所有中介軟體對ctx的呼叫一樣。保證所有的中介軟體都能訪問到Store。
- 最後將含有中介軟體呼叫鏈的新dispatch方法,合併到Store中。
- redux對中介軟體的定義格式為:mid1 = store => next => action => { next(action) };
看到這裡,你可能有這麼幾個疑問?
- 如何將所有的middleware串聯執行在一起?並可以保證最後一個執行的是dispatch方法?
- 如何讓所有的中介軟體都可以訪問到Store?
- 因為新形成的dispatch方法,為含有中介軟體呼叫鏈的方法結合。中介軟體如果呼叫dispatch,豈不是會死迴圈在呼叫鏈中?
- 為什麼將中介軟體格式定義為 mid1 = store => next => action => { next(action) } ?
為了解決這4個疑問,下面將針對相應問題,逐步解析。
3.1) 中介軟體串聯
疑問:
- 如何將所有的middleware串聯執行在一起?並可以保證最後一個執行的是dispatch(action)方法?
解決思路:
- 深度巢狀函式 / compose組合函式方法,將所有的中介軟體串聯起來。
- 封裝最後一個函式作為dispatch(action)方法。
const middleware1 = action => action;
const middleware2 = action => action;
const final = action => store.dispatch(action);
/*
1. compose(...)將所有中介軟體串聯
2. 定義final作為最後執行dispatch的函式
*/
compose(final, middleware2, middleware1)(action)
複製程式碼
3.2) 中介軟體可訪問Store
疑問:
- 如何讓所有的中介軟體都可以訪問到Store?
可以參考我們對Koa2中介軟體的定義 const koaMiddleware = async (ctx, next) => { };
解決思路:
- 給每一個middleware傳遞Store, 保證每一箇中介軟體訪問到的都是一致的。
const middleware1 = (store, action) => action;
const middleware2 = (store, action) => action;
const final = (store, action) => store.dispatch(action);
複製程式碼
如果我們想使用compose方法,將所有中介軟體串聯起來,那就必須傳遞單一引數。
根據上面函數語言程式設計講到的currying方法,對每個中介軟體柯里化處理。
// 柯里化處理引數
const middleware1 = store => action => action;
const middleware2 = store => action => action;
const final = store => action => store.dispatch(action);
// 將store儲存在各個函式中 -> 迴圈執行處理。
const chain = [final, middleware2, middleware1].map(midItem => midItem(store));
compose(...chain)(action);
複製程式碼
通過迴圈處理,將store內容,傳遞給所有中介軟體。這裡就體現了currying的作用,延遲計算和引數複用。
3.3) 中介軟體呼叫新dispatch方法死迴圈
疑問:
- 因為新形成的dispatch方法,為含有中介軟體呼叫鏈的方法結合。中介軟體如果呼叫dispatch,豈不是會死迴圈在呼叫鏈中?
new_dispatch = compose(...chain)(store.dispatch);
new_store = { ...store, dispatch: new_dispatch };
複製程式碼
根據原始碼的解析,新和成new_dispatch是帶有中介軟體呼叫鏈的新函式,並不是原來使用的store.dispatch方法。
如果根據3.2) 例子使用的方式傳入store, const chain = [final, middleware2, middleware1].map(midItem => midItem(store));
此時儲存在各個中介軟體中的store.dispatch為已組合中介軟體dispatch方法,中介軟體如果呼叫dispatch方法,會發生死迴圈在呼叫鏈中。
根據上述文字的描述,右圖是死迴圈的說明。
解決思路:
- 給定所有中介軟體的dispatch方法為原生store.dispatch方法,不是新和成的dispatch方法。
// 這就是為什麼在給所有middleware,共享Store的時候,會重新定義一遍getState和dispatch方法。
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
複製程式碼
3.4) 保證中介軟體不斷裂
疑問:
- 為什麼將中介軟體格式定義為 mid1 = store => next => action => { next(action) } ?
上述例子有提到每次都會返回action給下一個中介軟體,例如 const middleware1 = store => action => action;
如何保證中介軟體不會因為沒有傳遞action而斷裂?
這裡必須說明的是:Koa中介軟體可以通過呼叫await next()方法,繼續執行下一個中介軟體,也可以中斷當前執行,比如 ctx.response.body = ‘xxxx’ (直接中斷下面中介軟體的執行)。
一般情況下,Redux不允許呼叫鏈中斷,因為我們最終需要改變state內容。(* 比如redux-thunk使用有意截斷的除外)。
解決思路:
- 如果可以保證,上一個中介軟體都有下一個中介軟體的註冊,類似Koa對下一個中介軟體呼叫方式next(),不就可以保證了中介軟體不會斷裂。
// 柯里化處理引數
const middleware1 = store => next => action => { log(1); next(action)};
const middleware2 = store => next => action => { log(2); next(action)};
// 中介軟體串聯
const chain = [middleware1, middleware2 ].map(midItem => midItem({
dispatch: (action) => store.dispatch(action)}));
// compose(...chain)會形成一個呼叫鏈, next指代下一個函式的註冊, 如果執行到了最後next就是原生的store.dispatch方法
dispatch = compose(...chain)(store.dispatch);
複製程式碼
4) 總結
Redux applyMiddleware.js機制的核心在於,函數語言程式設計的compose組合函式,需將所有的中介軟體串聯起來。
為了配合compose對單參函式的使用,對每個中介軟體採用currying的設計。同時,利用閉包原理做到每個中介軟體共享Store。
另外,Redux / React應用函數語言程式設計思想設計,其實是通過組合和抽象來減低軟體管理複雜度。
簡單寫了個學習例子 參考 https://github.com/Linjiayu6/learn-redux-code, 如果有幫助到你,點個贊 咩~
簡歷請投遞至郵箱linjiayu@meituan.com