dva源碼解析

一 dva的源碼結構

dva的源碼分爲dva,和dva-core兩個部分,dva/src下的文件負責處理對外的邏輯,包括數據校驗,app對象封裝,配置用戶傳入的路由等參數,並在最後啓動了一個react應用。
dva-core的部分是dva核心功能實現部分,通過create方法返回一個dva對象給dva/src。使用內部的start方法初始化各種被dva/src配置過的屬性。

二 dva/src/index.js

1. export的函數的功能

export的函數返回了一個app對象,這個對象掛載了dva所有的屬性和方法,其中start方法是應用啓動方法。

2. 初始化配置

在導出函數開始,根據用戶傳入的配置初始化默認的dva配置,包括默認的HashHistory,react-router-redux提供的routerReducerrouterMiddleware(在dva-core裏面整合reducer的時候有用到),定義setupApp方法,用於代理history屬性到app上。

// history庫提供,將HTML5 BOM提供的History API進行封裝
const history = opts.history || createHashHistory();
const createOpts = {
  initialReducer: {
    routing,
  },
  
  setupMiddlewares(middlewares) {
    return [
      routerMiddleware(history),
      ...middlewares,
    ];
  },
  
  // 在dva-core/index: start方法中調用
  // patchHistroy(history)可以監聽history變動,從而觸發回調
  setupApp(app) {
    app._history = patchHistory(history);
  },
};

function patchHistory(history) {
  const oldListen = history.listen;
  history.listen = (callback) => {
    callback(history.location);
    return oldListen.call(history, callback);
  };
  return history;
}

3. start方法

(1)數據校驗

start方法在內部校驗了數據,主要有以下幾個:

  • 1)檢查傳入的container參數,接受dom元素或者是字符串,用querySelector方法查找dom元素,並判斷container是否是HTMLElement元素,依據是否存在nodeNamenodeType屬性。
  • 2)檢查是否在之前註冊過router,並且要求router是一個函數
(2)調用dva-core的start方法

之後,dva/src下面的start方法調用了dva/core返回的對象的start方法,並且由於要提供用戶配置的參數,將dva/src的app對象的this通過call方法傳入。
在這個start方法中,dva初始化了store,reducer,model等等,將這些對象全部掛載到了dva/src傳入的this下的app對象,也就是最終dva返回的對象。

(3)配置Provider

之所以我們使用dva,可以使用Providerconnect方法,而不需要在根組件上做配置,是因爲,在這一步,dva已經集成了react-redux插件的provider組件。

// app是dva對象,history默認是用history庫的createHashHistory生成的,
// 將BOM的History封裝後的history對象。
const DvaRoot = extraProps => (
    <Provider store={store}>
      { router({ app, history: app._history, ...extraProps }) }
    </Provider>
  );
(4)渲染react組件

Provider組件包裹後的JSX對象比傳遞給了react,用於渲染。

三 dva-core/index

1. create方法

create方法 是被默認導出的。create方法內部創造了一個app對象並掛載了_models,包含dvaModel和所有的用戶model,_store默認爲null,plugin屬性掛載各種插件,這些插件是基於dva生命週期函數的,plugin.use方法被plugin對象代理,以免找錯this,model方法用於註冊model,start方法用於啓動程序。最終create方法返回了這個app對象。

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

2. plugin對象

在create方法中,首先創建了Plugin對象,並用filterHooks方法過濾掉不合法的插件。plugin對象負責管理和使用中間件。

// hooks是所有生命週期鉤子函數的鍵名
const hooks = [];

// filterHooks函數過濾了所有的生命週期鉤子
export function filterHooks(obj) {}

export default class Plugin {
  constructor() {
    // 將this.hooks初始化爲一個對象,
  }
  // 中間件,即是使用plugin.use()方法註冊
  use(plugin) {}
  // 全局錯誤處理函數
  apply(key, defaultHandler) {}
  // 獲取生命週期處理函數
  get(key) {}
}

// 將某一個key對應的生命週期函數的回調函數數組組合起來,生成一個對象
function getExtraReducers(hook) {}

// 將用戶定義的reducer經過每一個reducer中間件處理
function getOnReducer(hook) {}

在plugin對象中,一共做了這麼幾件事:

  • (1)定義一個hooks生命週期鉤子函數數組,並按照這個數組,初始化一個hooks對象,每一個屬性是一個生命週期函數,被初始化爲一個數組,掛載用戶註冊的插件。實際上dva的中間件或者說插件就是在這裏被使用。
const hooks = [ 
  'onError', 'onStateChange', 'onAction', 'onHmr', 'onReducer', 'onEffect', 
  'extraReducers', 'extraEnhancers', '_handleActions',
];
  • (2)在Plugin的構造函數中,使用reduce方法創建hooks對象
// 將this.hooks初始化爲一個對象,
// 它包括上面hooks數組的所有元素作爲屬性名,每個值都是一個空數組
// 這個空數組用來存放用戶註冊的中間件
this.hooks = hooks.reduce((memo, key) => {
  memo[key] = [];
  return memo;
}, {});
  • (3)plugin.use()方法用來註冊中間件,註冊的過程只是將用戶定義的函數push進對應的鉤子的數組中。
use(plugin) {
  // 數據校驗,要求plugin是一個純對象
  const hooks = this.hooks;
  for (const key in plugin) {
    // 經官網文檔提示,使用Object.prototype.hasOwnProperty,
    //是爲了防止用戶在plugin對象上
    // 自定義hasOwnProperty覆蓋掉原函數,進行一些不恰當的操作
    // 保持對外界數據的不完全信任,是一個很好的習慣
    if (Object.prototype.hasOwnProperty.call(plugin, key)) {
      // 數據校驗,要求傳入的key,即生命週期函數名是存在的
      // 兩種情況特殊處理
      if (key === '_handleActions') {
        this._handleActions = plugin[key];
      } else if (key === 'extraEnhancers') {
        hooks[key] = plugin[key];
      } else {
        // 給生命週期鉤子添加中間件
        // 也包括onReducers
        hooks[key].push(plugin[key]);
      }
    }
  }
}
  • (4)定義一個get方法用於獲取對應鉤子的中間件。這裏做了判斷,如果key爲extraReducers,則將hook上掛載的所有函數組成一個對象返回,如果是onReducer,就返回一個函數,這個函數將傳入的reducer用對應鉤子的所有中間件處理一遍並返回。
// 將某一個key對應的生命週期函數的回調函數數組組合起來,生成一個對象
function getExtraReducers(hook) {
  let ret = {};
  for (const reducerObj of hook) {
    ret = { ...ret, ...reducerObj };
  }
  return ret;
}
// 這裏很鮮明的體現了redux中對reducer命名的來源
// 這裏將用戶定義的reducer經過每一個reducer中間件處理
// 之所以可以這樣處理,是因爲,redux原則上規定,reducer是一個純函數,
// 即,不對入參做任何更改
// 沒有副作用,因此,就可以像數組的reduce方法一樣使用
// 最終返回一個經過所有中間件處理的reducer函數
function getOnReducer(hook) {
  return function(reducer) {
    for (const reducerEnhancer of hook) {
      reducer = reducerEnhancer(reducer);
    }
    return reducer;
  };
}
  • (5)Plugin.js文件最後導出了另一個函數,filterHooks用於過濾不合法的hooks。
// filterHooks函數過濾了所有的生命週期鉤子
// 它使用數組的reduce方法,拿到hooks數組的所有元素
// 作爲生命週期鉤子函數對象的屬性名,用空對象作爲屬性值
// reduce方法是將數組每一個元素“加和”並返回最終的一個結果
// 這裏使用reduce方法遍歷obj,
//將上一次添加初始化屬性後的對象傳遞給下一個元素,
// 因此這個被遍歷的對象會逐漸增加屬性,最後將obj過濾爲只包括hooks的對象

export function filterHooks(obj) {
  return Object.keys(obj).reduce((memo, key) => {
    if (hooks.indexOf(key) > -1) {
      memo[key] = obj[key];
    }
    return memo;
  }, {});
}

3. model

(1)註冊model

index中使用model方法註冊model

// 像app中添加model,
// 添加前先用checkModel方法校驗model是否符合格式
// prefixNamespace方法給model加前綴,返回加了前綴的model對象
function model(m) {
  if (process.env.NODE_ENV !== 'production') {
    checkModel(m, app._models);
  }
  const prefixedModel = prefixNamespace({ ...m });
  app._models.push(prefixedModel);
  return prefixedModel;
}
(2)檢查model

checkModel檢查model的合法性。一個合法的model有以下幾個要求:

  • 1)namespace必須被定義爲字符串,不可爲空,不可重複
  • 2)reducers是一個數組或一個純對象,如果reducers是一個數組,那麼第一個元素必須是一個對象,第二個要求是一個函數,這在後面添加前綴會用上。
  • 3)effects必須是一個純對象
  • 4)subscriptions必須是一個純對象,每一個屬性必須是一個函數
(3)檢查並添加前綴

prefixedNamespace方法用於給model加上namespace前綴

// 檢查是否有加namespace前綴
// prefix通過Object.kes拿到所有的屬性的字段名數組
// 之後用reduce方法遍歷數組,返回加了前綴的reducer和effects對象,
function prefix(obj, namespace, type) {
  return Object.keys(obj).reduce((memo, key) => {
    const newKey = `${namespace}${NAMESPACE_SEP}${key}`;
    memo[newKey] = obj[key];
    return memo;
  }, {});
}

// 檢查reducer和effects是否加了namespace前綴
// 返回加上前綴的modal對象
// 因此在dva中才能通過dispatch一個帶有type屬性的對象的action,
// 來找到對應的reducer或effect,dva內部加了namespace前綴
export default function prefixNamespace(model) {
  const {
    namespace,
    reducers,
    effects,
  } = model;

  if (reducers) {
    if (isArray(reducers)) {
      model.reducers[0] = prefix(reducers[0], namespace, 'reducer');
    } else {
      model.reducers = prefix(reducers, namespace, 'reducer');
    }
  }
  if (effects) {
    model.effects = prefix(effects, namespace, 'effect');
  }
  return model;
}

至此,model已經合法了,中間件也已經註冊了,reducer和effects也已經有了調用的方式和依據。

3. start方法

start方法用於啓動程序,被dva/index的start代理。在start中做了這麼幾件事:

(1)createPromiseMiddleware()方法

createPromiseMiddleware()方法用於攔截type指向effect的action,並返回一個promise,封裝action(在後面的getSaga中有使用)

// dva-core/index: start()

// 攔截指向effects的action,
// 檢查action的type指向的方法是否屬於effects,如果是返回promise
const promiseMiddleware = createPromiseMiddleware(app);


// dva-core/createPromiseMiddleware.js
export default function createPromiseMiddleware(app) {

  // () => next => action => {} 是一箇中間件的標準寫法
  // 可以在return next(action)之前處理action
  // isEffect用於檢查action的type,如果指向effect,也就是異步方法
  // 則返回一個Promise對象,在action對象上掛載兩個Promise狀態方法
  
  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);
    }
  };
  
  // isEffect方法檢查type指向的modal裏的方法,
  // 如果方法屬於effects返回true
  function isEffect(type) {
    if (!type || typeof type !== 'string') return false;
    const [namespace] = type.split(NAMESPACE_SEP);
    const model = app._models.filter(m => m.namespace === namespace)[0];
    if (model) {
      if (model.effects && model.effects[type]) {
        return true;
      }
    }
    return false;
  }
}
(2)getSaga()方法

初始化_getSaga方法,這個函數借助redux-saga的能力來處理異步數據。getSaga方法返回一個generator函數,被push進了sagas數組中。這個函數給一個model的每個effect函數添加一個watcher,單獨開闢一個線程監聽action,執行effect,最後再開闢一個新的線程用於在必要的時候取消監聽。

// getSaga()方法接受四個參數
// 分別是model中的effects數組,model對象本身,
// 在dva-core/index裏面定義的全局錯誤處理函數onError
// 以及onEffect,它是在Plugin文件中定義的生命週期函數,
// 作爲調用effect時的回調函數數組
// 這裏的for循環結合dva-core/index: start方法裏面調用時的循環,
// 使得這些處理針對所有model的effect內定義的所有異步函數

export default function getSaga(effects, model, onError, onEffect) {
  return function*() {
    for (const key in effects) {
      if (Object.prototype.hasOwnProperty.call(effects, key)) {
      
        // getWatcher返回一個generator函數,
        // 調用takeEvery來實時監聽action和effect
        
        const watcher = getWatcher(key, effects[key], model, onError, onEffect);
        
        // fork方法接受一個generator函數或者返回promise對象的普通函數
        // 執行傳入的函數,以此開闢新的線程,創建一個分叉任務,
        // 在dva內,這裏完成了派發異步action自動執行對應effect的功能
        // 之後又創建一個分叉任務管理這個task對象,在必要時候卸載task
        
        const task = yield sagaEffects.fork(watcher);
        yield sagaEffects.fork(function*() {
          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
          yield sagaEffects.cancel(task);
        });
      }
    }
  };
}

getWatcher()方法返回一個generator函數,將effect函數添加一個watcher,用於監聽匹配的action。其內部做了以下幾件事(不考慮數組情況):

  • 1)定義sagaWidthCatch方法,這個方法內部調用用戶的effect方法。並在前後通知redux-saga。由於在之前已經將action封裝爲promise,因此在結束以後,調用promise相應的狀態方法。

  • 2)定義applyOnEffect()方法。這個方法將用戶提供的在onEffect生命週期處理函數註冊到effect上,不影響effect運行的同時,依次執行用戶添加的中間件。如果沒有插件。

  • 3)定義createEffects方法,封裝redux-saga提供的部分方法,添加數據校驗,主要是type的檢查。

  • 4)最後,用switch判斷用戶定義的代理類型,默認情況下,返回一個generator函數,用takeEvery(redux-saga提供)方法,監聽對應action,並在匹配到的時候自動創建一個異步任務,執行用戶定義的effect函數。

// 接受五個參數
// 分別是遍歷過程中選中的key,即函數名,和函數本身,以及model對象
// 最後是全局錯誤處理函數,和觸發effect時的回調,
// 即生命週期處理函數數組中保存的函數

function getWatcher(key, _effect, model, onError, onEffect) {
  let effect = _effect;
  // type定義默認值
  let type = 'takeEvery';
  let ms;
  
  // 如果是數組進行其他操作
  // if(Array.isArray(effect)){...}
  // sagaWithCatch實際執行effect,執行前後分別通知saga
  function* sagaWithCatch(...args) {
  
    // 在createPromiseMiddleware中添加了兩個Promise狀態方法
    const { 
      __dva_resolve: resolve = noop, 
      __dva_reject: reject = noop 
    } = args.length > 0 ? args[0] : {};
    try {
      yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
      // 執行用戶的effect
      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);
      }
    }
  }
  // applyOnEffect()方法將用戶提供的onEffect生命週期處理函數,
  // 註冊到effect上
  // 如果沒有插件,那麼sagaWithOnEffect的值就是sagaWithCatch
  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);
      };
    // 在effect不是數組的情況下,
    // 監聽每一個發出的action,getWatcher的返回值走的都是default
    // 即,每次發出指向effect的action時都會調用sagaWithOnEffect
    // takeEvery()方法接受兩個參數,
    // 要匹配的action和一個saga(一個saga就是一個generator函數)
    // takeEvery監聽action,在每次這個action被髮起時,
    // 創建一個新的saga任務
    // 因此,dva項目中,所有的指向effect的action的派發,
    // 都會在這裏創建一個實時任務
    default:
      return function* () {
        yield takeEvery(key, sagaWithOnEffect);
      };
  }
}

// applyOnEffect方法接受四個參數,onEffect, sagaWithCatch, model, key
// fns,即onEffect,是hooks裏面的生命週期函數onEffect的回調函數數組
// 這個函數的作用是,將所有運行的effect按照回調函數順序,依次傳入回調函數
// 最終得到一個被所有的回調函數處理(或稱爲代理)後的effect,並返回

function applyOnEffect(fns, effect, model, key) {
  for (const fn of fns) {
    effect = fn(effect, sagaEffects, model, key);
  }
  return effect;
}
(3)掛載reducers和effects

之前將model,reducer,effect的規則都確定了,但是還沒有掛載,這裏用一個for循環,在start方法裏把reducer掛載上去。getReducer方法返回對應類型的reducer(區別是reducer有可能是數組或對象)。

// 這裏遍歷了models裏面所有的namespace的model
// 對每一個model處理一遍reducer和effects
// 結合getSaga裏面的for循環,
// 兩個循環下對每一個effect裏面定義的函數都進行了異步處理
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')));
}

// 在plugin文件中有定義,拿到reducer插件代理過的reducer
const reducerEnhancer = plugin.get('onReducer');
const extraReducers = plugin.get('extraReducers');

// 如果有註冊在’onStateChange’的插件,運行並監聽
const listeners = plugin.get('onStateChange');

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

// 動態加載saga
// 也就是運行sagaMiddleware.run()返回的watcher函數
sagas.forEach(sagaMiddleware.run);
(4)組建store

store由dva管理的routerReducer(react-router-redux提供),用戶提供的reducer,extraReducer插件,異步reducer組成最後的reducer。之後是dva中間件plugin,create方法調用時傳進來的初始化配置,saga中間件,promise中間件。

// 從dva/index裏面傳進來的控制HashHistory(默認爲hashHistory)的reducer
const reducers = { ...initialReducer };
const store = (app._store = createStore({
  reducers: createReducer(),
  initialState: hooksAndOpts.initialState || {},
  plugin,
  createOpts,
  sagaMiddleware,
  promiseMiddleware,
}));

// combineReducers是redux提供的組合reducer的方法
function createReducer() {
  return reducerEnhancer(
    combineReducers({
      ...reducers,
      ...extraReducers,
      ...(app._store ? app._store.asyncReducers : {}),
    })
  );
}
(5)訂閱history改變

dva/index中將history代理到了app._history上,因此每次history改變,都會通知到redux,觸發state的更新。並且subscription中的函數接受app對象,可以訂閱到history的變化。

// dva-core/index: start
// setupApp是從dva/index中傳進來的,內部調用了patchHistory方法
// 監聽history變化,觸發回調。並將監聽的history添加到app上
// 這裏也是core中唯一一個使用外部this(找patchHistory)的地方
setupApp(app);

const unlisteners = {};
for (const model of this._models) {
 if (model.subscriptions) {
   unlisteners[model.namespace] = runSubscription(
     model.subscriptions,
     model,
     app,
     onError
   );
 }
}

// dva-core/subscription.js
// runSubscription函數接受四個參數,subs是subscription對象
// subs所在的model,core裏面創建的app對象,和全局錯誤處理函數
// 首先判斷key是否是subs的自有屬性,如果是,就運行這個函數
// 並且將history和dispatch傳進去,然後根據返回值push進相應的數組中
// 最終返回這個訂閱對象

function runSubscription(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 };
}

// 校驗數據,檢查無誤後運行unlistener函數
function unlisten(unlisteners, namespace) {
 const { funcs, nonFuncs } = unlisteners[namespace];
 for (const unlistener of funcs) {
   unlistener();
 }
 delete unlisteners[namespace];
}

By DoubleJan
2019.8.12

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