【THE LAST TIME】從 Redux 源碼中學習它的範式

THE LAST TIME

The last time, I have learnedhtml

【THE LAST TIME】 一直是我想寫的一個系列,旨在厚積薄發,重溫前端。前端

也是給本身的查缺補漏和技術分享。git

筆者文章集合詳見github

TLT往期

前言

範式概念是庫恩範式理論的核心,而範式從本質上講是一種理論體系。庫恩指出:按既定的用法,範式就是一種公認的模型或模式編程

而學習 Redux,也並不是它的源碼有多麼複雜,而是他狀態管理的思想,着實值得咱們學習。redux

講真,標題真的是很差取,由於本文是我寫的 redux 的下一篇。兩篇湊到一塊兒,纔是完整的 Redux後端

上篇:從 Redux 設計理念到源碼分析api

本文續上篇,接着看 combineReducersapplyMiddlewarecompose 的設計與源碼實現數組

至於手寫,其實也是很是簡單,說白了,去掉源碼中嚴謹的校驗,就是市面上手寫了。固然,本文,我也儘可能以手寫演進的形式,去展開剩下幾個 api 的寫法介紹。微信

combineReducers

從上一篇中咱們知道,newState 是在 dispatch 的函數中,經過 currentReducer(currentState,action)拿到的。因此 state 的最終組織的樣子,徹底的依賴於咱們傳入的 reducer。而隨着應用的不斷擴大,state 愈發複雜,redux 就想到了分而治之(我寄幾想的詞兒)。雖然最終仍是一個根,可是每個枝放到不一樣的文件 or func 中處理,而後再來組織合併。(模塊化有麼有)

combineReducers 並非 redux 的核心,或者說這是一個輔助函數而已。可是我我的仍是喜歡這個功能的。它的做用就是把一個由多個不一樣 reducer 函數做爲 valueobject,合併成一個最終的 reducer 函數。

進化過程

好比咱們如今須要管理這麼一個"龐大"的 state

龐大的 state

let state={
    name:'Nealyang',
    baseInfo:{
        age:'25',
        gender:'man'
    },
    other:{
        github:'https://github.com/Nealyang',
        WeChatOfficialAccount:'全棧前端精選'
    }
}
複製代碼

由於太龐大了,寫到一個 reducer 裏面去維護太難了。因此我拆分紅三個 reducer

function nameReducer(state, action) {
  switch (action.type) {
    case "UPDATE":
      return action.name;
    default:
      return state;
  }
}

function baseInfoReducer(state, action) {
  switch (action.type) {
    case "UPDATE_AGE":
      return {
        ...state,
        age: action.age,
      };
    case "UPDATE_GENDER":
      return {
        ...state,
        age: action.gender,
      };

    default:
      return state;
  }
}


function otherReducer(state,action){...}
複製代碼

爲了他這個組成一個咱們上文看到的 reducer,咱們須要搞個這個函數

const reducer = combineReducers({
  name:nameReducer,
  baseInfo:baseInfoReducer,
  other:otherReducer
})
複製代碼

因此,咱們如今本身寫一個 combineReducers

function combineReducers(reducers){
    const reducerKeys = Object.keys(reducers);

    return function (state={},action){
        const nextState = {};

        for(let i = 0,keyLen = reducerKeys.length;i<keyLen;i++){
            // 拿出 reducers 的 key,也就是 name、baseInfo、other
            const key = reducerKeys[i];
            // 拿出如上的對應的 reducer: nameReducer、baseInfoReducer、otherReducer
            const reducer = reducers[key];
            // 去除須要傳遞給對應 reducer 的初始 state
            const preStateKey = state[key];
            // 拿到對應 reducer 處理後的 state
            const nextStateKey = reducer(preStateKey,action);
            // 賦值給新 state 的對應的 key 下面
            nextState[key] = nextStateKey;
        }
        return nextState;
    }
}
複製代碼

基本如上,咱們就完事了。

關於 reducer 更多的組合、拆分、使用的,能夠參照我 github 開源的先後端博客的 Demo:React-Express-Blog-Demo

源碼

export type Reducer<S = any, A extends Action = AnyAction> = (
  state: S | undefined,
  action: A
) => S

export type ReducersMapObject<S = any, A extends Action = Action> = {
  [K in keyof S]: Reducer<S[K], A>
}
複製代碼

定義了一個須要傳遞給 combineReducers 函數的參數類型。也就是咱們上面的

{
  name:nameReducer,
  baseInfo:baseInfoReducer,
  other:otherReducer
}
複製代碼

其實就是變了一個 statekey,而後 key 對應的值是這個 Reducer,這個 Reducerstate 是前面取出這個 keystate 下的值。

export default function combineReducers(reducers: ReducersMapObject) {
  //獲取全部的 key,也就是將來 state 的 key,同時也是此時 reducer 對應的 key
  const reducerKeys = Object.keys(reducers)
  // 過濾一遍 reducers 對應的 reducer 確保 kv 格式麼有什麼毛病
  const finalReducers: ReducersMapObject = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    
    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // 再次拿到確切的 keyArray
  const finalReducerKeys = Object.keys(finalReducers)

  // This is used to make sure we don't warn about the same
  // keys multiple times.
  let unexpectedKeyCache: { [key: string]: true }
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError: Error
  try {
    // 校驗自定義的 reducer 一些基本的寫法
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }
  // 重點是這個函數
  return function combination( state: StateFromReducersMapObject<typeof reducers> = {}, action: AnyAction ) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }
    
    let hasChanged = false
    const nextState: StateFromReducersMapObject<typeof reducers> = {}
    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)
      // 上面的部分都是咱們以前手寫內容,nextStateForKey 是返回的一個newState,判斷不能爲 undefined
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      // 判斷是否改變,這裏其實我仍是很疑惑
      // 理論上,reducer 後的 newState 不管怎麼樣,都不會等於 preState 的
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }
}
複製代碼

combineReducers 代碼其實很是簡單,核心代碼也就是咱們上面縮寫的那樣。可是我是真的喜歡這個功能。

applyMiddleware

applyMiddleware 這個方法,其實不得不說,redux 中的 Middleware。中間件的概念不是 redux 獨有的。ExpressKoa等框架,也都有這個概念。只是爲解決不一樣的問題而存在罷了。

ReduxMiddleware 說白了就是對 dispatch 的擴展,或者說重寫,加強 dispatch 的功能! 通常咱們經常使用的能夠記錄日誌、錯誤採集、異步調用等。

其實關於ReduxMiddleware, 我以爲中文文檔說的就已經很是棒了,這裏我簡單介紹下。感興趣的能夠查看詳細的介紹:Redux 中文文檔

Middleware 演化過程

記錄日誌的功能加強

  • 需求:在每次修改 state 的時候,記錄下來 修改前的 state ,爲何修改了,以及修改後的 state
  • Action:每次修改都是 dispatch 發起的,因此這裏我只要在 dispatch 加一層處理就一勞永逸了。
const store = createStore(reducer);
const next = store.dispatch;

/*重寫了store.dispatch*/
store.dispatch = (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}
複製代碼

如上,在咱們每一次修改 dispatch 的時候均可以記錄下來日誌。由於咱們是重寫了 dispatch 不是。

增長個錯誤監控的加強

const store = createStore(reducer);
const next = store.dispatch;

store.dispatch = (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('錯誤報告: ', err)
  }
}
複製代碼

因此如上,咱們也完成了這個需求。

可是,回頭看看,這兩個需求如何纔可以同時實現,而且可以很好地解耦呢?

想想,既然咱們是加強 dispatch。那麼是否是咱們能夠將 dispatch 做爲形參傳入到咱們加強函數。

多文件加強

const exceptionMiddleware = (next) => (action) => {
  try {
    /*loggerMiddleware(action);*/
    next(action);
  } catch (err) {
    console.error('錯誤報告: ', err)
  } 
}
/*loggerMiddleware 變成參數傳進去*/
store.dispatch = exceptionMiddleware(loggerMiddleware);
複製代碼
// 這裏額 next 就是最純的 store.dispatch 了
const loggerMiddleware = (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}
複製代碼

因此最終使用的時候就以下了

const store = createStore(reducer);
const next = store.dispatch;

const loggerMiddleware = (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

const exceptionMiddleware = (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('錯誤報告: ', err)
  }
}

store.dispatch = exceptionMiddleware(loggerMiddleware(next));
複製代碼

可是如上的代碼,咱們又不能將 Middleware 獨立到文件裏面去,由於依賴外部的 store。因此咱們再把 store 傳入進去!

const store = createStore(reducer);
const next  = store.dispatch;

const loggerMiddleware = (store) => (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

const exceptionMiddleware = (store) => (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('錯誤報告: ', err)
  }
}

const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(logger(next));
複製代碼

以上其實就是咱們寫的一個 Middleware,理論上,這麼寫已經能夠知足了。可是!是否是有點不美觀呢?且閱讀起來很是的不直觀呢?

若是我須要在增長箇中間件,調用就成爲了

store.dispatch = exception(time(logger(action(xxxMid(next)))))
複製代碼

這也就是 applyMiddleware 的做用所在了

咱們只須要知道有多少箇中間件,而後在內部順序調用就能夠了不是

const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore);
const store = newCreateStore(reducer)
複製代碼

手寫 applyMiddleware

const applyMiddleware = function (...middlewares) {
  // 重寫createStore 方法,其實就是返回一個帶有加強版(應用了 Middleware )的 dispatch 的 store
  return function rewriteCreateStoreFunc(oldCreateStore) {
  // 返回一個 createStore 供外部調用
    return function newCreateStore(reducer, initState) {
      // 把原版的 store 先取出來
      const store = oldCreateStore(reducer, initState);
      // const chain = [exception, time, logger] 注意這裏已經傳給 Middleware store 了,有了第一次調用
      const chain = middlewares.map(middleware => middleware(store));
      // 取出原先的 dispatch
      let dispatch = store.dispatch;
      // 中間件調用時←,可是數組是→。因此 reverse。而後在傳入 dispatch 進行第二次調用。最後一個就是 dispatch func 了(回憶 Middleware 是否是三個括號~~~)
      chain.reverse().map(middleware => {
        dispatch = middleware(dispatch);
      });
      store.dispatch = dispatch;
      return store;
    }
  }
}
複製代碼

解釋全在代碼上了

其實源碼裏面也是這麼個邏輯,可是源碼實現更加的優雅。他利用了函數式編程的compose 方法。在看 applyMiddleware 的源碼以前呢,先介紹下 compose 的方法吧。

compose

其實 compose 函數作的事就是把 var a = fn1(fn2(fn3(fn4(x)))) 這種嵌套的調用方式改爲 var a = compose(fn1,fn2,fn3,fn4)(x) 的方式調用。

compose的運行結果是一個函數,調用這個函數所傳遞的參數將會做爲compose最後一個參數的參數,從而像'洋蔥圈'似的,由內向外,逐步調用。

export default function compose(...funcs: Function[]) {
  if (funcs.length === 0) {
    // infer the argument type so it is usable in inference down the line
    return <T>(arg: T) => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args: any) => a(b(...args))) } 複製代碼

哦豁!有點蒙有麼有~ 函數式編程就是燒腦🤯且直接。因此愛的人很是愛。

compose是函數式編程中經常使用的一種組合函數的方式。

方法很簡單,傳入的形參是 func[],若是隻有一個,那麼直接返回調用結果。若是是多個,則funcs.reduce((a, b) => (...args: any) => a(b(...args))).

咱們直接啃最後一行吧

import {componse} from 'redux'
function add1(str) {
	return 1 + str;
}
function add2(str) {
	return 2 + str;
}
function add3(a, b) {
	return a + b;
}
let str = compose(add1,add2,add3)('x','y')
console.log(str)
//輸出結果 '12xy'
複製代碼

輸出

dispatch = compose<typeof dispatch>(...chain)(store.dispatch) applyMiddleware 的源碼最後一行是這個。其實即便咱們上面手寫的 reverse 部分。

reduce 是 es5 的數組方法了,對累加器和數組中的每一個元素(從左到右)應用一個函數,將其減小爲單個值。函數簽名爲:arr.reduce(callback[, initialValue])

因此如若咱們這麼看:

[func1,func2,func3].reduce(function(a,b){
  return function(...args){
    return a(b(...args))
  }
})
複製代碼

因此其實就很是好理解了,每一次 reduce 的時候,callbacka,就是一個a(b(...args))function,固然,第一次是 afunc1。後面就是無限的疊羅漢了。最終拿到的是一個 func1(func2(func3(...args)))function

總結

因此回頭看看,redux 其實就這麼些東西,第一篇算是 redux 的核心,關於狀態管理的思想和方式。第二篇能夠理解爲 redux 的自帶的一些小生態。所有的代碼不過兩三百行。可是這種狀態管理的範式,仍是很是指的咱們再去思考、借鑑和學習的。

學習交流

  • 關注公衆號【全棧前端精選】,每日獲取好文推薦
  • 添加微信號:is_Nealyang(備註來源) ,入羣交流
公衆號【全棧前端精選】 我的微信【is_Nealyang】
相關文章
相關標籤/搜索