面試官:講一下redux怎麼處理異步數據流的?

糟了!是要看源碼的感受!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;
		}
	}
}
複製代碼

HOC(高階函數or組件)與Compose(組合)

  啥是高階函數,其實跟上面的閉包的操做手段有點像,最終都會再返回一個函數。只不過它會根據你實際需求場景進行一些附加的操做來「加強」傳入的原始函數的功能。像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)))
}
複製代碼

isPlainObject

  這個工具方法比較簡單,就是來判斷入參是不是由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內作了什麼操做,由於他們倆實際上是一個相互成就依賴注入的關係。網絡

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

combineReducers

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文件或函數導出的namespacevalue則是與其對應的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個參數,當第二個參數爲函數時,會自動初始化stateundefined,因此看到一些createStore只傳了2個參數不要以爲奇怪。

  而後往下看對enhancer函數的調用,這寫法一看就是個高階函數,接收一個方法createStore,而後返回一個函數。如今咱們能夠把applyMiddleware擡上來了,這個API也是redux自己提供的惟一用於store enhancer的動做。

applyMiddleware

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的入參傳入的中間件都執行了一次,傳參爲getStatedispatch。這裏可能初見者比較懵逼,咱們先把早期處理異步action的中間件redux-thunk的源碼翻出來看一眼:

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 => {...}的範式,而且導出的時候已經被調用了一次,即返回了一個須要接收getStatedispatch的函數。

  Get到這一點之後,咱們再日後看。經過compose將中間件高階組合並「加強」傳入原store.dispatch的功能,最後再在返回值內解構覆蓋原始storedispatch

  因此這個時候,若是我再問applyMiddleware作了什麼?應該你們都知道答案了吧,就是加強了原始createStore返回的dispatch的功能。

  那再回到那個如何處理redux中的異步數據流問題?其實核心解決方案就是引入中間件,而中間件最終達成的目的就是加強咱們的原始dispatch方法。仍是以上面的redux-thunkmiddleware來講,它傳入的dispatch就是它內部的next,換言之,調用時,若是action是個普通對象,那就跟往常dispatch沒啥差異,正常走reducer更新狀態;但若是是個函數,那咱們就要讓action本身玩了本身去處理內部的異步邏輯了,好比什麼網絡請求,當Promiseresolveddispatch一個成功actionrejecteddispatch一個失敗action

redux-devtools-extension

  在開發環境中,爲了追溯以及定位一些數據流向,咱們會引入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,到PromiseGenerator控制之類的;像前文例子的redux-thunk是比較早的異步中間件了,以後社區中有了更多的方案提供:如redux-promiseredux-sagadvajsredux-observable等等。咱們仍是須要根據實際團隊和業務場景使用最適合咱們的方案來組織代碼編寫。

簡單回憶

  1. store自己的dispatch派發action更新數據這個動做是同步的。

  2. 所謂異步action,是經過引入中間件的方案加強dispatch後實現的。具體是applyMiddleware返回dispatch覆蓋原始storedispatch,當action爲函數時,進行定製的異步場景dispatch派發。

  3. 爲什麼會採起這種中間件加強的模式,我我的看來一是集中在一個位置方便統一控制處理,另外一個則是減小代碼中的冗餘判斷模板。

相關文章
相關標籤/搜索