深刻解析dva源碼之dva-core篇

前言

關於 Dva 源碼解析,官網已有相關指南 Dva 源碼解析html

而本篇文章與其不一樣的是:深刻解析 dva-core 源碼,和探討 dva-core 的相關技術及應用場景前端

好了正文開始!react

dva 的基本分離架構

大概翻一下 dva 的項目文件目錄,能夠知道 dva 是使用 lerna 進行多包管理的項目。主要分爲如下四個包,以下所示:git

~/Desktop/dva $ tree ./packages/ -L 1
./packages/
├── dva
├── dva-core
├── dva-immer
└── dva-loading

4 directories, 0 files
複製代碼

dva 使用 react-redux 實現了 view 層。github

dva-core 基於 redux 和 redux-saga 處理 model 層,好比包括了 state 管理、數據的異步加載、訂閱-發佈模式。web

dva-immer 依賴 immer 來優雅處理不可變狀態。(備註:若是想在項目中輕量引入不可變狀態管理的話,能夠考慮 immer.js)編程

dva-loading 實現了自動處理 loading 狀態。redux

dva-immer 和 dva-loading 其實都是做爲 dva 的核心插件存在的。以下圖方式註冊插件便可:api

const { create, saga } = require("dva-core");
const createImmerPlugin = require("dva-immer");
const createLoadingPlugin = require("dva-loading");

const app = create();
app.use(createImmerPlugin());
app.use(createLoadingPlugin());
複製代碼

我的仍是很喜歡 immer 的,能夠用可變操做的方式生產不可變的值。dva-immer 的確值得推薦。數組

不過,實際上兩個插件都是寥寥幾行代碼,而咱們應該學習的是 dva 的插件註冊機制。

固然,dva 核心依賴了 dva-core。本文的重點也在於此。

dva-core

dva-core 因爲集成了 redux 和 redux-saga。那麼在對於應用的狀態管理和反作用管理這兩種場景應該具有強大的能力。

dva-core 包只導出了三個主要的 API: create、saga、utils。咱們大致上能夠忽略後面兩者。將注意力集中在 create 上。

首先,使用 create 建立一個最簡單的 app 對象。

const { create } = require("dva-core");
const app = create();
app.start();
複製代碼

打印 app 對象以下:

{
  // 能夠認爲私有屬性
  _models: [ { namespace: '@@dva', state: 0, reducers: [Object] } ],
  _store:
   { dispatch: [Function],
     subscribe: [Function: subscribe],
     getState: [Function: getState],
     replaceReducer: [Function: replaceReducer],
     runSaga: [Function: bound runSaga],
     asyncReducers: {},
     [Symbol(observable)]: [Function: observable] },
  _plugin:
   Plugin {
     _handleActions: null,
     hooks:
      { onError: [],
        onStateChange: [],
        onAction: [],
        onHmr: [],
        onReducer: [],
        onEffect: [],
        extraReducers: [],
        extraEnhancers: [],
        _handleActions: [] } },
  _getSaga: [Function: bound getSaga],
  // 能夠認爲公有屬性,即真正暴露給第三方的 api
  start: [Function: start],
  use: [Function: bound use],
  model: [Function: bound injectModel],
  unmodel: [Function: bound unmodel],
  replaceModel: [Function: bound replaceModel]
};
複製代碼

咱們忽略如下劃線開頭對象,由於語義上通常咱們認爲是私有屬性。因而咱們關注其餘屬性能夠發現,存在如下幾個 api:start、use、model、unmodel、replaceModel

精簡 dva/packages/dva-core/src/index.js 中的代碼發現,的確如此。

// dva/packages/dva-core/src/index.js
// ...
export function create(hooksAndOpts = {}, createOpts = {}) {
  // ...
  const plugin = new Plugin();
  // 掛載了use、model、start方法
  const app = {
    use: plugin.use.bind(plugin),
    model,
    start
  };
  return app;

  function model(m) {
    // ...
    const prefixedModel = prefixNamespace({ ...m });
    app._models.push(prefixedModel);
    return prefixedModel;
  }
  function injectModel(createReducer, onError, unlisteners, m) {}
  function unmodel(createReducer, reducers, unlisteners, namespace) {}
  function replaceModel(createReducer, reducers, unlisteners, onError, m) {}
  function start() {
    // ...
    // 更新掛載 model、unmodel、replaceModel
    app.model = injectModel.bind(app, createReducer, onError, unlisteners);
    app.unmodel = unmodel.bind(app, createReducer, reducers, unlisteners);
    app.replaceModel = replaceModel.bind(app, createReducer, reducers, unlisteners, onError);
    // ...
  }
}
複製代碼

這裏咱們注意到 app.model 方法實際上在 start()裏經過 injectModel.bind(app, createReducer, onError, unlisteners)柯里化後更新了。

由於在一開始設置的 model 方法,只是簡單更新了 app._models 列表。而 injectModel 才處理 model 相關的邏輯。

而 unmodel、replaceModel 方法在 app.start()以前都不存在。而是在 start() 裏對內部 unmodel、replaceModel 方法進行柯里化返回到的新方法。

由此大概整理了 create 以後的幾個 api 的來源。

model 註冊:model

顧名思義,就是註冊 dva model,以及相反操做取消註冊。

model 函數,本質上就是將從新定義命名空間的 model 推入內部的 model 列表。

// dva/packages/dva-core/src/index.js
function model(m) {
  const prefixedModel = prefixNamespace({ ...m });
  app._models.push(prefixedModel);
  return prefixedModel;
}
複製代碼

prefixNamespace 函數實際上將 model 上用戶定義的 reducers 和 effects 的 key 映射在此 model 的命名空間下。好比如下 model:

app.model({
  namespace: "users",
  state: ["foo"],
  reducers: {
    add(state, { payload }) {
      return [...state, payload];
    }
  },
  effects: {
    *fetch(_, { put }) {
      yield delay(200);
      yield put({ type: "add", payload: "{data}" });
    }
  }
});
複製代碼

裏面的 model 在通過 prefixNamespace 以後:

{
  namespace: 'users',
  state: [ 'foo' ],
  reducers: { 'users/add': [Function: add] },
  effects: { 'users/fetch': [GeneratorFunction: fetch] }
}
複製代碼

能夠看到,users/addusers/fetch 都是修改後的新 key。

所以,通過 prefixNamespace 的全部的 model 的 reducers 和 effects 在最後彙總成一個對象的時候,也能夠錯落有致地根據 namespace 進行歸類。

固然,這只是新增 model 和從新映射 key 而已。咱們前面提到,model 函數其實是 injectModel 柯里化後的產生的函數。所以咱們有必要在 injectModel 函數裏看到底幹了什麼事情、

// dva/packages/dva-core/src/index.js
function injectModel(createReducer, onError, unlisteners, m) {
  // 推入model到內部model列表
  m = model(m);

  const store = app._store;
  // 將namespace下的model全部reducers函數compose成爲一個reducer
  store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
  store.replaceReducer(createReducer());
  if (m.effects) {
    store.runSaga(app._getSaga(m.effects, m, onError, plugin.get("onEffect"), hooksAndOpts));
  }
  if (m.subscriptions) {
    unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);
  }
}
複製代碼

咱們首先來看store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);這一句,裏面的的確確幹了不少事情。來,咱們深刻分析。

// dva/packages/dva-core/src/getReducer.js
import defaultHandleActions from "./handleActions";

export default function getReducer(reducers, state, handleActions) {
  // Support reducer enhancer
  // e.g. reducers: [realReducers, enhancer]
  if (Array.isArray(reducers)) {
    return reducers[1]((handleActions || defaultHandleActions)(reducers[0], state));
  } else {
    return (handleActions || defaultHandleActions)(reducers || {}, state);
  }
}
複製代碼

這裏的 isArray 分支判斷條件是由於 dva 中的 reducer 支持數組形式傳入 enhancer。什麼意思呢?請看如下示例代碼:

app.model({
  namespace: "char",
  state: "sulirc",
  reducers: [
    {
      timestamp(state, { payload }) {
        return state + (payload || Date.now());
      }
    },
    function enhancer(reducer) {
      return (state, action) => {
        if (action.type === "char/timestamp") {
          return reducer(`[${state.toUpperCase()}]@`, action);
        }
        return reducer(state, action);
      };
    }
  ]
});
複製代碼

留意咱們的 reducers 變成了數組。因此當咱們 dispatch 任意一個 action 的時候,都會通過 enhancer 先處理。

app._store.dispatch({ type: "char/timestamp" });
console.log(app._store.getState());
// => { '@@dva': 0, char: '[SULIRC]@1575169473563' }
複製代碼

若是想搞懂以上 reducer 的 compose、enhancer 的實現,咱們先來看這一句:

(handleActions || defaultHandleActions)(reducers || {}, state);
複製代碼

handlerActions 其實就是入口 getReducer 中傳入 plugin._handleActions。本質上依賴外界傳入。好比 dva-immer 這個 plugin 就是實現了_handleActions 這個鉤子。

而咱們來看 defaultHandleActions,也便是如下代碼的 handleActions。

// dva/packages/dva-core/src/handleActions.js
// 能夠想象爲一個大型的 switch case 語句,type匹配的時候才使用對應的reducer
function handleAction(actionType, reducer = identify) {
  return (state, action) => {
    const { type } = action;
    // 將閉包中的 actionType 和動態傳入的 type 進行匹配判斷
    if (actionType === type) {
      return reducer(state, action);
    }
    return state;
  };
}

// 好比 [reducer1, reducer2] 通過 reduceReducers 處理後變成:
// (state, action) => reducer2(reducer1(state, action), action)
function reduceReducers(...reducers) {
  return (previous, current) => reducers.reduce((p, r) => r(p, current), previous);
}

function handleActions(handlers, defaultState) {
  // 將全部reducer從新映射爲有type類型判斷的reducer。
  const reducers = Object.keys(handlers).map(type => handleAction(type, handlers[type]));
  // compose成爲一個大的reducer
  const reducer = reduceReducers(...reducers);
  // 這裏只是經過返回一個高階函數作了 state = defaultState 默認操做
  return (state = defaultState, action) => reducer(state, action);
}

export default handleActions;
複製代碼

想要理解上面這一段代碼,仍是須要熟悉閉包和高階函數,以及函數組合等概念的。由於做者利用了閉包和高階函數所要達成的目的,就是讓使用 dva 的人寫 reducer 時能夠更加便利,不用寫一個巨大的 switch case 語句。

因而咱們能夠知道store.asyncReducers[m.namespace]即得到了一個將 model 命名空間下的全部 reducers 進行 compose(組合)後的巨大 reducer。

而後 store.replaceReducer(createReducer()); store.replaceReducer 實際上是 redux 提供的 api。

官方代碼註釋中寫到:

Replaces the reducer currently used by the store to calculate the state. You might need this if your app implements code splitting and you want to load some of the reducers dynamically. You might also need this if you implement a hot reloading mechanism for Redux.

同窗們,上述文字的意思很明顯,若是開發者有 code splitting 或者 動態加載 reducers 的需求,那須要這個 api 來進行熱重載。

結合 dva-core 的代碼來看,asyncReducers 就是動態加載 reducers。具體理解就是說在 app.start()以後的 app.model()中的 reducers 就會被劃分在 asyncReducers 裏面。所以也就須要熱重載。由於在 redux 裏,reducers 實際上是文件裏的一個對象,在初始化的 createStore 的時候就肯定了。而在 dva-core 中,app.start()時即進行了 createStore 操做,因此須要 replaceReducer 來指示 redux,替換更新 reducers 對象。

那麼咱們看 createReducer 函數,其實就是將如下四者的一個有機整合:

  1. app.start 以前肯定的靜態 reducers
  2. plugin.get('extraReducers')中獲取的附加 reducers
  3. app.start 以後新增的動態 asyncReducers
  4. 以及經過 plugin.get('onReducer')獲取的,將以上三者進行加強的 enhancer

代碼以下:

// dva/packages/dva-core/src/index.js
function createReducer() {
  // reducerEnhancer => plugin.get('onReducer')
  return reducerEnhancer(
    combineReducers({
      ...reducers,
      // extraReducers => plugin.get('extraReducers');
      ...extraReducers,
      ...(app._store ? app._store.asyncReducers : {})
    })
  );
}
複製代碼

在 injectModel 裏最後兩句判斷語句,其實就是在註冊 effects 和 subscriptions。

if (m.effects) {
  // 註冊 effects
  store.runSaga(app._getSaga(m.effects, m, onError, plugin.get("onEffect"), hooksAndOpts));
}
if (m.subscriptions) {
  // 註冊 subscriptions。
  unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);
}
複製代碼

runSaga 其實就是 sagaMiddleware.run。經過 app._getSaga 一樣封裝返回了一個巨大的 saga。具體邏輯下文會深刻描述。

runSubscription 將全部寫在 model 的裏的 subscriptions 運行,並將每一個 subscription 返回的取消訂閱事件的函數,收集後返回出去。具體邏輯下文一樣會深刻描述。

移除 model:unmodel

接下來,咱們看 unmodel 幹了什麼事情。若是你們理解了上面的 model 函數。下面的 unmodel 函數天然不難理解。

// dva/packages/dva-core/src/index.js
function unmodel(createReducer, reducers, unlisteners, namespace) {
  const store = app._store;

  // 刪除reducers
  delete store.asyncReducers[namespace];
  delete reducers[namespace];
  store.replaceReducer(createReducer());
  store.dispatch({ type: "@@dva/UPDATE" });

  // 經過分發一個內部事件,取消反作用
  store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });

  // 取消監聽這個命名空間的全部訂閱
  unlistenSubscription(unlisteners, namespace);

  // 在app的內部models列表裏刪除此model
  app._models = app._models.filter(model => model.namespace !== namespace);
}
複製代碼

從語義上來看,unmodel 是取消註冊一個 model,如何作到乾淨的移除這個 model 呢?分如下幾步:

  1. 刪除全部該 model 命名空間的靜態、動態 reducers。並通知 redux、dva。
  2. 經過分發一個內部事件 ${namespace}/@@CANCEL_EFFECTS 來通知取消 effects 反作用。
  3. 執行該 model 全部訂閱函數返回的取消訂閱函數。
  4. 從內部的 model 列表中過濾刪除此 model。

更新 model:replaceModel

能夠在 app.start()以後替換或新增已有 model。邏輯大體與 model、unmodel 相似。同窗們能夠參照上述兩者理解。

// dva/packages/dva-core/src/index.js
function replaceModel(createReducer, reducers, unlisteners, onError, m) {
  const store = app._store;
  const { namespace } = m;
  const oldModelIdx = findIndex(app._models, model => model.namespace === namespace);

  if (~oldModelIdx) {
    // 經過分發一個內部事件,取消反作用
    store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });

    // 刪除reducers
    delete store.asyncReducers[namespace];
    delete reducers[namespace];

    // 取消監聽這個命名空間以前的全部訂閱
    unlistenSubscription(unlisteners, namespace);

    // 在app的內部models列表裏刪除此model
    app._models.splice(oldModelIdx, 1);
  }

  // 直接更新此model
  app.model(m);

  store.dispatch({ type: "@@dva/UPDATE" });
}
複製代碼

訂閱:subscriptions

執行訂閱函數以下:

// dva/packages/dva-core/src/subscription.js
export function run(subs, model, app, onError) {
  const funcs = [];
  const nonFuncs = [];
  for (const key in subs) {
    // 只執行用戶編寫的訂閱函數
    if (Object.prototype.hasOwnProperty.call(subs, key)) {
      const sub = subs[key];
      const unlistener = sub(
        {
          dispatch: prefixedDispatch(app._store.dispatch, model),
          history: app._history
        },
        onError
      );
      // 指望返回的是取消訂閱函數
      if (isFunction(unlistener)) {
        funcs.push(unlistener);
      } else {
        nonFuncs.push(key);
      }
    }
  }
  return { funcs, nonFuncs };
}
複製代碼

從 sub 的調用來看,傳參格式爲 ({ dispatch, history }, onError) => unlistenFunction。

dva 倡導你們寫訂閱函數的時候必定要返回取消訂閱函數,就好比存在 addEventListener 就必定得存在 removeEventListener。這纔是良好的開發習慣。一樣在 React 的 useEffect 鉤子裏,也是如此。

若是開發者不返回取消訂閱函數,在會被歸類到 nonFuncs 裏。在 unmodel、replaceModel 的時候則會喜提 dva 送你的大大的 warning。

// dva/packages/dva-core/src/subscription.js
export function unlisten(unlisteners, namespace) {
  if (!unlisteners[namespace]) return;

  const { funcs, nonFuncs } = unlisteners[namespace];
  // dva不想看到你不寫取消訂閱函數,並絕不客氣地向你丟了一個警告。
  warning(
    nonFuncs.length === 0,
    `[app.unmodel] subscription should return unlistener function, check these subscriptions ${nonFuncs.join( ", " )}`
  );
  // 遍歷執行取消訂閱的函數
  for (const unlistener of funcs) {
    unlistener();
  }
  delete unlisteners[namespace];
}
複製代碼

淺談 reducers

reducers 是惟一能夠修改 state 的地方。由 action 觸發。

dva-core 所處理的部分上文有解釋過,大致上來講就是將 reducers 的從新映射到對應命名空間。同時收集動態、靜態、附加 reducers、以及經過 enhancer 加強處理。剩餘的就是 redux 的工做了。

enhancer 的來源有 enhancers = [applyMiddleware(...middlewares), ...extraEnhancers]。其中 extraEnhancers 經過 plugin.get('extraEnhancers') 獲取。applyMiddleware(...middlewares)則是將裏面全部中間件在 redux 中注入{ getState, dispatch }的 api 後,再組合(compose)全部中間件以後返回出去。

redux 中間件的約定格式:

const reduxMiddleware = store => next => action => {
  // ...
  next(action);
  // => dispatch(action)
};
複製代碼

其中 next 就是注入的 dispatch。

本質上 applyMiddleware 所作的工做是將 store.dispatch 經過注入的中間件不斷加強,一層套一層(能夠必定程度上參考 koa 中的中間件),最終返回的就是基本上是{ getState, dispatch },可是裏面的 dispatch 倒是一個 compose 了全部中間件的 dispatch 了。

這一整套邏輯裏面運用了大量的高階函數、函數組合的技巧,的確讓人頭暈,建議你們能夠自行整理一下。

幫助理解方式,結合函數堆棧的壓棧和彈棧,這套中間件感受就像是在打乒乓球同樣,來回來回,的確十分有趣。

// dva/packages/dva-core/src/createStore.js
// ...
// applyMiddleware這套邏輯得去redux裏看。上文有描述。
const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];
// 再一次的compose enhancers,而後createStore也是去redux裏看。
return createStore(reducers, initialState, composeEnhancers(...enhancers));
複製代碼

能夠看到,不管是 redux 仍是 dva。compose,也便是 reduce 都用到飛起。這裏面涉及到了函數式編程裏的函數組合概念,的確是高級、簡潔又有用。

通過了這麼多彎彎繞繞,帶來的效果就是,dispatch 操做在走到 reducers 以前,必然得先通過層層中間件,最後纔是最初的那個 dispatch 發揮它的做用。

關於 effects

effects 用於處理異步操做和業務邏輯,不直接修改 state。由 action 觸發,能夠觸發 action。

關於 effects,實際上是 dva 依賴 redux-saga 實現對異步操做、業務邏輯的一種管理,對其在內部進行了一次封裝,暴露更加便利的 api。

在 start 初始化,以及在 model、repaceModel 的時候都有 runSaga(便是 sagaMiddleware.run)方法對指定 model 下全部 effects 進行一次。

// dva/packages/dva-core/src/index.js
function injectModel() {
  // ...
  if (m.effects) {
    // 對該 model 的 effects 註冊 saga 並運行
    store.runSaga(app._getSaga(m.effects, m, onError, plugin.get("onEffect"), hooksAndOpts));
  }
  // ...
}

function start() {
  const sagas = [];
  const reducers = { ...initialReducer };
  for (const m of app._models) {
    reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
    if (m.effects) {
      // 對全部 model 經過 effects 註冊saga
      sagas.push(app._getSaga(m.effects, m, onError, plugin.get("onEffect"), hooksAndOpts));
    }
  }
  // 運行全部 sagas
  sagas.forEach(sagaMiddleware.run);
}
複製代碼

而 app._getSaga 又是什麼?本質上是將指定 model 下的全部 effect 集合生成一個大型的 saga,內部爲一個根據 type 類型判斷返回不一樣 saga 處理函數的 switch case 語句。好比有 watcher、takeEvery、takeLatest、throttle、poll 等 type 類型。

type 類型由 create(hooksAndOpts, createOpts)中的 hooksAndOpts 傳入,不一樣的 type 類型返回不一樣的 saga 處理邏輯。type 默認爲 takeEvery。

那麼做爲 dva 處理 effects 的入口,實現邏輯以下,貼上 getSaga 代碼和註釋:

// dva/packages/dva-core/src/getSaga.js
export default function getSaga(effects, model, onError, onEffect, opts = {}) {
  // 返回了一個集合 saga
  return function*() {
    for (const key in effects) {
      if (Object.prototype.hasOwnProperty.call(effects, key)) {
        const watcher = getWatcher(key, effects[key], model, onError, onEffect, opts);
        // fork 是非阻塞調用
        const task = yield sagaEffects.fork(watcher);
        // 提供了一種取消反作用的方式
        yield sagaEffects.fork(function*() {
          // 等待取消該effect的信號
          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
          // 執行取消
          yield sagaEffects.cancel(task);
        });
      }
    }
  };
}
複製代碼

基本上不一樣 type,都會走如下這一套 getWatcher 函數裏的 sagaWithCatch 這個邏輯。

// dva/packages/dva-core/src/getSaga.js
function* sagaWithCatch(...args) {
  // createPromiseMiddleware中會用到的 __dva_resolve 和 __dva_reject
  const { __dva_resolve: resolve = noop, __dva_reject: reject = noop } =
    args.length > 0 ? args[0] : {};
  try {
    // 發起saga的開始信號
    yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
    // 執行 effect這個 generator
    const ret = yield effect(...args.concat(createEffects(model, opts)));
    // 發起saga的結束信號
    yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });
    resolve(ret);
  } catch (e) {
    // onError鉤子觸發
    onError(e, {
      key,
      effectArgs: args
    });
    if (!e._dontReject) {
      reject(e);
    }
  }
}
複製代碼

能夠看到 __dva_resolve 和 __dva_reject 實際上是在 createPromiseMiddleware 這個中間件裏拋出的。可是決定 resolve 和 reject 的權力在 saga 裏。

// dva/packages/dva-core/src/createPromiseMiddleware.js
export default function createPromiseMiddleware(app) {
  return () => next => action => {
    const { type } = action;
    if (isEffect(type)) {
      return new Promise((resolve, reject) => {
        next({
          __dva_resolve: resolve,
          __dva_reject: reject,
          ...action
        });
      });
    } else {
      return next(action);
    }
  };
  // ...
}
複製代碼

所以咱們能夠手動實驗, app._store.dispatch 一個 reducer 的時候,返回的是一個 action。可是 app._store.dispatch 一個 effect 的時候,卻返回了一個 Promise,就是由於 createPromiseMiddleware 這個中間件的做用。

同時也會每一個 effect 初始化的時候也會觸發 plugin.get('onEffect') 鉤子。

具體 effect 的處理邏輯其實還可深挖,但筆者對 redux-saga 並非很熟,所以關於 saga 的介紹到此爲止。

淺談插件

關於 dva 中的鉤子是由 Plugin 進行觸發的。有以下這麼多鉤子:

// dva/packages/dva-core/src/Plugin.js
const hooks = [
  "onError",
  "onStateChange",
  "onAction",
  "onHmr",
  "onReducer",
  "onEffect",
  "extraReducers",
  "extraEnhancers",
  "_handleActions"
];
複製代碼

dva-core 特地實現了一個 Plugin 類進行鉤子的管理。

在 constructor 裏經過 reduce 方式將上述鉤子數組初始化爲如下的形式:

{
  "onError": [],
  "onStateChange": [],
  "onAction": [],
  "onHmr": [],
  "onReducer": [],
  "onEffect": [],
  "extraReducers": [],
  "extraEnhancers": [],
  "_handleActions": []
}
複製代碼

經過 plugin.use 方式往對應鉤子 key 裏更新數組,經過 plugin.apply、plugin.get 獲取並調用。本質上並不複雜。

不過,這裏提一句,關於插件 Plugin 的使用,在前端的各類庫裏的確是層出不窮。好比 Webpack、Rollup 等都大量使用。筆者也偏心這種方式,它將內在的核心代碼與外在注入的代碼共同運行,但又從設計上,隔離了穩定與不穩定。同時甚至能夠動態註冊、插拔。

plugin 思想值得學習和實踐。

關於 redux 和 redux-saga

因爲 dva-core 依賴了 redux 和 redux-saga。在介紹 dva-core 以前,仍是先簡單熟悉一下二者的基本概念。(redux 是 dva-core 的 peerDependencies)

依賴方向:

dva <- dva-core <- redux-saga & redux
複製代碼

redux的介紹,官網已經很詳盡。

首先,redux 是一個很輕量的可預測的狀態管理庫。API 也只有 createStore、store.subscribe、 store.dispatch、 store.getState。簡單易於理解。

createStore(reducer)建立單一狀態樹,同時配置一個函數簽名爲 (state, action) => state 的純函數 reducer,用於描述不一樣的 action 應如何操做更新 state。 reducer 須要保持兩個原則,第一是保持純函數特性,第二是保持不可變數據,不修改上一次的 state,每次計算後返回新的 state。

那麼 store.subscribe 和 store.dispatch 分別爲狀態樹的訂閱和事件分發。而 store.getState 能夠獲取當前的狀態樹快照。

所以使用的基本概念上能夠理解爲,將全部應用的狀態 state 保存在單一的狀態樹 store 裏,經過純淨的 reducer 控制如何更新狀態,分發擁有具體描述和動做參數的 action 事件以應用對應的 reducer。而後通過 redux 處理後最終得以更新狀態樹,同時通知全部訂閱者。

若是說 redux 是能夠處理純函數狀況的話,那麼 redux-saga 則是對應用反作用的一種加強管理工具。redux-saga 是 redux 的中間件。

淺談 dva-core 的場景

筆者在半年之前,提起 redux 這些框架總會不自覺的與 react 相關聯,事實上真的這些框架脫離了 react 難道就沒法使用了麼?以及如何擴展使用這些框架達成咱們的應用需求呢?

然後來筆者因項目緣由不進行 web 應用開發,第一次嘗試開發業務底層相關 SDK 的時候,意識到雖然在使用 redux 上 react 是主要場景。可是 redux 做爲狀態管理庫,做用域理應更大。那麼基於 redux 衍生的 redux-saga、甚至於 dva(也就是今日的主題),那也能夠脫離 web 應用開發場景進行理解與使用。

dva-core 實際上是脫離了視圖限制的一個類庫(dva 依賴於 dva-core 實現了 model 層的管理)

那麼請同窗們細想一下,dva-core 其實相比 dva 擴展性更強,同時使用場景更廣。好比,若是是構建一個 SDK,純粹利用 dva-core 管理狀態和反作用,也何嘗不可。

同時 dva-core 封裝了對於 redux 和 redux-saga 的一種更友好的使用方式。基於理解了 dva-core 的狀況下,能夠經過鉤子加強、或者甚至本身 fork 下來進行加強。

所以,能夠基於 dva-core 封裝出適用於各類框架的狀態管理方案。

做爲一個底層類庫,dva-core 是比上層類庫 dva 更具備通用價值的。

同窗們能夠盡情發揮想象~

小結

文章最後仍是囉嗦的提了一下 redux 和 redux-saga。雖然本文的確在解析 dva 中的 dva-core 的代碼,可是若是想要真正熟悉dva-core,前兩者仍是要稍微熟悉一下的。

關於 dva-core 的代碼,筆者也是在業餘時間斷斷續續研讀了一兩週,源代碼解析類文章也是第一次嘗試,不必定講的清楚,可是以後確定會愈來愈好。

另外關於讀 dva-core 源代碼的一點點建議:

  1. redux 的源代碼必定要讀(早於 dva-core)
  2. 讀單元測試能夠幫助你理解 dva 的 api: dva/packages/dva-core/test
  3. 多寫點 demo,反覆驗證想法
  4. 學點函數式編程的知識和思想

此次的文章超乎想象的長,若是我發現有錯別字或者不穩當的地方,會持續修改。

謝謝你們。

相關文章
相關標籤/搜索