Redux 實現過程的推演

這是一篇淺入淺出的 Redux 實現過程的推演筆記!正常來說應該是要從源碼下手開始解析,這裏是逆向推演,假如有需求是要這麼一個東西,那麼該如何從零開始實現?

通過該筆記,更多的是希望自己能夠多熟悉從無到有的開發思維,而非源碼解析這種從有到有的輪子思維。

Rudex 介紹

首先確認目標,要寫個什麼樣的東西。

官宣:A predictable state container for JavaScript apps. JavaScript 應用程序中可預測的狀態容器。

通過這句話,挖掘一些關鍵點:

    1. 要有個狀態:state
    1. 要有個(狀態)容器:store
    1. 在狀態改變的流程中,狀態是可預測的,即:
    • 3.1 何時觸發狀態進行改變? dispatch,觸發 state 的改變
    • 3.2 觸發了什麼改變? action,描述 state 該做什麼修改
    • 3.3 狀態做什麼樣改變? reducer,描述 state 的改變過程,傳入改變前的 state,返回改變後的 state

reducer 是個純函數很重要,能夠消除其他影響讓 state 的變化真正是可預測的。

有了這些關鍵點,接下來就是實現了

Redux 實現

注意,被控對象(state)被包含在整個鏈路中,我們關心鏈路即可

/*
 * createStore 狀態容器
 * @param reducers 容器總得需要知道是做什麼樣的改變
 * @param initialState 初始化(預置)的 state
 */

const createStore = (reducers, initialState) => {
  // 通過 reducer 取到改變後的值,重新存儲
  let currentReducer = reducers;

  // 存
  let currentState = initialState;

  // 取
  const getState = () => {
    return currentState;
  };

  // 改
  const dispatch = action => {
    currentState = currentReducer(currentState, action);
    return action;
  };

  // 這裏可能還需要可被觀察的,留坑、不實現,有興趣的看文章後的源碼閱讀鏈接
  return {
    getState,
    dispatch
  };
};

至此,容器部分完成。

接下來看改變流程可預測的實現:

action 描述 state 該做什麼修改,這僅僅是個對象而已,我們僅需要定義好格式,如下例子(比如數字的重置)

/*
 * action
 * @property type 描述需要做什麼操作
 * @property preload 預加載數據,包含 state 的新值
 */

const RESET = "RESET";
const RESET_ACTION = {
  type: RESET,
  preload: {
    count: 0
  }
};

reducer 描述狀態做了什麼改變?

/*
 * reducer
 * @currentState 當前的 state,舊值
 * @action 該做什麼修改的類型描述
 */

const reducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case RESET: {
      return {
        ...state,
        ...action.preload
      };
    }
    default: {
      return state;
    }
  }
};

將上面三份代碼合一起,試一試

const store = createStore(reducer);
store.dispatch({ type: RESET, preload: { count: 10 } });
store.getState(); // output { count: 10}
store.dispatch(RESET_ACTION);
store.getState(); // output { count: 0}

流程是正常了,但情況稍微有點不對,如果在 dispatch 之前去 getState,那麼 state 是 {},而不是給的初始值{count:0}

對於這點,只要在 createStore 時候的默認執行一次 dispatch,用以生成初始的 state tree

在 createStore 的 return 之前加入 dispatch

dispatch({ type: "@redux/INIT" });

ok,至此已經有個簡版 redux 了(observable 和 subscribe 的話,加個 listeners,這裏不多做介紹,有興趣的點擊文末鏈接),但是 redux 還有 middleware 的功能(而且這部分代碼會比這裏好玩一點),繼續實現 middleware

Middleware 實現

首先,明確一下希望 middleware 能夠做什麼?

官方描述:

Middleware is the suggested way to extend Redux with custom functionality. Middleware lets you wrap the store's dispatch method for fun and profit. The key feature of middleware is that it is composable. Multiple middleware can be combined together, where each middleware requires no knowledge of what comes before or after it in the chain.

Middleware 是通過自定義功能來擴展 redux 的推薦方法,它能夠讓你有效的包裹 store 的 dispatch 方法已達到所需的目的,其關鍵特徵在於組合,多個 middleware 能夠進行組合,每個 middleware 都是獨立的,它們不需要知道在流程的之前或之後會發生什麼。

從上面的描述中,得出結論:

  • 首先,middleware 要是個 function 且該函數對 dispatch 的執行做包裹;
  • 然後,每個 middleware 互不相干且可組合的;
  • 最後,值得注意的是 middleware 內部能夠訪問及操作 state,不然只能做些和 state 不相干的事情,那這個擴展的意義就不大了。

接下來是推演實現的過程:

首先,我們需要考慮的是怎麼處理每個函數,讓其既是獨立的,又是可組合的,而且內部還得包裹 dispatch。看下面的思考過程:

比如我有個函數 a 和函數 dispatch 我們希望執行的過程是 dispatch 被包裹在 a 內部執行,首先想到的肯定是 callback 形式,沒毛病,看代碼

var a = function(next) {
  console.log("a-before");
  next();
  console.log("a-after");
};
var dispatch = function(action) {
  console.log("do ", action);
  return action;
};

a(dispatch);
// output:
// a-before
// do undefined
// a-after

但是沒有能夠把 dispatch 的參數傳進去呀,於是對於內部函數的傳參,不陌生的,我們又想到,外包再套一層 function,閉包存參(用 jqyery 的時候綁定事件沒少這麼幹吧),看代碼:

var a = function(next) {
  return function(action) {
    console.log("a-before");
    next(action);
    console.log("a-after");
  };
};
var dispatch = function(action) {
  console.log("do ", action);
  return action;
};

a(dispatch)("test action");
// output:
// a-before
// do test action
// a-after

但如果 a 的這種類型的包裹函數是多個的,試下加個函數 b,因爲要嵌套的是函數,所以將 action 作爲第二次執行的參數

var a = function(next) {
  return function(action) {
    console.log("a-before");
    next(action);
    console.log("a-after");
  };
};
var b = function(next) {
  return function(action) {
    console.log("b-before");
    next(action);
    console.log("b-after");
  };
};
var dispatch = function(action) {
  console.log("do ", action);
  return action;
};

a(b(dispatch))("test action");
// output:
// a-before
// b-before
// do test action
// b-after
// a-after

然後問題又來了 ,我們要再加個函數 c,難道讓我寫 a(b(c(dispacth)))(action)?

既然函數 a, b, c 都是一種類型的東西,可以格式化成數組,回想一下什麼方法能夠依次組合數組的每一項。沒錯,是 reduce,繼續看代碼:

var a = function(next) {
  return function(action) {
    console.log("a-before");
    next(action);
    console.log("a-after");
  };
};
var b = function(next) {
  return function(action) {
    console.log("b-before");
    next(action);
    console.log("c-after");
  };
};
var c = function(next) {
  return function(action) {
    console.log("c-before");
    next(action);
    console.log("c-after");
  };
};
var dispatch = function(action) {
  console.log("do ", action);
  return action;
};

var d = [a, b, c].reduce((pre, now) => (...args) => pre(now(...args)));

d(dispatch)("test action");
// output:
// a-before
// b-before
// c-before
// do test action
// c-after
// b-after
// a-after

好了,想到了如何將 middleware 串起來和如何將 dispatch 封裝的方法後,集成到 redux 的代碼裏試試

單獨抽一個 compose 函數用以處理一個或多個 middleware,代碼如下:

const 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)));
};

修改 createStore 裏的 dispatch 方法,讓其支持當存在 middleware 的時候,dispatch 需要被重寫。代碼如下:

/*
 * createStore 狀態容器
 * @param reducers 容器總得需要知道是做什麼樣的改變
 * @param initialState 初始化(預置)的 state
 * @param enhancer 擴展的 middlewares
 */

const createStore = (reducers, initialState, enhancer) => {
  // 參數互換 如果 initialState 是個函數,enhancer = undefined 則 enhancer 和 initialState 互換
  if (typeof initialState === "function" && typeof enhancer === "undefined") {
    enhancer = initialState;
    initialState = undefined;
  }

  // 如果有 middleware 的時候,則 createStore 稍後處理,處理詳情參照 applyMiddleware 函數
  if (typeof enhancer !== "undefined" && typeof enhancer === "function") {
    // 爲什麼是這樣寫? 繼續往下看
    return enhancer(createStore)(reducer, initialState);
  }

  // ...
  // 之前的代碼
};

結合 createStore,注意到前文提出的,middleware 內部支持訪問和操作 state,我們需要實現 createStore 裏面的 enhancer 函數,就是函數 applyMiddleware,於是給出代碼:

/*
 * applyMiddleware 實現中間件的應用
 * @param ...middlewares 插入的 state 處理流程的中間件
 */
const applyMiddleware = (...middlewares) => {
  // 傳入 middlewares
  return createStore => (...args) => {
    const store = createStore(...args);
    // middleware 內部能做的 state 操作
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    };
    // 將 middleware 處理,以 middlewareAPI 作爲參數執行並且取到 middleware 的內部函數
    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    // 進行 compose 組合
    // 如存在 3 個 middleware A(ABefore,AAfter) B(BBefore,BAfter) C(CBefore,CAfter)
    // 則執行順序是 ABefore - BBefore - CBefore - (真實的操作) - CAfter - BAfter - AAfter
    dispatch = compose(...chain)(store.dispatch);

    return {
      ...store,
      dispatch
    };
  };
};

核心實現的代碼寫完了,然後測試一下?

我們需要在 createStore 的 dispatch 里加個 "do dispatch" 的 log,方便看執行流程

const store = createStore(reducer, applyMiddleware(logger));

再寫 middleware 的函數,

const logger = ({ getState }) => {
  return next => action => {
    console.log("will dispatch", action);

    const returnValue = next(action);

    console.log("state after dispatch", getState());

    return returnValue;
  };
};
store.dispatch(RESET_ACTION);
// output
// will dispatch {type: "RESET", preload: {…}}
// do dispatch
// state after dispatch {count: 0}

好了,來回顧一下實現過程,首先有個目標:做一個可預測的狀態容器;然後分析目標,挖掘關鍵點,依次實現。實現的過程更多在於"容器操作的規範",技術方面好像確實都是一些 js 基礎知識的運用。

注意,本文僅解析 redux 的實現思路,代碼與源碼並不完全相同。redux 不止這些代碼(但其實也沒多少其餘代碼),還有一些斷言、錯誤提示、開發提示、bindActionCreators 啥的操作等等...

源碼筆記鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章