前提:公司兼容了兩種技術棧
Vue
和React
,Vue
研究的比較多一些,反觀React
還停留在查官方文檔階段
,最近恰好維護了一個React項目
,項目中用到Redux
,藉此從新複習Redux
javascript
一句話介紹
Redux
:Redux是一個可預測化的JavaScript狀態管理容器。
html
理解
Redux
離不開這三大原則java
整個應用的 state
被儲存在一棵 object tree
中,而且這個 object tree
只存在於惟一一個 store
中。git
惟一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通對象。
github
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
複製代碼
爲了描述 action 如何改變 state tree ,你須要編寫 reducers
。action是描述修改操做
,而真正去操做修改state是reducers
web
function reducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
複製代碼
經過上面👆的分析咱們獲得三個關鍵的信息點:state
,action
,reducers
redux
在複習工做流以前咱們先來搞清楚幾個概念,如下的概念解釋都出自於詞彙表 , 爲了方便閱讀,我就
摘抄
過來了promise
State
(也稱爲 state tree) 是一個寬泛的概念,可是在 Redux API 中,一般是指一個惟一的 state 值,由 store 管理且由 getState() 方法得到。它表示了 Redux 應用的所有狀態,一般爲一個多層嵌套的對象。網絡
頂層 state 或爲一個對象,或像 Map 那樣的鍵-值集合,也能夠是任意的數據類型。然而你應儘量確保 state 能夠被序列化,並且不要把什麼數據都放進去,致使沒法輕鬆地把 state 轉換成 JSON。app
Action 是一個普通對象,用來表示即將改變 state 的意圖。它是將數據放入 store 的惟一途徑。不管是從 UI 事件、網絡回調,仍是其餘諸如 WebSocket 之類的數據源所得到的數據,最終都會被 dispatch 成 action。
action 必須擁有一個 type 域
,它指明瞭須要被執行的 action type。Type 能夠被定義爲常量,而後從其餘 module 導入。比起用 Symbols 表示 type,使用 String 是更好的方法,由於 string 能夠被序列化。
Reducer (也稱爲 reducing function) 函數接受兩個參數:以前累積運算的結果和當前被累積的值,返回的是一個新的累積結果。該函數把一個集合歸併成一個單值。
在 Redux 中,累計運算的結果是 state 對象,而被累積的值是 action。Reducer 由上次累積的結果 state 與當前被累積的 action 計算獲得一個新 state。這些 Reducer 必須是純函數,並且當輸入相同時返回的結果也會相同。它們不該該產生任何反作用。正因如此,才使得諸如熱重載和時間旅行這些很棒的功能成爲可能。
dispatching function
(或簡言之 dispatch function) 是一個接收 action 或者異步 action的函數,該函數要麼往 store 分發一個或多個 action
,要麼不分發任何 action。
Action Creator
很簡單,就是一個建立 action 的函數。不要混淆 action 和 action creator 這兩個概念。Action 是一個信息的負載,而 action creator 是一個建立 action 的工廠
。
異步 action 是一個發給 dispatching 函數的值
,可是這個值還不能被 reducer 消費
。在發往 base dispatch() function 以前,middleware 會把異步 action 轉換成一個或一組 action。異步 action 能夠有多種 type
,這取決於你所使用的 middleware
。它一般是 Promise 或者 thunk 之類的異步原生數據類型
,雖然不會當即把數據傳遞給 reducer,可是一旦操做完成就會觸發 action 的分發事件
。
Middleware 是一個組合 dispatch function 的高階函數
,返回一個新的 dispatch function
,一般將異步 actions 轉換成 action。
❗️❗️這也是接下來咱們要重點分析
最後放出一張記憶腦圖
瞭解了
三大原則
以及概念
之後,來看看Redux的工做流吧
對比下圖就能輕鬆理解了
從圖中咱們知道Redux是單向數據流
,那麼根據上面所學的知識咱們來設計下咱們的Redux目錄結構
吧
如上圖,大部分公司的Redux目錄結構應該相似
這樣,咱們須要actionCreators
文件來建立咱們的action
,action對象
必須擁有一個 type 域
,而後reducer根據不一樣的type觸發對應的操做
,因此建立actionTypes
文件,接下來就是處理reducer了
來修改咱們的state
,因此建立reducer
文件📃,因此明白上述概念,有助於咱們對目錄結構的理解
,而不是傻乎乎的跟着別人的目錄結構照貓畫虎的建立
,起碼要明白爲何這樣劃分
再看中間件原理時,咱們來實現下
compose函數
,理解它對於理解咱們中間件原理有很大幫助
dispatch=fn1(fn2(fn3))
dispatch=compose(fn1,fn2,fn3)
複製代碼
咱們期待有一個聚合的方法compose,能夠這樣使用,參數從右至左,將第一個參數fn3做爲第二個參數fn2的參數,並將運行結果做爲第三個參數fn1的參數,依次遞推,最終返回一個新的函數
,這個新函數在基礎函數f3
的基礎上,獲得了全部的高階函數的能力
,🤔思考:假設這個f3參數
是換成是dispatch函數
呢?,不着急,咱們接着往下分析compose函數的實現
// compose聚合函數的順序是從右到左 from right to left
const compose = function (...funcs) {
return funcs.reduce((a, b) => {
return (...args) => {
return a(b(...args))
}
})
}
複製代碼
這些串聯函數不優雅。ES6 的箭頭函數簡寫
,從而看起來更舒服一些
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製代碼
因此compose(fn1, fn2, fn3) (...args) 至關於 fn1(fn2(fn3(...args)))
講清楚了上述👆基本內容後,到了本問的關鍵點了,那就是Redux中間件是用來幹嗎的,原理是什麼❓
前面分析Redux工做流
,Action
發出之後,Reducer
當即算出 State,是個同步流程
,那麼想一想如何支持異步操做
,不僅僅支持異步操做
,還要支持錯誤處理、日誌監控
,那麼是在Redux工做流
哪一個環節進行攔截操做呢❓,答案是dispatch過程
,在分發action
進行攔截處理
在Redux中,與中間件的實現相關聯的方法是applyMiddleware
,因此咱們來分析下這個方法吧(這裏筆者提供一份調試中間件代碼
,點擊進入倉庫
// 調用applyMiddleware
applyMiddleware(thunk, logger)
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => {
return dispatch(...args)
}
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
複製代碼
能夠看到dispatch = compose(...chain)(store.dispatch)
這行關鍵代碼,Redux
爲了支持中間件
,內部重寫了原來的dispatch方法
,同時最後傳入原來的store.dispatch
方法,也就是將原來的disptach方法
也當作中間件處理了
分析到這裏,咱們能夠知道傳入的中間件
被compose函數聚合
後改寫dispatch方法,因此咱們也能夠理解成中間件是對dispatch方法的加強
,好比說:加強dispatch函數對action是函數、Promise的處理
舉個例子🌰
倉庫內修改爲這段代碼
function logger(store) {
return function wrapDispatchToAddLogging(next) {
return function dispatchAndLog(action) {
let result = next(action)
return result
}
}
}
function thunk({ dispatch, getState }) {
return function wrapDispatchToThunk(next) {
return function dispatchThunk(action) {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
}
}
applyMiddleware(thunk, logger) // 至關於 wrapDispatchToThunk(wrapDispatchToAddLogging(dispatch))
複製代碼
打印dispatch
方法
dispatchThunk(action) {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
複製代碼
轉換next
// 轉換next
dispatchThunk(action) {
if (typeof action === 'function') {
return action(dispatch, getState);
}
// next(action)
// 這裏的next由來是執行logger方法返回了dispatchAndLog函數
return (function dispatchAndLog(action) {
let result = next(action)
return result
})(action)
}
複製代碼
繼續轉換next
看到
dispatchAndLog
函數裏還有個next
,咱們繼續轉換
// 轉換next
dispatchThunk(action) {
if (typeof action === 'function') {
return action(dispatch, getState);
}
// next(action)
// 這裏的next由來是執行logger方法返回了dispatchAndLog函數
return (function dispatchAndLog(action) {
// let result = next(action)
// 這裏的dispatch是原來的dispacth
let result=(function(action){dispatch(action)})(action)
return result
})(action)
}
複製代碼
看到這裏咱們已經知道,Redux實現中間件的原理核心是加強原來dispatch函數的能力
,然函數擁有某種能力
天然而然想到高階函數
的處理方式,compose 方法將新的 middlewares 和 store.dispatch 結合起來,生成一個新的 dispatch 方法,另外經過改寫後的dispatch
方法,能夠肯定Redux中間件
也是基於洋蔥模型
執行順序聽從
洋蔥模型
applyMiddleware(
logger,
thunk
)
複製代碼
工做中咱們明白了Redux工做流
的狀況下,其實幹擾最多的可能就是Redux中間件了
,經常使用的有redux-thunk、redux-soga、redux-promise
等等,因此掌握中間件原理仍是很重要的,那麼如何去寫一箇中間件❓,咱們經過繼續分析applyMiddleware
方法
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => {
return dispatch(...args)
}
}
// 這裏執行了一層中間件接收了{store.getState,dispatch}參數
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// compose(...chain)(store.dispatch) 至關於fn1(fn2(fn3(store.dispatch)))
// 又執行了一層中間件 這一層接收next參數 也就是下一個中間件參數
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
複製代碼
經過上面👆的分析可知,一個Redux中間件
的基本形式(結構)以下
// 中間件邏輯代碼須要通過三次柯里化
store => next => action => {
// 中間件邏輯代碼
}
複製代碼
dispacth函數
,讓其能解析action爲函數形式
,從而讓Redux支持異步操做dispacth函數
,讓其能解析action爲Promise對象形式
,從而讓Redux支持異步操做根據這個中間件結構
咱們來分析redux-thunk
中間件的源碼
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
// 若是是函數 thunk來處理
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
// 其它處理不了,交給下一個中間件處理
return next(action);
};
}
const thunk = createThunkMiddleware();
複製代碼
嗯嗯…,redux-thunk
就這麼幾行源代碼就實現了支持Redux異步操做
用法
const testRes = () => {
return new Promise((res, rej) => {
res({
type: 'TEST_RESOLVE'
})
});
}
store.dispatch(testRes());
複製代碼
// redux-promise簡易版源碼
const vanillaPromise = store => next => action => {
// 判斷不是Promise對象,交給下箇中間件處理
if (typeof action.then !== 'function') {
return next(action)
}
// action爲Promise對象,promise中間件能作處理
// 最後異步執行完觸發執行store.dispatch ---> (...args) => dispatch(...args)
return Promise.resolve(action).then(store.dispatch)
}
複製代碼
一句話Redux
用起來太笨重了
,去年實習期開始寫React
項目時候,跟着別人的風格來寫而已,直到維護同事的React
項目,才猛然意識到項目中糟糕Redux
寫法,濫用Redux
,致使了這個項目是災難級別的
後面意識到能不能二次封裝下Redux
,簡化寫法
好比:
import { createModel } from "../../../model.js";
const model = {
namespace: 'counter',
state: {
count: 10
},
reducer: {
add(state: any, action: any) { // counter/add
state.count += 1
},
minus(state: any, action: any) {
state.count--
},
}
}
export default createModel(model)
複製代碼
enennene….就有了以上對話,也感謝大佬的幫助,最後選用了remacth方案,這個庫也挺好友好,兼容了老的寫法
,具體能夠細看這個它的文檔,這裏就不作分析了