dva-原始碼解析-下

opt_bbt發表於2018-10-08

首先這是一篇找媽媽的故事, model中的state,reducers,effects,是如何找到各自的媽媽呢?23333~

dva 是對redux、react-redux、react-router、redux-saga的整合,所以在看dva原始碼之前建議先要熟悉這些庫的用法。 廢話不多說,那我們就開始。先看一下生成的 index.js 檔案,在這裡我加入了dva-loading,用來分析plugin。

import dva from 'dva';
import createLoading from 'dva-loading';

// 1. Initialize
const app = dva();

// 2. Plugins
app.use(createLoading());

// 3. Model
app.model(require('./models/example').default);

// 4. Router
app.router(require('./router').default);

// 5. Start
app.start('#root');
複製程式碼

可以看到,dva生成app例項,並且繫結plugin、models、router,最後啟動專案。

先去看一下dva的原始碼,位置是dva/src/index.js

export default function (opts = {}) {
  const history = opts.history || createHashHistory();
  const createOpts = {
    initialReducer: {
      routing,
    },
    setupMiddlewares(middlewares) {
      return [
        routerMiddleware(history),
        ...middlewares,
      ];
    },
    setupApp(app) {
      app._history = patchHistory(history);
    },
  };

  const app = core.create(opts, createOpts);
  const oldAppStart = app.start;
  app.router = router;
  app.start = start;
  return app;

  function router(router) {
    app._router = router;
  }

  function start(container) {
    // 允許 container 是字串,然後用 querySelector 找元素
    if (isString(container)) {
      container = document.querySelector(container);
    }

    if (!app._store) {
      oldAppStart.call(app);
    }
    const store = app._store;

    // If has container, render; else, return react component
    if (container) {
      render(container, store, app, app._router);
      app._plugin.apply('onHmr')(render.bind(null, container, store, app));
    } else {
      return getProvider(store, this, this._router);
    }
  }
}
複製程式碼

上面程式碼重點在兩句

  • const app = core.create(opts, createOpts); 利用dva-core生成資料層app
  • app.start = start; 由於dva-core僅僅是資料層,所以這裡需要代理start方法,建立view層,比如react、router

接下來我想會重點分析model方法(這是整個dva的核心部分) 位置是dva-core/index.js

export function create(hooksAndOpts = {}, createOpts = {}) {
  const { initialReducer, setupApp = noop } = createOpts;

  const plugin = new Plugin();
  plugin.use(filterHooks(hooksAndOpts));

  const app = {
    _models: [prefixNamespace({ ...dvaModel })],
    _store: null,
    _plugin: plugin,
    use: plugin.use.bind(plugin),
    model,
    start,
  };
  return app;

  // 註冊model,將model裡的reducers與effects方法增加namespace,並且儲存在app._models中
  function model(m) {
    if (process.env.NODE_ENV !== 'production') {
      checkModel(m, app._models);
    }
    const prefixedModel = prefixNamespace({ ...m });
    app._models.push(prefixedModel);
    return prefixedModel;
  }
}
複製程式碼

在一個專案中我們會註冊多個model,現在models都已經被存在了app_models中。dva是如何將每個model裡的state,reducers, effects, 放在redux與redux-saga中的呢?答案就在 app.start 裡。start裡主要是redux和redux-saga的初始化

const reducers = { ...initialReducer };
for (const m of app._models) {
  reducers[m.namespace] = getReducer(
    m.reducers,
    m.state,
    plugin._handleActions
  );
  if (m.effects)
    sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
}
複製程式碼

上面程式碼遍歷models分別將reducers和effects存下來,先看一下reducer的處理

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);
  }
}
複製程式碼

一般情況下都不會是陣列,話說看到這裡我才知道原來還可以加一個enhancer。在去看一下defaultHandleActions

function handleActions(handlers, defaultState) {
  const reducers = Object.keys(handlers).map(type =>
    handleAction(type, handlers[type])
  );
  const reducer = reduceReducers(...reducers);
  return (state = defaultState, action) => reducer(state, action);
}
複製程式碼

在這裡遍歷了reducers裡的每一個key值,並把一個models下的reduers合併為一個。

function handleAction(actionType, reducer = identify) {
  return (state, action) => {
    const { type } = action;
    invariant(type, 'dispatch: action should be a plain Object with type');
    if (actionType === type) {
      return reducer(state, action);
    }
    return state;
  };
}
複製程式碼

關鍵在於 if (actionType === type), 判斷型別不同就返回state,類似於 switch...case。 下面看一下合併reducer的函式

function reduceReducers(...reducers) {
  return (previous, current) =>
    reducers.reduce((p, r) => r(p, current), previous);
}
複製程式碼

這裡的合併方式我是有點不太理解的, 這裡採用的是reducer1 -> reducer2 -> reducer3,假如有一個action,models裡的handleAction要全跑一遍。 為什麼不直接找到相應鍵值的reducer執行呢?,reducer已經分析完了然後就是effects

export default function getSaga(effects, model, onError, onEffect) {
  return function*() {
    for (const key in effects) {
      if (Object.prototype.hasOwnProperty.call(effects, key)) {
        const watcher = getWatcher(key, effects[key], model, onError, onEffect);
        const task = yield sagaEffects.fork(watcher);
        yield sagaEffects.fork(function*() {
          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
          yield sagaEffects.cancel(task);
        });
      }
    }
  };
}
複製程式碼

fork每一個effect,然後是 getWatcher 函式

// onEffect就是plugin中的OnEffect, 就如開頭的那個例子dva-loading中就有一個onEffect
function getWatcher(key, _effect, model, onError, onEffect) {
  // 這裡給每一個effect增加了開始、結束的事件和錯誤處理,並且__dva_resolve, __dva_reject是PromiseMiddleware的引數。
  // PromiseMiddleware的作用是當你dispatch effect時,返回一個promise。this.props.dispatch.then這樣的寫法你一定熟悉。
  function* sagaWithCatch(...args) {
    const { __dva_resolve: resolve = noop, __dva_reject: reject = noop } =
      args.length > 0 ? args[0] : {};
    try {
      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
      // createEffects 封裝了redux-saga的effects,主要是改變了type值,這就是你為什麼可以在當前model裡省略namespace的原因
      const ret = yield effect(...args.concat(createEffects(model)));
      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });
      resolve(ret);
    } catch (e) {
      onError(e, {
        key,
        effectArgs: args,
      });
      if (!e._dontReject) {
        reject(e);
      }
    }
  }

  // 如果外掛中有onEffects,則再封裝一層
  const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);

  switch (type) {
    case 'watcher':
      return sagaWithCatch;
    case 'takeLatest':
      return function*() {
        yield takeLatest(key, sagaWithOnEffect);
      };
    case 'throttle':
      return function*() {
        yield throttle(ms, key, sagaWithOnEffect);
      };
    default:
      return function*() {
        yield takeEvery(key, sagaWithOnEffect);
      };
  }
}
複製程式碼

到目前為止我們已經處理好了reduer與effects,接下來就是redux與redux-saga的初始化工作了 首先是redux

const store = (app._store = createStore({
  // eslint-disable-line
  reducers: createReducer(),
  initialState: hooksAndOpts.initialState || {},
  plugin,
  createOpts,
  sagaMiddleware,
  promiseMiddleware,
}));
複製程式碼

redux-saga

sagas.forEach(sagaMiddleware.run);
複製程式碼

ola, dva的原始碼差不多就到這裡了,再去看一個dva-loading是如何工作的。 先想一下dva-loading作用是監聽effects的開始和結束,並把狀態存在store裡,那麼他肯定需要一個reducer用來儲存狀態,和effect用來丟擲action

const initialState = {
    global: false,
    models: {},
    effects: {},
};

const extraReducers = {
  [namespace](state = initialState, { type, payload }) {
    const { namespace, actionType } = payload || {};
    let ret;
    switch (type) {
      case SHOW:
        ret = {
          ...state,
          global: true,
          models: { ...state.models, [namespace]: true },
          effects: { ...state.effects, [actionType]: true },
        };
        break;
      case HIDE: // eslint-disable-line
        res = {...}
        break;
      default:
        ret = state;
        break;
    }
    return ret;
  },
};

function onEffect(effect, { put }, model, actionType) {
  const { namespace } = model;
  if (
      (only.length === 0 && except.length === 0)
      || (only.length > 0 && only.indexOf(actionType) !== -1)
      || (except.length > 0 && except.indexOf(actionType) === -1)
  ) {
      return function*(...args) {
          yield put({ type: SHOW, payload: { namespace, actionType } });
          yield effect(...args);
          yield put({ type: HIDE, payload: { namespace, actionType } });
      };
  } else {
      return effect;
  }
}
複製程式碼

onEffect在之前getWatcher中已經講過,而extraReducers最後會通過combineReducers合併

總結

dva的原始碼已經分析完了,看完這篇文章相信你已經對dva的工作方式有了大致的瞭解。下期我會分享react原始碼。

相關文章