一 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提供的routerReducer
,routerMiddleware
(在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
元素,依據是否存在nodeName
和nodeType
屬性。 - 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,可以使用Provider
,connect
方法,而不需要在根組件上做配置,是因爲,在這一步,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