dva源碼分析

本文同步在我的博客shymean.com上,歡迎關注react

dva是基於react的二次封裝,在工做中常常用到,所以有必要了解一下其中的實現。本文將從源碼層面分析dva是如何將reduxredux-sagareact-router等進行封裝的。git

本文使用的dva版本爲2.6.0-beta.20,參考github

dva構建流程

下面是一個簡單的dva應用json

// 初始化應用
const app = dva({
    onError(error) {
        console.log(error)
    }
})
app.use(createLoading()) // 使用插件
app.model(model) // 註冊model
app.router(App) // 註冊路由
app.start('#app') // 渲染應用
複製代碼

咱們從這幾個api開始,瞭解dva是如何封裝react應用的。redux

整個dva項目使用lerna管理的,在每一個package的package.json中找到模塊對應的入口文件,而後查看對應源碼。api

dva start

// dva/src/index.js
export default function(opts = {}) {
  const history = opts.history || createHashHistory();
  const createOpts = {
    initialReducer: {
      router: connectRouter(history),
    },
    setupMiddlewares(middlewares) {
      return [routerMiddleware(history), ...middlewares];
    },
    setupApp(app) {
      app._history = patchHistory(history);
    },
  };

  const app = create(opts, createOpts);
  const oldAppStart = app.start;
  app.router = router; 
  app.start = start;
  return app;
  // 爲app暴露的router接口,主要是更新app._router 
  function router(router) {
    app._router = router;
  }
  // 爲app暴露的start接口,主要渲染整個應用
  function start(container) {
    // 根據container找到對應的DOM節點
    if (!app._store) oldAppStart.call(app); // 執行本來的app.start
    const store = app._store;

    // 爲HMR暴露_getProvider接口
    app._getProvider = getProvider.bind(null, store, app);
    // 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);
    }
  }
}

// 渲染邏輯
function getProvider(store, app, router) {
  const DvaRoot = extraProps => (
    // app.router傳入的組件在此處被渲染,並傳入app,history等prop
    <Provider store={store}>{router({ app, history: app._history, ...extraProps })}</Provider>
  );
  return DvaRoot;
}
function render(container, store, app, router) {
  const ReactDOM = require('react-dom');
  ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}
複製代碼

能夠看見app是經過create(opts, createOpts)進行初始化的,其中opts是暴露給使用者的配置,createOpts是暴露給開發者的配置,真實的create方法在dva-core中實現。數組

dva-core craete

下面是create方法的大體實現promise

// dva-core/src/index.js
const dvaModel = {
  namespace: '@@dva',
  state: 0,
  reducers: {
    UPDATE(state) {
      return state + 1;
    },
  },
};
export function create(hooksAndOpts = {}, createOpts = {}) {
  const { initialReducer, setupApp = noop } = createOpts; // 在dva/index.js中構造了createOpts對象
  const plugin = new Plugin(); // dva-core中的插件機制,每一個實例化的dva對象都包含一個plugin對象
  plugin.use(filterHooks(hooksAndOpts)); // 將dva(opts)構造參數opts上與hooks相關的屬性轉換成一個插件

  const app = {
    _models: [prefixNamespace({ ...dvaModel })],
    _store: null,
    _plugin: plugin,
    use: plugin.use.bind(plugin), // 暴露的use方法,方便編寫自定義插件
    model, // 暴露的model方法,用於註冊model
    start, // 本來的start方法,在應用渲染到DOM節點時經過oldStart調用
  };
  return app;
}
複製代碼

能夠看見上面代碼主要暴露了usemodelstart這三個接口。react-router

插件系統

dva實現高度依賴內置的Plugin系統,爲了便於理解後續代碼,咱們暫時跳出start方法,去探究Plugin原理。app

dva的插件時一個包含下面部分屬性的JS對象,每一個屬性對應一個函數或配置項

// plugin對象只暴露了下面這幾個屬性,顧名思義,以on開頭的方法會在整個應用的某些時刻觸發,相似於生命週期鉤子函數
const hooks = [
  'onError',
  'onStateChange',
  'onAction',
  'onHmr',
  'onReducer',
  'onEffect',
  'extraReducers',
  'extraEnhancers',
  '_handleActions',
];
複製代碼

一個大概的插件結構相似於

const testPlugin = {
  onStateChange(newState){
    console.log(newState)
  }
  // ...其餘須要監聽的hook
}
app.use(testPlugin)
複製代碼

來看看在start初始化時構造的Plugin管理器

// Plugin管理對象
class Plugin {
  constructor() {
    this._handleActions = null;
    // 每一個鉤子對應的處理函數默認都是空數組
    this.hooks = hooks.reduce((memo, key) => {
      memo[key] = [];
      return memo;
    }, {});
  }
  // 接收一個plugin對象
  use(plugin) {
    const { hooks } = this;
    for (const key in plugin) {
      if (Object.prototype.hasOwnProperty.call(plugin, key)) {
        // ...檢測use的plugin上的key,並處理特定形式的key
        if (key === '_handleActions') {
          this._handleActions = plugin[key]; // 若是定義了插件的_handleActions方法,會覆蓋Plugin對象自己的_handleActions
        } else if (key === 'extraEnhancers') {
          hooks[key] = plugin[key]; // 若是定義了插件的extraEnhancers方法,會覆蓋this.hooks自己的extraEnhancers
        } else {
          hooks[key].push(plugin[key]); // 其餘key對應的方法會追加到hooks對應key數組中
        }
      }
    }
  }
  // 返回一個經過key執行hooks上註冊的全部方法的函數
  apply(key, defaultHandler) {
    const { hooks } = this;
    const fns = hooks[key];
    // 遍歷fns並以此執行
    return (...args) => {};
  }

  // 根據key找到hooks上對應的方法
  get(key) {
    const { hooks } = this;
    if (key === 'extraReducers') {
      // 將hooks[key]都合併到一個對象中,如將[{ x: 1 }, { y: 2 }]轉換爲{x:1,y:2}的對象
      return getExtraReducers(hooks[key]);
    } else if (key === 'onReducer') {
      // 返回一個接受reducer並依次運行hooks.onReducer的函數,每次循環都會將前一個hooks.onReducer元素方法返回值做爲新的reducer
      return getOnReducer(hooks[key]);
    } else {
      return hooks[key];
    }
  }
}
function getOnReducer(hook) {
  return function(reducer) {
    for (const reducerEnhancer of hook) {
      reducer = reducerEnhancer(reducer);
    }
    return reducer;
  };
}
複製代碼

能夠看見,整個插件系統的實現仍是比較簡單的,主要暴露了use註冊插件、get根據key獲取插件等接口。

app.use = plugin.use.bind(plugin)
複製代碼

初始化Store

回到前面章節,繼續查看start方法的執行流程,咱們能夠看見完成的store構建流程

// oldStart方法
function start() {
  // 全局錯誤處理函數,若是某個插件註冊了onError鉤子,將在此處經過plugin.apply('onError')調用
  const onError = (err, extension) => {};
  // 遍歷app._models,收集saga
  app._getSaga = getSaga.bind(null);
  const sagas = [];
  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'), hooksAndOpts));
    }
  }
  // 建立 app._store,具體參看下面章節的createStore
	// app._store = craeteStore()... 
  const store = app._store;

  // Extend store
  store.runSaga = sagaMiddleware.run;
  store.asyncReducers = {};

  // Execute listeners when state is changed
  // 獲取全部監聽了onStateChange的插件,經過store.subscribe註冊到redux中
  const listeners = plugin.get('onStateChange');
  for (const listener of listeners) {
    store.subscribe(() => {
      listener(store.getState());
    });
  }

  // 運行全部sagas
  sagas.forEach(sagaMiddleware.run);
  setupApp(app);
  // 運行全部model的subscriptions方法
  const unlisteners = {};
  for (const model of this._models) {
    if (model.subscriptions) {
      unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
    }
  }

  // 在執行start時註冊model、unmodel和replaceModel三個接口,具體參看下面章節
  // ...
}
複製代碼

可見在整個start方法中

  • 遍歷app._models,收集並運行sagas
  • 初始化app._store,使用store.subscribe註冊全部插件的onStateChange回調
  • 運行全部的model.subscriptions
  • 暴露app.modelapp.unmodelapp.replaceModel三個接口

createStore

createStore方法是封裝的redux createStore,傳入了一些中間件和reducers

const sagaMiddleware = createSagaMiddleware(); // 初始化saga中間件
const promiseMiddleware = createPromiseMiddleware(app);
app._store = createStore({
  reducers: createReducer(),
  initialState: hooksAndOpts.initialState || {},
  plugin,
  createOpts,
  sagaMiddleware,
  promiseMiddleware,
});

function createStore({ reducers, initialState, plugin, sagaMiddleware, promiseMiddleware, createOpts: { setupMiddlewares = returnSelf }, }) {
  // extra enhancers
  const extraEnhancers = plugin.get('extraEnhancers');
  const extraMiddlewares = plugin.get('onAction');
  const middlewares = setupMiddlewares([
    promiseMiddleware,
    sagaMiddleware,
    ...flatten(extraMiddlewares),
  ]);

  const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];
  // redux.createStore
  return createStore(reducers, initialState, compose(...enhancers));
}
複製代碼

其中的reducers參數由createReducer方法構建,該方法返回一個加強版的reducer

const reducerEnhancer = plugin.get('onReducer'); // 在插件系統中提到,傳入onReducer時實際返回全部插件組合後的onReducer方法
const extraReducers = plugin.get('extraReducers'); // 傳入extraReducers實際返回的是全部插件合併後的reducerObj對象
function createReducer() {
    return reducerEnhancer(
    // redux.combineReducers
    combineReducers({
        ...reducers, // start Opts參數上的默認reducer
        ...extraReducers,
        ...(app._store ? app._store.asyncReducers : {}),
    }),
    );
}
複製代碼

其中promiseMiddleware中間件由createPromiseMiddleware建立,是一個當isEffect(action.type)時返回Promise的中間件,並在本來的action上掛載該Promise的{__dva_resolve: resolve, __dva_reject: reject}

export default function createPromiseMiddleware(app) {
  return () => next => action => {
    const { type } = action;
    // isEffect的實現爲:從app._models中找到namespace與action.type.split('/')[0]相同的model,而後根據model.effects[type]是否存在判斷當前action是否須要返回Promise
    if (isEffect(type)) {
      return new Promise((resolve, reject) => {
        next({
          __dva_resolve: resolve,
          __dva_reject: reject,
          ...action,
        });
      });
    } else {
      return next(action);
    }
  };
}
複製代碼

model三接口

一個model大概是下面的結構

{
    namespace: 'index_model',
    state: {
        text: 'hello'
    },
    effects: {
        * asyncGetInfo({payload = {}}, {call, put}) {
        }
    },
    reducers: {
        updateData: (state, {payload}) => {
            return {...state, ...payload}
        }
    }
}
複製代碼

create方法中,app.model方法實現以下,其本質是

function model(m) {
  // 將model上的reducers和effects各個key處理爲`${namespace}${NAMESPACE_SEP}${key}`形式
  const prefixedModel = prefixNamespace({ ...m });
  app._models.push(prefixedModel);
  return prefixedModel;
}
複製代碼

在執行start方法後,會覆蓋app.model方法爲injectModel,並新增unmodelreplaceModel

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

// 動態添加model,這樣就能夠實現model與route組件相似的異步加載方案
function injectModel(createReducer, onError, unlisteners, m) {
  m = model(m);

  const store = app._store;
  store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
  // 從新調用createReducer
  store.replaceReducer(createReducer());
  if (m.effects) {
    store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
  }
  // 更新unlisteners方便後面移除
  if (m.subscriptions) {
    unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);
  }
}
// 根據namespace移除對應model,須要移除store.asyncReducers並調用store.replaceReducer(createReducer())從新生成reducer
function unmodel(createReducer, reducers, unlisteners, namespace) {}
// 找到namespace相同的model並替換,若是不存在,則直接添加
function replaceModel(createReducer, reducers, unlisteners, onError, m) {}
複製代碼

經過此處瞭解到,dva中的model分爲兩類

  • 在調用start以前註冊的model
  • 在調用start以後動態註冊的model

路由

在前面的dva.start方法中咱們看到了createOpts,並瞭解到在dva-corestart中的不一樣時機調用了對應方法

import * as routerRedux from 'connected-react-router';
const { connectRouter, routerMiddleware } = routerRedux;

const createOpts = {
  initialReducer: {
    router: connectRouter(history),
  },
  setupMiddlewares(middlewares) {
    return [routerMiddleware(history), ...middlewares];
  },
  setupApp(app) {
    app._history = patchHistory(history);
  },
};
複製代碼

其中initialReducersetupMiddlewares在初始化store時調用,而後才調用setupApp

能夠看見針對router相關的reducer和中間件配置,其中connectRouterrouterMiddleware均使用了connected-react-router這個庫,其主要思路是:把路由跳轉也當作了一種特殊的action。

首先來看看connectRouter方法的實現,該方法會返回關於router的reducer

// connected-react-router/src/reducer.js
export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'
const createConnectRouter = (structure) => {
  const { fromJS, merge } = structure // 複製和合並JS對象的兩個工具方法
  const createRouterReducer = (history) => {
    const initialRouterState = fromJS({
      location: injectQuery(history.location),
      action: history.action,
    })
    return (state = initialRouterState, { type, payload } = {}) => {
      if (type === LOCATION_CHANGE) {
        const { location, action, isFirstRendering } = payload
        // 首次渲染時不更新state
        return isFirstRendering
          ? state
          : merge(state, { location: fromJS(injectQuery(location)), action })
      }
      return state
    }
  }

  return createRouterReducer
}
複製代碼

而後是routerMiddleware,該方法返回關於router的中間件

export const CALL_HISTORY_METHOD = '@@router/CALL_HISTORY_METHOD'
const routerMiddleware = (history) => store => next => action => {
  if (action.type !== CALL_HISTORY_METHOD) {
    return next(action)
  }
  // 當action爲路由跳轉時,調用history方法而不是執行next
  const { payload: { method, args } } = action
  history[method](...args)
}
複製代碼

最後回到dva中,查看patchHistory方法

function patchHistory(history) {
  // 劫持了history.listen方法
  const oldListen = history.listen;
  history.listen = callback => {
    const cbStr = callback.toString();
    // 當使用了dva.routerRedux.ConnectedRouter路由組件時,其構造函數會執行history.listen,此時isConnectedRouterHandler將爲true
    const isConnectedRouterHandler =
      (callback.name === 'handleLocationChange' && cbStr.indexOf('onLocationChanged') > -1) ||
      (cbStr.indexOf('.inTimeTravelling') > -1 && cbStr.indexOf('arguments[2]') > -1);
    // 在app.start以後的其餘地方如model.subscriptions中使用history時,會接收到location和action兩個參數
    callback(history.location, history.action);
    return oldListen.call(history, (...args) => {
      if (isConnectedRouterHandler) {
        callback(...args);
      } else {
        // Delay all listeners besides ConnectedRouter
        setTimeout(() => {
          callback(...args);
        });
      }
    });
  };
  return history;
}
複製代碼

能夠看見,dva對router並未作過多封裝,只是經過connected-react-router提供了一個reducer和一箇中間件,並將該庫暴露爲routerRedux

小結

從源碼能夠看見,dva主要是對reduxredux-saga進行封裝,簡化並對外暴露了幾個有限接口。除此以外,還內置了react-router和其餘一些庫,所以也能夠看作是一個輕量級應用框架

  • dva主要對外提供了相關的api
  • dva-corereduxredux-saga進行封裝,並實現了一個簡單的插件系統

學習dva源碼的一個好處可讓你去了解整個React的生態,並學會使用一種還不錯的開發方案。瞭解dva的封裝實現只是學習的第一步,要想編寫高效可維護的代碼,還須要深刻reduxredux-sagareact-router等庫的使用和實現。

相關文章
相關標籤/搜索