(图片来自Dva官网)
dva 的源码核心部分包含 dva 和 dva-core 两部分,在 Dva源码解读系列之app对象 一文中我们对 dva 源码的 dva 部分作了解析,dva/src/index.js 负责处理对外的逻辑,使用 react-redux 的Provider实现了View层。它创建了一个挂载了model、router、start等dva所有属性和方法的 app 对象,在最后通过 start 方法启动了一个React应用。
在 dva/src/index.js 中,调用了 dva-core 的 create 方法 创建了一个 app 对象:
// 调用 dva-core 的 create 方法创建一个 app 对象
const app = create(opts, createOpts);
接下来,我们来看看 dva-core 中的 create 方法都做了什么。
create 方法
create 方法是被默认导出的。它接受两个参数,第一个参数 hooksAndOpts 是使用者添加的控制选项,第二个参数createOpts是初始化了 reducer 和 redux 的中间件。方法内部创建了一个app对象并挂载了_models、_store、_plugin、use、model、start 等属性。
其中 _models 包含dvaModel和所有的用户model,_store默认为null,plugin属性挂载各种插件,这些插件是基于dva生命周期函数的,plugin.use方法被plugin对象代理,以免找错this,model方法用于注册model,start方法用于启动程序,最终create方法返回了这个app对象。
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;
// ...
}
plugin 对象
创建 plugin 对象
const plugin = new Plugin();
plugin.use(filterHooks(hooksAndOpts));
在create方法中,首先创建了一个 plugin 对象,并用filterHooks方法过滤掉不合法的插件,然后对过滤后的插件进行了注册。plugin对象负责管理和使用中间件。
export default class Plugin {
constructor() {
// 将 this.hooks 初始化为一个对象
}
// 注册中间件
use(plugin) {
// ...
}
// 全局错误处理函数
apply(key, defaultHandler) {
// ...
}
// 获取生命周期处理函数
get(key) {
// ...
}
}
// 将某一个 key 对应的生命周期函数的回调函数数组组合起来,生成一个对象
function getExtraReducers(hook) {
// ...
}
// 将用户在项目中定义的 reducer 经过每一个reducer 中间件处理
function getOnReducer(hook) {
// ...
}
在 plugin 对象中,主要做了以下几件事:
定义生命周期函数
const hooks = [
'onError',
'onStateChange',
'onAction',
'onHmr',
'onReducer',
'onEffect',
'extraReducers',
'extraEnhancers',
'_handleActions',
];
在 Plugin.js 中,首先定义一个hooks生命周期钩子函数数组,并按照这个数组,在 plugin 对象中初始化一个hooks对象,每一个属性是一个生命周期函数,被初始化为一个数组,挂载用户注册的插件。实际上dva的中间件或者说插件就是在这里被使用的。
创建 hooks 对象
constructor() {
this._handleActions = null;
// 将this.hooks初始化为一个对象,
// 它包括上面hooks数组的所有元素作为属性名,每个值都是一个空数组
// 这个空数组用来存放用户注册的中间件
this.hooks = hooks.reduce((memo, key) => {
memo[key] = [];
return memo;
}, {});
}
在 Plugin 的构造函数中,使用 reduce 方法将创建一个 hooks 对象,它包括上面 hooks 生命周期钩子函数数组的所有元素作为属性名,每个值都是一个空数组,用户注册的中间件就放在这个数组里。
注册中间件
use(plugin) {
// 数据格式校验,要求 plugin 必须是一个 纯对象
invariant(isPlainObject(plugin), 'plugin.use: plugin should be plain object');
const { hooks } = this;
for (const key in plugin) {
// 使用 Object.prototype.hasOwnProperty 是为了防止用户在 plugin 对象上自定义 hasOwnProperty 覆盖掉原函数,进行一些不恰当的操作
if (Object.prototype.hasOwnProperty.call(plugin, key)) {
// 生命周期钩子函数名校验,要求传入的 key,即生命周期函数名是存在的
invariant(hooks[key], `plugin.use: unknown plugin property: ${key}`);
// 对生命周期钩子 _handleActions 和 extraEnhancers 作特殊处理
if (key === '_handleActions') {
this._handleActions = plugin[key];
} else if (key === 'extraEnhancers') {
hooks[key] = plugin[key];
} else {
// 给生命周期钩子添加中间件,也包括 onReducers
hooks[key].push(plugin[key]);
}
}
}
}
plugin.use() 方法用来注册中间件,将用户定义的函数 push 进对应生命周期钩子的数组中,为了防止用户在 plugin 对象上自定义 hasOwnProperty 覆盖掉原函数,进行一些不恰当的操作,源码中使用了 Object.prototype.hasOwnProperty.call() 来检测用户是否在 plugin 对象上自定义了 hasOwnProperty 。
全局错误处理函数
apply(key, defaultHandler) {
const { hooks } = this;
// 通过 validApplyHooks 进行过滤,apply 方法只能应用在全局报错或者热更替上
const validApplyHooks = ['onError', 'onHmr'];
invariant(validApplyHooks.indexOf(key) > -1, `plugin.apply: hook ${key} cannot be applied`);
// 从 plugin 实例的 hooks 中取出挂载的 onError、onHmr 回调函数
const fns = hooks[key];
return (...args) => {
// hooks 中挂载了onError、onHmr 的回调函数,则以依次执行所有的回调函数
if (fns.length) {
for (const fn of fns) {
fn(...args);
}
} else if (defaultHandler) {
// hooks 中没有挂载的 onError、onHmr 的回调函数,则直接执行传递进来的处理函数,抛出错误
defaultHandler(...args);
}
};
}
在 plugin.apply() 方法中,对 hooks 生命周期钩子进行了过滤,使 apply 方法只能应用在全局报错(onError) 或 热更替(onHmr) 上。
获取生命周期处理函数
get(key) {
const { hooks } = this;
// 对 生命周期钩子进行校验,检测当前的生命周期钩子是否是在 hooks 中定义的
invariant(key in hooks, `plugin.get: hook ${key} cannot be got`);
// 当前 key 为在 hooks 中定义的 extraReducers 钩子,将 hooks 上挂载的所有函数组成一个对象然后返回
if (key === 'extraReducers') {
return getExtraReducers(hooks[key]);
}
// 当前 key 是 hooks 中定义的 onReducer 钩子,将用户定义的 reducer 用对应钩子的所有中间件进行处理
else if (key === 'onReducer') {
return getOnReducer(hooks[key]);
} else {
// 直接返回对应钩子的处理函数
return hooks[key];
}
}
get() 方法用户获取对应生命周期钩子的中间件。在 get 方法中对钩子的类型作了判断,如果当前 key 为 hooks 中定义的 extraReducers 钩子,则调用 getExtraReducers 方法,将 hooks 上挂载所有函数组成一个对象然后返回,如果当前 key 为 hooks 中定义的 onReducer 钩子,则调用 getOnReducer 方法,将用户项目中定义的 reducer 用对应钩子的所有中间件处理一遍然后返回。如果是其它的钩子,则直接返回对应钩子的中间件。
下面,我们分别来看看 getExtraReducers 方法和 getOnReducer 方法。
getExtraReducers
// 将某一个 key 对应的生命周期函数的回调函数数组组合起来,生成一个对象
function getExtraReducers(hook) {
let ret = {};
for (const reducerObj of hook) {
ret = { ...ret, ...reducerObj };
}
return ret;
}
getOnReducer
// 将用户在项目中定义的 reducer 经过每一个reducer 中间件处理
// 最终返回一个经过所有中间件处理的 recuer 函数
function getOnReducer(hook) {
return function(reducer) {
for (const reducerEnhancer of hook) {
reducer = reducerEnhancer(reducer);
}
return reducer;
};
}
过滤不合法的 hooks
export function filterHooks(obj) {
// 数组的 reduce 方法遍历 obj,将符合要求的的 hooks 钩子函数放进一个新对象中,最后将obj过滤为只包括hooks的对象
return Object.keys(obj).reduce((memo, key) => {
if (hooks.indexOf(key) > -1) {
memo[key] = obj[key];
}
return memo;
}, {});
}
在 Plugin.js 中还导出了一个 filterHooks 方法,该方法用于过滤不合法的 hooks,最终过滤出只包括在 hooks 中定义的生命周期钩子的对象。
对 plugin 对象的解读就到此结束了,下面介绍在创建 app 对象时给model添加前缀的 prefixNamespace 方法。
prefixNamespace 方法
app对象的 _model 属性,它包括了 dvaModel 和所有用户在dva项目中定义的model,在创建 app对象的时候,使用 prefixNamespace 对所有 model 的 reducers 和 effects 都加上了 namespace 的前缀。
import warning from 'warning';
import { isArray } from './utils';
import { NAMESPACE_SEP } from './constants';
function prefix(obj, namespace, type) {
return Object.keys(obj).reduce((memo, key) => {
warning(
key.indexOf(`${namespace}${NAMESPACE_SEP}`) !== 0,
`[prefixNamespace]: ${type} ${key} should not be prefixed with namespace ${namespace}`,
);
const newKey = `${namespace}${NAMESPACE_SEP}${key}`; //
memo[newKey] = obj[key];
return memo;
}, {});
}
export default function prefixNamespace(model) {
const { namespace, reducers, effects } = model;
// 给每个 reducer 添加前缀 // ${namespace}/${reducer}
if (reducers) {
if (isArray(reducers)) {
// 需要复制一份,不能直接修改 model.reducers[0], 会导致微前端场景下,重复添加前缀
const [reducer, ...rest] = reducers;
model.reducers = [prefix(reducer, namespace, 'reducer'), ...rest];
} else {
model.reducers = prefix(reducers, namespace, 'reducer');
}
}
// 给每个 effect 添加前缀 ${namespace}/${effect}
if (effects) {
model.effects = prefix(effects, namespace, 'effect');
}
return model;
}
接下来介绍app 对象的 model 属性。
model
注册 model
function model(m) {
if (process.env.NODE_ENV !== 'production') {
// 使用 invariant 对 传入的model 进行合法性检查
checkModel(m, app._models);
}
// 给传入的 model 添加前缀
const prefixedModel = prefixNamespace({ ...m });
// 将添加了前缀的 model push 进 _models 中
app._models.push(prefixedModel);
return prefixedModel;
}
app 对象中的 model,是通过 model 方法注册的。在 model 方法中,首先用 checkModel 方法对传入的 model 进行合法性检查,然后使用 prefixNamespace 方法给所有的 model 添加前缀,最后返回加了前缀的 model 对象。在 model 方法中,仅仅只是对传入的model添加了前缀,并未做其它处理,因此这个model 就是 dva 项目里用户定义的 model。
检查model
export default function checkModel(model, existModels) {
const { namespace, reducers, effects, subscriptions } = model;
// namespace 必须被定义
invariant(namespace, `[app.model] namespace should be defined`);
// 并且是字符串
invariant(
typeof namespace === 'string',
`[app.model] namespace should be string, but got ${typeof namespace}`,
);
// 并且唯一
invariant(
!existModels.some(model => model.namespace === namespace),
`[app.model] namespace should be unique`,
);
// state 可以为任意值
// reducers 可以为空,PlainObject 或者数组
if (reducers) {
invariant(
isPlainObject(reducers) || isArray(reducers),
`[app.model] reducers should be plain object or array, but got ${typeof reducers}`,
);
// 数组的 reducers 必须是 [Object, Function] 的格式
invariant(
!isArray(reducers) || (isPlainObject(reducers[0]) && isFunction(reducers[1])),
`[app.model] reducers with array should be [Object, Function]`,
);
}
// effects 可以为空,PlainObject
if (effects) {
invariant(
isPlainObject(effects),
`[app.model] effects should be plain object, but got ${typeof effects}`,
);
}
if (subscriptions) {
// subscriptions 可以为空,PlainObject
invariant(
isPlainObject(subscriptions),
`[app.model] subscriptions should be plain object, but got ${typeof subscriptions}`,
);
// subscription 必须为函数
invariant(isAllFunction(subscriptions), `[app.model] subscription should be function`);
}
}
checkModel 方法检查 model 的合法性。一个合法的model 应该满足以下几个要求:
-
namespace必须被定义为字符串,不可为空,并且是唯一的。
-
reducers是一个数组或一个纯对象,如果reducers是一个数组,那么第一个元素必须是一个对象,第二个要求是一个函数,这在后面添加前缀会用上。
-
effects必须是一个纯对象。
-
subscriptions必须是一个纯对象,每一个属性必须是一个函数。
添加前缀
使用 checkModel 检查完 model 的合法性后,使用了 prefixNamespace 方法给 model 添加前缀。prefixNamespace 方法在上文已有介绍,在此不再赘述。
至此,model已经合法了,中间件也已经注册了,reducer和effects也已经有了调用的方式和依据,接下来,dva 要做的就是启动应用程序了。
app 对象的最后一个属性,start 方法,就是用于启动应用程序的。
start
start 方法是 dva-core 的核心,它用于启动应用程序,被 dva/index 中的app对象的 start 代理。在 start 中做了以下几件事:
- 定义全局错误处理
- 将 sagas 连接到 Redux Store
- 拦截指向 effects 的action
- 初始化化 _getSaga 处理异步数据
- 挂载 reducers 和 effects
- 创建 store
- 订阅 history 改变
- 挂载 model
接下来,我们一一解读 start 方法所做的事情。
定义全局错误处理
const onError = (err, extension) => {
if (err) {
if (typeof err === 'string') err = new Error(err);
err.preventDefault = () => {
err._dontReject = true;
};
// 调用 plugin 的 apply 方法对错误进行处理,apply方法只能在应用在全局报错或者热更替上
plugin.apply('onError', err => {
// 全局报错
throw new Error(err.stack || err);
})(err, app._store.dispatch, extension);
}
};
在 start 方法中,首先定义了一个 onError 函数,用于全局错误处理。以 err、app._store.dispatch 和 extension 为参数执行 plugin 的 apply 方法,指定 onError 只能用在全局报错上。plugin.apply 方法请参考前文 plugin 对象的全局错误处理函数 。
将 sagas 连接到 Redux Store
start 方法做的第二件事,是调用 redux-saga 的 createSagaMiddleware 方法创建一个 Redux middleware,将 Sagas 连接到了 Redux Store。
const sagaMiddleware = createSagaMiddleware();
拦截指向 effects 的action
const promiseMiddleware = createPromiseMiddleware(app);
start 方法接下来做的事情,是调用 createPromiseMiddleware 方法来拦截指向 effects 的 action,并检查 action 的 type 指向的方法是否属于 effects,如果是,就返回一个挂载了 Promise 状态方法 resolve 和 reject 的 promise 中间件,否则就将 action 对象返回,不作处理。
import { NAMESPACE_SEP } from './constants';
export default function createPromiseMiddleware(app) {
// const middleware = ({dispatch}) => next => (action) => {... return next(action)} 是一个中间件的标准写法,在 return next(action) 之前可以对 action 做各种各样的操作
// 中间件拦截了指向 effects 的 action
return () => next => action => {
const { type } = action;
// isEffect 检查 action 的type,是否指向 effect,也就是在 model 的 effects 中定义的异步方法
// 如果 type 指向 effect,则返回一个 Promise 对象,并在 action 对象上挂载两个Promise的状态方法 resolve 和 reject
if (isEffect(type)) {
return new Promise((resolve, reject) => {
next({
__dva_resolve: resolve,
__dva_reject: reject,
...action,
});
});
} else {
// type 指向的不是 effect,则直接将 action 对象返回,不做处理
return next(action);
}
};
// isEffect 检查 type 是否指向 model 的 effects 里的方法
function isEffect(type) {
if (!type || typeof type !== 'string') return false;
// dva 里 action 的 type 有固定格式: model.namespace/model.effects
const [namespace] = type.split(NAMESPACE_SEP);
// 根据 namespace 过滤出对应的 model
const model = app._models.filter(m => m.namespace === namespace)[0];
// 如果 model 存在并且 model.effects[type] 也存在,那必定是 effects
if (model) {
// type 指向 model 的 effects 中定义的异步方法
if (model.effects && model.effects[type]) {
return true;
}
}
return false;
}
}
初始化 _getSaga 处理异步数据
接下来,start 方法调用 getSaga 方法来初始化 app 对象的 _getSaga 方法,这个函数借助了 redux-saga 的能力来处理数据。
app._getSaga = getSaga.bind(null);
getSaga 调用 bind 来初始化 _getSaga,使 _getSaga 拥有 getSaga 预设预设的初始参数,这些参数(如果有的话)会作为 bind() 的第二个参数跟在 this 后面,之后它们会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们的后面。
getSaga
getSaga 方法接受5个参数,第一个参数 effects 是 model 中定义的 effects,第二个参数 model 就是在项目中定义的 model 对象,第三个参数 onError 是在 dva-core/index 中定义的全局错误处理函数 onError,第四个参数 onEffect 则是在 Plugin.js 中定义的生命周期钩子,而最后有一个函数 opts,是在调用 create 方法时传递进来的 用户添加的控制选项。getSaga 方法最终返回了一个 generator 函数。
// getSaga 方法借助 redux-saga 的能力来处理异步数据
export default function getSaga(effects, model, onError, onEffect, opts = {}) {
// 返回一个 generator 函数
return function*() {
for (const key in effects) {
if (Object.prototype.hasOwnProperty.call(effects, key)) {
// getWatcher 返回一个 generator 函数,调用 redux-saga 提供 的 takeEvery 来实时监听 action 和 effect
const watcher = getWatcher(key, effects[key], model, onError, onEffect, opts);
// 使用 redux-saga 提供的 fock 方法将 watcher 分离到一个单独的线程中去执行(即生成了一个新的 task 对象)
const task = yield sagaEffects.fork(watcher);
// 为了方便对 task 的控制,开辟一个新的进程,执行一个 generator 函数,在卸载 effect 所在 model 的action 时取消正在执行的 task 线程
yield sagaEffects.fork(function*() {
yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
yield sagaEffects.cancel(task);
});
}
}
};
}
在 getSaga 返回的 generator 函数中,遍历 model 的 effects 属性,给每一个 effect (注:同样是 generator 函数) 添加一个 watcher,然后使用 redux-saga 的 fork 函数单独开辟一个线程监听 action,并执行 effect(生成了一个 task 对象)。同时为了方便控制该线程,又另外开辟了一个新的线程执行一个 generator 函数,拦截取消 effect 的action,一旦监听到则立刻取消已经分出去的 task 线程。
getWatcher
getWatcher 方法返回一个 generator 函数,给传递进去的_effect (model 中的effect) 添加一个 watcher,用来监听匹配的 action。getWatcher 接收 6 个参数:
- key 经过 prefixNamespace 转义后的 effect 方法名,(在 model 中定义的 generator 函数名称)
- _effect 在 model 的 effects 中定义的异步方法
- model model 对象本身
- onError 在dva-core/index里面定义的全局错误处理函数onError
- onEffect 在 plugin 定义的生命周期函数
- opts 用户添加的控制选项
function getWatcher(key, _effect, model, onError, onEffect, opts) {
let effect = _effect;
// 定义 type 的默认值
let type = 'takeEvery';
let ms;
let delayMs;
// _effect 是数组则执行相关的操作
if (Array.isArray(_effect)) {
[effect] = _effect;
const opts = _effect[1];
if (opts && opts.type) {
({ type } = opts);
if (type === 'throttle') {
invariant(opts.ms, 'app.start: opts.ms should be defined if type is throttle');
({ ms } = opts);
}
if (type === 'poll') {
invariant(opts.delay, 'app.start: opts.delay should be defined if type is poll');
({ delay: delayMs } = opts);
}
}
invariant(
['watcher', 'takeEvery', 'takeLatest', 'throttle', 'poll'].indexOf(type) > -1,
'app.start: effect type should be takeEvery, takeLatest, throttle, poll or watcher',
);
}
function noop() {}
// sagaWithCatch 实际上执行用户定义的effect方法,执行前后分别通知redux-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` });
// 执行用户在model的effects 中定义的 effect 方法
const ret = yield effect(...args.concat(createEffects(model, opts)));
yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });
resolve(ret);
} catch (e) {
onError(e, {
key,
effectArgs: args,
});
if (!e._dontReject) {
reject(e);
}
}
}
// 使用 plugin 中定义的 hooks 中的回调函数依次处理 effect
const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
switch (type) {
case 'watcher':
return sagaWithCatch;
case 'takeLatest':
return function*() {
yield sagaEffects.takeLatest(key, sagaWithOnEffect);
};
case 'throttle':
return function*() {
yield sagaEffects.throttle(ms, key, sagaWithOnEffect);
};
case 'poll':
return function*() {
function delay(timeout) {
return new Promise(resolve => setTimeout(resolve, timeout));
}
function* pollSagaWorker(sagaEffects, action) {
const { call } = sagaEffects;
while (true) {
yield call(sagaWithOnEffect, action);
yield call(delay, delayMs);
}
}
const { call, take, race } = sagaEffects;
while (true) {
const action = yield take(`${key}-start`);
yield race([call(pollSagaWorker, sagaEffects, action), take(`${key}-stop`)]);
}
};
// 在effect不是数组的情况下,
// 监听每一个发出的action,getWatcher的返回值走的都是default
// 即,每次发出指向effect的action时都会调用sagaWithOnEffect
//
// takeEvery()方法接受两个参数,
// 要匹配的action和一个saga(一个saga就是一个generator函数)
// takeEvery监听action,在每次这个action被发起时,创建一个新的saga任务
// 因此,dva项目中,所有的指向effect的action的派发,都会在这里创建一个实时任务
default:
return function*() {
yield sagaEffects.takeEvery(key, sagaWithOnEffect);
};
}
}
在 getWatcher 内部,主要做了以下几件事(不考虑数组的情况):
-
定义 sagaWithCatch 方法在内部执行用户定义的 effect 方法,并在执行前后通知 redux-saga。由于在 createPromiseMiddleware 中已经给 action 添加了 Promise 状态方法 resolve 和 reject,因此在 effect 执行结束后调用 Promise 相应的状态方法返回对应的执行结果。
-
在执行用户定义的 effect 时,调用 createEffects 方法对数据进行校验。该方法重新封装了 redux-saga 的 put 和 take 方法,对 action 的 type 添加了数据类型及格式的校验。
-
执行 applyOnEffect 方法,这个方法将用户提供的 onEffect 生命周期处理函数注册到 effect 上,在不影响 effect 运行的同时,依次执行用户添加的中间件。
-
最后,用 switch 判断用户定义的代理类型,默认情况下,返回一个generator 函,用 redux-saga 的 takeEvery 方法,监听对应的 action,并在匹配到的时候自动创建一个异步任务,执行用户定义的 effect 函数。
createEffects
createEffects 方法重新封装了 redux-saga 的 put、take 方法,并对 action 的 type 添加了数据类型及格式的校验。
// 封装redux-saga提供的put、take方法,添加数据校验,主要是type的检查
function createEffects(model, opts) {
// 给数据添加校验
function assertAction(type, name) {
invariant(type, 'dispatch: action should be a plain Object with type');
const { namespacePrefixWarning = true } = opts;
if (namespacePrefixWarning) {
warning(
type.indexOf(`${model.namespace}${NAMESPACE_SEP}`) !== 0,
`[${name}] ${type} should not be prefixed with namespace ${model.namespace}`,
);
}
}
// 重新封装 redux-saga 的 put 方法
function put(action) {
const { type } = action;
assertAction(type, 'sagaEffects.put');
return sagaEffects.put({ ...action, type: prefixType(type, model) });
}
// The operator `put` doesn't block waiting the returned promise to resolve.
// Using `put.resolve` will wait until the promsie resolve/reject before resuming.
// It will be helpful to organize multi-effects in order,
// and increase the reusability by seperate the effect in stand-alone pieces.
// https://github.com/redux-saga/redux-saga/issues/336
function putResolve(action) {
const { type } = action;
assertAction(type, 'sagaEffects.put.resolve');
return sagaEffects.put.resolve({
...action,
type: prefixType(type, model),
});
}
put.resolve = putResolve;
// 重新封装 redux-saga 的 take 方法
function take(type) {
if (typeof type === 'string') {
assertAction(type, 'sagaEffects.take');
return sagaEffects.take(prefixType(type, model));
} else if (Array.isArray(type)) {
return sagaEffects.take(
type.map(t => {
if (typeof t === 'string') {
assertAction(t, 'sagaEffects.take');
return prefixType(t, model);
}
return t;
}),
);
} else {
return sagaEffects.take(type);
}
}
return { ...sagaEffects, put, take };
}
applyOnEffect
applyOnEffect 方法,将用户提供的 onEffect 生命周期处理函数注册到 effect 上,在不影响 effect 运行的同时,依次执行用户添加的中间件。
/**
* 函数的作用,是将所有运行的 effect 按照 plugin 中定义的hooks的回调函数的顺序,并依次执行回调函数处理 effect
* 最终得到一个被所有回调函数处理(或称为代理)后的 effect,并返回
* @param {*} fns 在 plugin 定义的生命周期函数
* @param {*} effect 用户在 model 中定义的 effect
* @param {*} model model 对象本身
* @param {*} key 经过 prefixNamespace 转义后的 effect 方法名,(在 model 中定义的 generator 函数名称)
*/
function applyOnEffect(fns, effect, model, key) {
for (const fn of fns) {
effect = fn(effect, sagaEffects, model, key);
}
return effect;
}
组合 reducers 和 effects
reducer、effect 的规则都已经确定了,但还是存储在各自的model上,接下来 start 要做的事情,就是把各个model 的 reducer、effect 组合在一起。在这里使用一个 for 循环,将各个 model 的 reducer 和 effects 统一组合到一个总的 reducers 和 sagas 上。
getReducer 方法返回对应类型的 reducer (区别是 reducer 有可能是数组或对象)。
在上面初始化 _getSaga 时,getSaga 调用 bind 方法提前设定好了 _getSaga 的预设参数,在这里组合 effects 时,便可以传入完整的参数,执行 _getSaga 方法,将effect 放进 sagas 数组里。
// 将各个 model 的 reducer 和 effects 统一挂载到一个总的 reducers 和 sagas 上
const sagas = [];
const reducers = { ...initialReducer };
for (const m of app._models) {
// getReducer 返回对应类型的 reducer(区别是 reducer有可能是数组或对象)
reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
if (m.effects) {
// 传入完整的参数,执行 _getSaga 方法,执行后的结果放进 sagas 数组里
sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
}
}
// onReducer、extraReducers 在 plugin 中有定义,从 plugin 拿到 reducer 插件代理过的 reducer
const reducerEnhancer = plugin.get('onReducer');
const extraReducers = plugin.get('extraReducers');
invariant(
Object.keys(extraReducers).every(key => !(key in reducers)),
`[app.start] extraReducers is conflict with other reducers, reducers list: ${Object.keys(
reducers,
).join(', ')}`,
);
创建 store
同步数据流和异步数据流的解决方案都已经有了,那么接下来该创建 store 了。
createStore(reducer, [preloadedState], enhancer)
正常情况下 redux 的 createStore 接收三个参数,即 reducer、initState、applyMiddleware(middlewares)(注:对createStore 的三个参数进行了转义处理)。
不过 dva 通过扩展 redux 的 createStore,提供了自己的 createStore 方法,用来组织一系列自己创建的参数。
// Create store
app._store = createStore({
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin,
createOpts,
sagaMiddleware,
promiseMiddleware,
});
const store = app._store;
// Extend store
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {};
store 有 reducers、state、dva 中间件 plugin,create 方法调用时传递进来的初始化配置,saga 中间件,promise 中间件等属性。其中 reducers 是由 dva 管理的 routerReducer (connected-react-router 将 react-router 绑定到了 redux),用户提供的 reducer,extractReducer插件,异步 reducer 组成的。
createStore
dva 提供的 crateStore 方法,扩展了 redux 的 createStore 方法,以便于组织一系列自己创建的参数
import { createStore, applyMiddleware, compose } from 'redux';
import flatten from 'flatten';
import invariant from 'invariant';
import win from 'global/window';
import { returnSelf, isArray } from './utils';
export default function({
reducers,
initialState,
plugin,
sagaMiddleware,
promiseMiddleware,
createOpts: { setupMiddlewares = returnSelf },
}) {
// extra enhancers
const extraEnhancers = plugin.get('extraEnhancers');
invariant(
isArray(extraEnhancers),
`[app.start] extraEnhancers should be array, but got ${typeof extraEnhancers}`,
);
const extraMiddlewares = plugin.get('onAction');
const middlewares = setupMiddlewares([
promiseMiddleware,
sagaMiddleware,
...flatten(extraMiddlewares),
]);
const composeEnhancers =
process.env.NODE_ENV !== 'production' && win.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? win.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true, maxAge: 30 })
: compose;
const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];
return createStore(reducers, initialState, composeEnhancers(...enhancers));
}
createReducer
function createReducer() {
// 使用 plugin 里的 onReducer 扩展 reducer 的功能,在上面引入了 onReducer: const reducerEnhancer = plugin.get('onReducer');
return reducerEnhancer(
// 使用 redux 提供 combineReducers 接口组合 reducer
combineReducers({
...reducers, // 从 dva 里传入的 historyReducer,以及通过 reducers[m.namespace] = getReducer(m.reducers, m.state); 剥离出的 model 中的 reducer (用户提供的 reducer)
...extraReducers, // 手动在 plugin 里添加的 extraReducers
...(app._store ? app._store.asyncReducers : {}), // 异步 reducer,主要是用于在 dva 运行以后动态加载 model 里的 reducer
}),
);
}
createReducer 实际上是用 plugin 里的 onReducer 扩展了 reducer 的功能:
const reducerEnhancer = plugin.get('onReducer');
在 reducerEnhancer 里,借助了 redux 的 combineReducers 来组合 historyReducer、extraReducers 和 异步 reducer。
而在 combineReducers 中:
-
第一个
...reducers是从 dva 里传入的 historyReducer,以及通过reducers[m.namespace] = getReducer(m.reducers, m.state);剥离出的 model 中的 reducer -
第二个extraReducers 为手动在 plugin 里添加的 extraReducers
-
第三个参数为异步 reducer,主要是用于在 dva 运行以后动态加载 model 里的 reducer
下面我们来看看 Plugin 里的 onReducer:
onReducer
// 将用户在项目中定义的 reducer 经过每一个reducer 中间件处理
// 最终返回一个经过所有中间件处理的 redcuer 函数
function getOnReducer(hook) {
return function(reducer) {
// 如果有 onReducer 的插件,则用 reducer 的插件扩展 reducer
for (const reducerEnhancer of hook) {
reducer = reducerEnhancer(reducer);
}
return reducer;
};
}
onReducer 将用户在项目中定义的 reducer 使用 reducer 的中间件处理,最终返回一个经过所有中间处理的 reducer 函数。
订阅 history 改变
监听 onStateChange
// 当 状态发生变化时,执行挂载在 onStateChange 上的监听器,并在 store 上添加一个监听
const listeners = plugin.get('onStateChange');
for (const listener of listeners) {
store.subscribe(() => {
listener(store.getState());
});
}
从 plugin 对象中获取挂载到 onStateChange 上的监听器,当状态发生了改变时,执行监听器,并在 store 上添加一个监听。
运行所有sagas
// Run sagas
// 动态加载saga,也就是运行sagaMiddleware.run()返回的watcher函数
sagas.forEach(sagaMiddleware.run);
动态加载 saga,运行 sagaMiddleware.run 返回的 watcher 函数。
监听 history 变化
// Setup app
// setupApp 是从 dva/index 中传进来的,内部调用了 patchHistory 方法监听 history变化,触发回调,并将监听的 history 添加到 app 对象上
setupApp(app);
setupApp 是从 dva/index 中传进来的,在 dav/index 中将 history 代理到了app._history 上,并且调用了patchHistory 方法监听 history,因此每次 history 发生变化,都会通知到 redux,触发 state 的更新。
运行 subscriptions
// Run subscriptions
const unlisteners = {};
for (const model of this._models) {
if (model.subscriptions) {
// subscription中的函数接受app对象,可以订阅到history的变化。
unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
}
}
在运行 model 的 subscriptions 时,调用了 runSubscription 方法:
/**
* 首先判断 key 是否是 subs 的自有属性,如果是,就运行这个函数(在 model 的 subscription 中定义的 function),将 dispatch 和 history 传入这个 function 中,最后返回订阅对象
* @param {*} subs model 中的 subscription 对象
* @param {*} model 对应的 model 对象
* @param {*} app 在 dva-core 中创建的 app 对象
* @param {*} onError 全局异常捕获额 onError
*/
export function run(subs, model, app, onError) {
const funcs = [];
const nonFuncs = [];
for (const key in subs) {
// 使用 原型方法判断 key 是否是 subs (model 中的 subscription) 的自有属性
if (Object.prototype.hasOwnProperty.call(subs, key)) {
// 拿到属性对应的值,即在 model 的 subscription 中定义的 function
const sub = subs[key];
// 执行该 function,分别传入 dispatch 和 history 属性
const unlistener = sub(
{
// prefixedDispatch 给 action 里的 type 添加 ${model.namespance}/ 的前缀
dispatch: prefixedDispatch(app._store.dispatch, model),
// 在 dav/index 中使用 patchHistory 扩展过的 history
history: app._history,
},
onError,
);
if (isFunction(unlistener)) {
funcs.push(unlistener);
} else {
nonFuncs.push(key);
}
}
}
// 返回订阅对象
return { funcs, nonFuncs };
}
run 方法接受四个参数:
-
第一个参数是 model 中的 subscription 对象。
-
第二个参数是对应的 model
-
第三个参数是 dva-core 中创建的 app
-
第四个参数是全局异常捕获的 onError
在 run 方法中,首先判断 key 是否是 subs (model 中的 subscription 对象) 的自有属性,如果是,就运行这个函数(在 model 的 subscription 中定义的 function),将 dispatch 和 history 传入这个 function 中,最后返回订阅对象。
挂载 model
start 方法做的最后一件事,就是将 model 到 app 对象上。
// Setup app.model and app.unmodel
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);
injectModel
在注册 model 时,首先给 model 添加了前缀,然后在 store 上挂载当前 model 的 reducers、effects 以及 subscriptions。
/**
* Inject model after app is started.
*
* @param createReducer
* @param onError
* @param unlisteners
* @param m
*/
function injectModel(createReducer, onError, unlisteners, m) {
// 注册 model ,实际上是给传入的model添加前缀
m = model(m);
const store = app._store;
// 挂载 reducers、effects(异步操作)、subscriptions(订阅)
store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
store.replaceReducer(createReducer());
if (m.effects) {
store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
}
if (m.subscriptions) {
unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);
}
}
unModel
unModel 卸载 app 对象上的 model,并同时清除 app 对象的 store 上的 reducers、effects 以及订阅(subscrioptions)。
/**
* Unregister model. 卸载 model,将存储在 app 对象 _store 上的 reducers删除,并取消相关的 effects 及订阅(subscrioptions)
*
* @param createReducer
* @param reducers
* @param unlisteners
* @param namespace
*
* Unexpected key warn problem:
* https://github.com/reactjs/redux/issues/1636
*/
function unmodel(createReducer, reducers, unlisteners, namespace) {
const store = app._store;
// 删除存储在 app 对象 _store 上的 reducers
// Delete reducers
delete store.asyncReducers[namespace];
delete reducers[namespace];
// 将 app 对象 _sotre 上的 reducer 替换为初始化时创建的全局 reducer
store.replaceReducer(createReducer());
store.dispatch({ type: '@@dva/UPDATE' });
// 取消 异步操作
// Cancel effects
store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });
// 取消相关事件的监听
// Unlisten subscrioptions
unlistenSubscription(unlisteners, namespace);
// Delete model from app._models
app._models = app._models.filter(model => model.namespace !== namespace);
}
replaceModel
在热更新的时候,检查 app 对象上是否已经有 model,如果已经存在,先将旧的 model 清除,并同时清除该 model 上的 reducer、effects 及 subscrioptions,然后再给 app 对象添加一个新的 model。如果 app 对象上没有 model,则直接给 app 对象添加 model。
// 如果 model 已经存在,先将 model 清除,然后再给 app 对象添加新的 model
// 如果 app 对象没有 model,则直接给 app 对象 添加 model
/**
* Replace a model if it exsits, if not, add it to app
* Attention:
* - Only available after dva.start gets called
* - Will not check origin m is strict equal to the new one
* Useful for HMR
* @param createReducer
* @param reducers
* @param unlisteners
* @param onError
* @param m
*/
function replaceModel(createReducer, reducers, unlisteners, onError, m) {
const store = app._store;
const { namespace } = m;
const oldModelIdx = findIndex(app._models, model => model.namespace === namespace);
// model 已经存在,将 model 清除
if (~oldModelIdx) {
// Cancel effects
store.dispatch({ type: `${namespace}/@@CANCEL_EFFECTS` });
// Delete reducers
delete store.asyncReducers[namespace];
delete reducers[namespace];
// Unlisten subscrioptions
unlistenSubscription(unlisteners, namespace);
// Delete model from app._models
app._models.splice(oldModelIdx, 1);
}
// 给 app 对象添加新的 model
// add new version model to store
app.model(m);
store.dispatch({ type: '@@dva/UPDATE' });
}
小结
dva-core 是 dva 核心功能的实现部分,它通过 create 方法创建并返回一个 dva 对象 给 dva/src/index。在create 方法内部,使用内部的 start 方法初始化各种被 dva/src/index 配置过的属性。而 start 方法是 dva-core 的核心,它用于启动应用程序,被 dva/src/index 中的 app 对象的 start 代理。在 start 方法中,完成了 store 的初始化以及 redux-saga 的调用。