糟了!是要看源碼的感受!javascript
嘛,有點標題黨了,其實原問是applyMiddleware
的實現細節,不過研究了一下感受最終的處理目的仍是爲了應對異步數據流的場景,因此就安排上了。java
emm,我但願閱讀這篇文章的人都能有所收穫(帶哥能夠略過)。所以,會先從一些比較基礎的東西開始。git
啥是閉包,簡單點來說就是你在一個函數裏返回了一個函數,在返回的這個函數內,你具備訪問包裹它的函數做用域內的變量的能力。github
通常來講在咱們聲明的函數體內聲明變量,只會在函數被調用時在當前函數塊的做用域內存在。當函數執行完畢後會垃圾回收。但,若是咱們返回的函數中存在對那個變量的引用,那這個變量便不會在函數調用後被銷燬。也基於這一特性,延展出不少閉包的應用,如常見的防抖(debounce)、節流(throttle)函數,它們都是不斷對內部的一個定時器進行操做;又如一些遞歸的緩存結果優化,也是設置了一個內部對象去比對結果來跳過一些冗餘的遞歸場景。redux
// 一個比較常見的節流函數
function throttle(fn, wait) {
let timeStart = 0; // 不會被銷燬,返回的函數執行時具備訪問該變量的能力
return function (...args) {
let timeEnd = Date.now();
if (timeEnd - timeStart > wait) {
fn.apply(this, args);
timeStart = timeEnd;
}
}
}
複製代碼
啥是高階函數,其實跟上面的閉包的操做手段有點像,最終都會再返回一個函數。只不過它會根據你實際需求場景進行一些附加的操做來「加強」傳入的原始函數的功能。像React
中的一些HOC(高階組件)的應用其實也是同理,畢竟class
也不過是function
的語法糖。網上的應用場景也不少,這裏不贅述了。主要再提一嘴的是compose
函數,它能讓咱們在進行多層高階函數嵌套時,書寫代碼更爲清晰。如咱們有高階函數A、B、C ,要實現A(B(C(...args)))的效果,若是沒有compose
,就須要不斷地將返回結果賦值,調用。而使用compose
,只須要一次賦值let HOC = compose(A, B, C);
,而後調用HOC(...args)
便可。數組
瞅瞅compose
源碼,比較簡單,無傳參時,返回一個按傳入返回的函數;一個入參時,直接返回第一個入參函數;多個則用數組的reduce
方法進行迭代,最終返回組合後的結果:promise
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)))
}
複製代碼
這個工具方法比較簡單,就是來判斷入參是不是由Object
直接構造的且中間沒有修改繼承關係:緩存
let isObjectLike = obj => {
return typeof obj === 'object' && obj !== null;
}
let isPlainObject = obj => {
if (!isObjectLike(obj) || !Object.prototype.toString.call(obj) === '[object Object]') {
return false;
}
if (Object.getPrototypeOf(obj) === null) return true; // Object.prototype 自己
let proto = obj; // 拷貝指針,移動指針直至原型鏈頂端
while (Object.getPrototypeOf(proto) !== null) { // 是否純粹,若是中間發生繼承,則__proto__的最終跨越將不會是1層
proto = Object.getPrototypeOf(proto);
}
return Object.getPrototypeOf(obj) === proto;
}
複製代碼
在聊applyMiddleware
前,咱們有必要先分析一波createStore
內作了什麼操做,由於他們倆實際上是一個相互成就依賴注入的關係。網絡
function createStore(reducer, preloadedState, enhancer) {
// 略
// return {
// dispatch, // 去改變state的方法 派發 action
// subscribe, // 監聽state變化 而後觸發回調
// getState, // 訪問這個createStore的內部變量currentState 也就是全局那個大state
// replaceReducer, // 傳入新的reducer 來替換以前內部的reducer 可能場景是在代碼拆分、redux的熱加載?
// [$$observable]: observable // symbol屬性 返回一個observable方法
// }
}
複製代碼
從源碼中的聲明能夠看到,createStore
接收三個參數,第一個是reducer
,這個在項目中一般咱們會用combineReducers
組合成一個大的reducer
傳入。這個combineReducers
使用頻率仍是很高的,先簡要看看:session
function combineReducers(reducers) {
// 略去一些
return function combination(state = {}, action) {
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
/** * 好比傳入的子reducer函數是 * function childA(state = 0, action) { * switch (action.type) { * case 'INCREMENT': * return state + 1 * case 'DECREMENT': * return state - 1 * default: * return state * } * } * 那初始狀況下的store.getState() // { childA: 0 } */
複製代碼
首先combineReducers
接收一個對象,裏面的key
是每個小reducer
文件或函數導出的namespace
,value
則是與其對應的reducer
函數實體。而後它會將這些不一樣的reducer
函數合併到一個reducer
函數中。它會調用每個合併的子reducer
,而且會將他們的結果放入一個state
中,最後返回一個閉包使咱們能夠像操做以前的子reducer
同樣操做這個大reducer
。
preloadedState
就是咱們傳入的初始state
,固然源碼中的註釋裏描述還能夠向服務端渲染中的應用注入該值or恢復歷史用戶的session記錄,不過沒實踐過,就不延展了...
最後的入參enhancer
比較關鍵,字面理解就是用來加強功能的,先看看部分源碼:
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
複製代碼
在這裏咱們發現其實createStore
能夠只接收2個參數,當第二個參數爲函數時,會自動初始化state
爲undefined
,因此看到一些createStore
只傳了2個參數不要以爲奇怪。
而後往下看對enhancer
函數的調用,這寫法一看就是個高階函數,接收一個方法createStore
,而後返回一個函數。如今咱們能夠把applyMiddleware
擡上來了,這個API也是redux
自己提供的惟一用於store enhancer
的動做。
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) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
複製代碼
咱們注意到applyMiddleware
做爲enhancer
又把createStore
這個函數做爲參數傳入並在內部返回函數中調用了,這其實也是依賴注入的理念。而後咱們發現內部其實將applyMiddleware
的入參傳入的中間件都執行了一次,傳參爲getState
和dispatch
。這裏可能初見者比較懵逼,咱們先把早期處理異步action
的中間件redux-thunk
的源碼翻出來看一眼:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
複製代碼
經過代碼,咱們能夠得知通常middleWare
的內部構造都聽從一個({ getState, dispatch }) => next => action => {...}
的範式,而且導出的時候已經被調用了一次,即返回了一個須要接收getState
和dispatch
的函數。
Get到這一點之後,咱們再日後看。經過compose
將中間件高階組合並「加強」傳入原store.dispatch
的功能,最後再在返回值內解構覆蓋原始store
的dispatch
。
因此這個時候,若是我再問applyMiddleware
作了什麼?應該你們都知道答案了吧,就是加強了原始createStore
返回的dispatch
的功能。
那再回到那個如何處理redux
中的異步數據流問題?其實核心解決方案就是引入中間件,而中間件最終達成的目的就是加強咱們的原始dispatch
方法。仍是以上面的redux-thunk
的middleware
來講,它傳入的dispatch
就是它內部的next
,換言之,調用時,若是action
是個普通對象,那就跟往常dispatch
沒啥差異,正常走reducer
更新狀態;但若是是個函數,那咱們就要讓action
本身玩了本身去處理內部的異步邏輯了,好比什麼網絡請求,當Promiseresolved
了dispatch
一個成功action
,rejected
了dispatch
一個失敗action
。
在開發環境中,爲了追溯以及定位一些數據流向,咱們會引入redux-devtools-extension
,這個模塊有2種使用方式,一種是沉浸式,即在開發環境安裝對應依賴,而後經過2次加強咱們的applyMiddleWare
返回一個傳入createStore
中的enhancer
,好比下面這樣的:
import { composeWithDevTools } from 'redux-devtools-extension';
const composeEnhancers = composeWithDevTools(options);
const store = createStore(reducer, /* preloadedState, */
composeEnhancers(
// 一個 enhancer入口 套中套
applyMiddleware(...middleware),
// other store enhancers if any
));
複製代碼
又或者是插件擴展式的:
const composeEnhancers = typeof window === 'object' && typeof window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ !== 'undefined' ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
// 剩下操做跟上面同樣
複製代碼
更細節定製見官方。
如今處理異步邏輯的中間件已經很多了,可是原理都是差很少的,只不過說從之前的傳function
,到Promise
、Generator
控制之類的;像前文例子的redux-thunk
是比較早的異步中間件了,以後社區中有了更多的方案提供:如redux-promise
、redux-saga
、dvajs
、redux-observable
等等。咱們仍是須要根據實際團隊和業務場景使用最適合咱們的方案來組織代碼編寫。
1. store
自己的dispatch
派發action
更新數據這個動做是同步的。
2. 所謂異步action
,是經過引入中間件的方案加強dispatch
後實現的。具體是applyMiddleware
返回dispatch
覆蓋原始store
的dispatch
,當action
爲函數時,進行定製的異步場景dispatch
派發。
3. 爲什麼會採起這種中間件加強的模式,我我的看來一是集中在一個位置方便統一控制處理,另外一個則是減小代碼中的冗餘判斷模板。