做者從2016年開始接觸 React+Redux,經過閱讀Redux源碼,瞭解了其實現原理。git
Redux代碼量很少,結構也很清晰,函數式編程思想貫穿着整個Redux源碼,如純函數,高階函數,Curry,Compose。github
本文首先會介紹函數式編程的思想,再逐步介紹Redux中間件的實現。編程
看完本文,但願能夠幫助你瞭解中間件實現的原理。redux
Redux是可預測的狀態管理框架。它很好的解決多交互,多數據源的訴求。閉包
Redux設計理念有三個原則: 1. 單一數據源 2. State只讀 3. 使用純函數變動state值。app
基本概念 | 原則 | 解釋 |
---|---|---|
Store | (1) 單一數據源 (2) State只讀 | Store能夠看作是數據存儲的一個容器。在這個容器裏面,只會維護惟一的一個State Tree。 Store會給定4種基礎操做方法:dispatch(action), getState(), replaceReducer(nextReducer), subscribe(listener)框架 根據單一數據源原則,全部數據會經過store.getState()方法調用獲取。dom 根據State只讀原則,數據變動會經過store,dispatch(action)方法。koa |
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的異步數據操做,就須要用到中間件的概念。如圖所示。
函數式編程貫穿着Redux的核心。這裏會簡單介紹幾個基本概念。若是你已經瞭解了函數式編程的核心技術,例如 高階函數,compose, currying,遞歸,能夠直接繞過這裏。
我簡單理解的函數式編程思想是: 經過函數的拆解,抽象,組合的方式去編程。複雜問題能夠拆解成小粒度函數,最終利用組合函數的調用達成目的。
Higher order functions can take functions as parameters and return functions as return values.
接受函數做爲參數傳入,並能返回封裝後函數。
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);複製代碼
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只容許接受單參數。
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 方法有三個主要的步驟,以下:
看到這裏,你可能有這麼幾個疑問?
爲了解決這4個疑問,下面將針對相應問題,逐步解析。
疑問:
解決思路:
const middleware1 = action => action;
const middleware2 = action => action;
const final = action => store.dispatch(action);
/*
1. compose(...)將全部中間件串聯
2. 定義final做爲最後執行dispatch的函數
*/
compose(final, middleware2, middleware1)(action)
複製代碼
疑問:
能夠參考咱們對Koa2中間件的定義 const koaMiddleware = async (ctx, next) => { };
解決思路:
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的做用,延遲計算和參數複用。
疑問:
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方法,會發生死循環在調用鏈中。
根據上述文字的描述,右圖是死循環的說明。
解決思路:
// 這就是爲何在給全部middleware,共享Store的時候,會從新定義一遍getState和dispatch方法。
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
複製代碼
疑問:
上述例子有提到每次都會返回action給下一個中間件,例如 const middleware1 = store => action => action;
如何保證中間件不會由於沒有傳遞action而斷裂?
這裏必須說明的是:Koa中間件能夠經過調用await next()方法,繼續執行下一個中間件,也能夠中斷當前執行,好比 ctx.response.body = ‘xxxx’ (直接中斷下面中間件的執行)。
通常狀況下,Redux不容許調用鏈中斷,由於咱們最終須要改變state內容。(* 好比redux-thunk使用有意截斷的除外)。
解決思路:
// 柯里化處理參數
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);
複製代碼
Redux applyMiddleware.js機制的核心在於,函數式編程的compose組合函數,需將全部的中間件串聯起來。
爲了配合compose對單參函數的使用,對每一箇中間件採用currying的設計。同時,利用閉包原理作到每一箇中間件共享Store。
另外,Redux / React應用函數式編程思想設計,實際上是經過組合和抽象來減低軟件管理複雜度。
簡單寫了個學習例子 參考 https://github.com/Linjiayu6/learn-redux-code, 若是有幫助到你,點個贊 咩~
簡歷請投遞至郵箱linjiayu@meituan.com