dva源碼解析(一)

寫在前面

dva是螞蟻金服推出的一個單頁應用框架,對reduxreact-routerredux-saga進行了上層封裝,沒有引入新的概念,可是極大的程度上提高了開發效率;下面內容爲本人理解,若有錯誤,還請指出,不勝感激。react

redux的痛苦

redux的優勢不少,痛點也有,好比異步控制,redux-saga的出現使得異步操做變得優雅,可是基於redux-saga不得不認可的一點就是開發過程實在是太麻煩了,倘若增長一個操做,不得不操做actionsreducerssagas,對於sagas能夠還須要進行watch,然後還要進行fork;(PS: 原本就夠麻煩了,再加上一個sagas);在添加一個操做時,不得不操做這麼多的文件,實在是麻煩,而dva的出如今必定程度上解決了這個問題。redux

dva基本概念

未使用dva下的目錄常常是這樣的:api

actions
   --/ user.js
   --/ team.js
reducers
   --/ user.js
   --/ team.js
sagas/
   --/ user.js
   --/ team.js

dva將其合併:數組

models
   --/ user.js
   --/ team.js

dva中有着幾個概念:react-router

namespace       =>  combineReducers中對應的key值
state           =>  對應初始的state,也就是initialState
effects         =>  saga的處理函數
reducers        =>  對應reducers,不一樣的是,寫法上將switch...case轉化爲對象

除了這些之外,dva中還有subscriptions,這一律念來源於elmapp

dva的實現

初始化

const app = dva({
    history: browserHistory
});

上面的過程發生了什麼?
dva本質上調用了下面函數:框架

function dva(hooks = {}) {
    const history = hooks.history || defaultHistory;
    const initialState = hooks.initialState || {};
    delete hooks.history;
    delete hooks.initialState;

    const plugin = new Plugin();
    plugin.use(hooks);

    const app = {
      // properties
      _models: [],
      _router: null,
      _store: null,
      _history: null,
      _plugin: plugin,
      _getProvider: null,
      // methods
      use,
      model,
      router,
      start,
    };
    return app;
}

hooks爲傳入的一些配置,例如能夠經過傳入history來改變路由的實現,dva默認採用的是hashHistory;從這裏能夠看出dva暴露出來的方法:dom

  • app.router():指定路由,須要傳入一個函數,通常相似於({ history }) => (<Router>...</Router>)異步

  • app.use():添加插件,這個稍後來看~ide

  • app.model():添加model,也就是對應的添加一個store下的數據,該方法作的就是對傳入的model進行檢查,對reducers添加命名空間,然後將其push_models中。

    • namespace必須且惟一,由於內置了react-redux-router,因此namespace也不能爲routing

    • subscriptionseffects均爲可選參數,傳入的話必須爲對象

    • reducers爲可選,支持對象和數組兩種傳入方式(傳入數組的方式,每每伴隨着高階reducer的應用,具體稍後再看~)

  • app.start():初始化應用,接受參數爲選擇器或者DOM節點

須要注意的是:

  • reducerseffectskey不須要用namespace/action的形式了,由於dva會自動將其加上,dispatch的時候,saga須要加上namespace,而saga中的put不須要加入namespace,緣由是dvaput進行了重載

  • dva同時支持rn應用,引入dva/mobile便可,這時react-router不在須要,利用rn中的Navigator便可,不會引用react-routerreact-redux-routernamespace能夠命名爲routing;正是因爲這點差別,做者將路由相關的內容做爲參數傳入了進去,具體能夠參見這個文件。

建立

將一些配置項初始化好後,就能夠app.start就是來建立一個應用,下面就一點點的看看start的過程(如下基於默認狀況,也就是使用了react-router):

  • 參數校驗,是否爲DOM元素或者檢查是否能夠根據傳入的選擇器字符串找到對應的DOM,這個DOM對應的就是ReactDOM.render的第二個參數。

  • 錯誤處理,使得發生錯誤時,不至於應用奔潰,固然須要傳入自定義hooks.onError來處理:

// 傳入hooks.onError則調用,反之調用默認函數處理,拋出異常便可
  const onError = plugin.apply('onError', (err) => {
    throw new Error(err.stack || err);
  });
  // 目的是出現錯誤時,也能夠進行dispatch操做
  const onErrorWrapper = (err) => {
    if (err) {
      if (typeof err === 'string') err = new Error(err);
      onError(err, app._store.dispatch);
    }
  };
  • 遍歷_models,初始化reducers,sagas

const sagas = [];
// initalReducer爲{ routing: routerReducer }
const reducers = { ...initialReducer };  // 爲rootReducer
for (const m of this._models) {
    // 獲得默認的state
    reducers[m.namespace] = getReducer(m.reducers, m.state);
    if (m.effects) sagas.push(getSaga(m.effects, m, onErrorWrapper));
}

處理reducers

對於reduxreducers最多見的是基於switch..case的,而dva作出了一些改變,將每個case分支變做了一個函數:

clipboard.png

(PS: 本人認爲,這個能夠塊能夠更改,利用some操做來儘量少的調用無心義的reducer,因而我提了一個pr)

每個reducer的實現以下:

// actionType對應的是dva的reducers中的key值
(state, action) => {
    const { type } = action;
    if (type && actionType !== type) {
        return state;
    }
    return reducer(state, action);
};

處理sagas

看完了對於reducers的處理,下面來看一下對於sagas的處理:

function getSaga(effects, model, onError) {
  return function *() {
    for (const key in effects) {
      if (Object.prototype.hasOwnProperty.call(effects, key)) {
        const watcher = getWatcher(key, effects[key], model, onError);
        const task = yield sagaEffects.fork(watcher);
        // 爲了移除時能夠將saga任務註銷
        yield sagaEffects.fork(function *() {
          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
          yield sagaEffects.cancel(task);
        });
      }
    }
  };
}

getWatcher返回一個saga監聽函數,也就是一般寫的watchXXXmodel.effects[key]能夠是一個任務函數;也能夠是個數組,第一個參數爲任務函數,第二爲配置對象,能夠傳入typetype有4個可選值,takeEvery(默認),takeLatestthrottlewatcher四種,dvaeffects作了一個錯誤處理:

effect => function *(...args) {
  try {
    yield effect(...args.concat(createEffects(model)));
  } catch (e) {
    onError(e);   // 爲以前的onErrorWrapper
  }
}

注意:

  • watcher是指傳入的任務函數就是一個watcher直接fork就好

  • throttle還要傳入一個ms配置,這個ms表明着在多少毫秒內只觸發一次同一類型saga任務,而takeEvery是不會限制同一類型執行次數,takeLatest只能執行一個同一類型任務,有執行中的再次執行就會取消

  • getSaga能夠看出,${namespace}/@@CANCEL_EFFECTS能夠取消對應的任務監聽

  • 能夠經過配置hooks.onEffect來增長sagawatcher

加強redux

  • redux中間件,由sagaMiddwarerouterMiddware(啓用react-router時),hooks.onAction傳入的其它中間件,如redux-logger

  • 其它加強,如redux-devtools,內置了redux-devtools,另需的話在hooks.extraReducers傳入

const enhancers = [
    applyMiddleware(...middlewares),
    devtools(),
    ...extraEnhancers,
  ];
  const store = this._store = createStore(  // eslint-disable-line
    createReducer(),
    initialState,
    compose(...enhancers),
  );

設置redux的回調函數

經過配置hooks.onStateChange能夠指定reduxstate改變後所觸發的回調函數:

const listeners = plugin.get('onStateChange');
  for (const listener of listeners) {
    store.subscribe(() => {
      listener(store.getState());
    });
  }
}

新概念subscriptions

subscriptions是一個新概念,會在dom ready以後執行,在這裏面能夠作一些基礎數據的獲取:
通常會將初始數據的獲取放在react的生命週期中,好比componentWillMount,可是假設咱們作了代碼分割,實現了按需加載,那麼咱們開始獲取數據的時間爲:獲取相應的js+解析js+執行react生命週期,可是redux的數據加載和ui組件沒有太大關係,能夠將數據獲取的時間點提早,subscriptions提供瞭解決方法,其意義爲訂閱,對於上面的場景,咱們能夠訂閱路由,到了執行的路由執行相應的dispatch(),如:

setup({ dispatch, history }) {
  return history.listen(({ pathname, query }) => {
    if (pathname === '/users') {
      dispatch({ type: 'fetch', payload: query });
    }
  });
}

(PS: 對於這個新概念,我也不是很清楚,後面的文章會有專門的描述,你們先有一個概念就好)

掛載

上述過程均爲初始化的過程,就是獲取到須要的reducerssagas以及對於一些中間件和插件的配置,下面要進行的就是掛載了,也就熟悉的render(<Provider>, container)

動態處理model

dva.modeldva.unmodel,封裝了在運行時的store進行一類增長和刪除的操做,例如能夠再切換到某一路由時動態的加入一個model(我的猜想,熱更新頗有可能也利用了這個兩個apihooks.onHmr)。

未完結

關於redux還有一個利器,那就是高階reduce,固然在dva中也有體現,這篇文章已經很長了,這些內容留在下一篇中介紹。以上是本人對於dva的粗略的理解,內容若有錯誤,還請你們指出。dva的確簡化了開發的流程,並且在螞蟻金服的不少業務線也有着應用,是一個很值得你們一試!

相關文章
相關標籤/搜索