前言
在学习antd的UI框架时,了解到了dva这个轻量级的应用框架,集成了react,redux,redux-saga,react-router 。视图,数据流,路由都有。开发起来还是比较简洁的。学习难度不大,主要还是一些约定开发,因为核心还是上面的4个库。
源码主要分成两大块,dva是建立路由,视图与数据层的关系,dva-core是数据层。
开始
版本 : 2.5.0-beta.2
dva入口
src/index.js
只有100多行代码。实际是把配置传入dva-core中生成app实例,然后绑定视图,而视图就是路由组件,并且使用Provider 组件(参考官网高级用法context,有点像Vue的provide和inject)包裹,共享了store。
有些小细节。
- app.model是dva-core实例的方法,后面再看。
- app.router方法是为了检测参数是否为function。因为后期要传入对象。只接受function类型。
- patchHistory是为了装饰一下history.listen,使得能在监听设置时就能获得当前的location值。对于框架来说,是为了获得应用启动时的location。
- 除router方法外,初始化dva应用时使用的方法都是dva-core也就是数据层的方法。不过start就作了装饰代理,使数据与视图绑定,并传入history等参数给用户去绑定路由。
import React from 'react';
import invariant from 'invariant';
import createHashHistory from 'history/createHashHistory';
import {
routerMiddleware,
routerReducer as routing,
} from 'react-router-redux';
import document from 'global/document';
import { Provider } from 'react-redux';
import * as core from 'dva-core';
import { isFunction } from 'dva-core/lib/utils';
export default function (opts = {}) {
const history = opts.history || createHashHistory();
// 传给core初始化数据层。
const createOpts = {
initialReducer: {
routing,
},
setupMiddlewares(middlewares) {
return [
routerMiddleware(history),// 路由的中间件
...middlewares,
];
},
setupApp(app) {
app._history = patchHistory(history);// 为了能通过listen获得初始的location数据
},
};
const app = core.create(opts, createOpts); // 创建数据层实例
const oldAppStart = app.start;
app.router = router; // router组件
app.start = start; // 主入口
return app;
// 为了断言router须为function
function router(router) {
invariant(
isFunction(router),
`[app.router] router should be function, but got ${typeof router}`,
);
app._router = router;
}
//
function start(container) {
// 允许 container 是字符串,然后用 querySelector 找元素
if (isString(container)) {
container = document.querySelector(container);
invariant(
container,
`[app.start] container ${container} not found`,
);
}
// 并且是 HTMLElement
invariant(
!container || isHTMLElement(container),
`[app.start] container should be HTMLElement`,
);
// 路由必须提前注册
invariant(
app._router,
`[app.start] router must be registered before app.start()`,
);
if (!app._store) {
oldAppStart.call(app);
}
const store = app._store;
// export _getProvider for HMR
// ref: https://github.com/dvajs/dva/issues/469
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 isHTMLElement(node) {
return typeof node === 'object' && node !== null && node.nodeType && node.nodeName;
}
function isString(str) {
return typeof str === 'string';
}
function getProvider(store, app, router) {
const DvaRoot = extraProps => (
{ router({ app, history: app._history, ...extraProps }) }
);
return DvaRoot;
}
function render(container, store, app, router) {
const ReactDOM = require('react-dom'); // eslint-disable-line
ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}
// 装饰一下history.listen,使得能在监听设置时能获得当前的location值
function patchHistory(history) {
const oldListen = history.listen;
history.listen = (callback) => {
callback(history.location); // 初始值
return oldListen.call(history, callback);
};
return history;
}
接下来就要进入dva-core.create方法里。
dva-core/src/index.js
代码的编写好清晰,主要的都在前面,主到次的写法。
- 第一个参数是可传入钩子函数
- new Plugin() 是保存及应用钩子的对象
- dvaModel的是在model更新时触发reducer。
- prefixNamespace方法是为reducers和effects的对象添加namespace前缀,并提醒开发者代码中不需要加前缀
- 能发现app.use 能设置钩子函数。
// 第一个参数是可传入钩子函数
export function create(hooksAndOpts = {}, createOpts = {}) {
const { initialReducer, setupApp = noop } = createOpts;
const plugin = new Plugin();// 保存及应用钩子的对象
plugin.use(filterHooks(hooksAndOpts)); // 筛选及保存钩子函数
// dvaModel好似是unmodel时值加一。
// prefixNamespace是为reducers和effects的对象添加namespace前缀,并提醒开发者代码中不需要加前缀
// 能发现app.use 能设置钩子函数。
const app = {
_models: [prefixNamespace({ ...dvaModel })],
_store: null,
_plugin: plugin,
use: plugin.use.bind(plugin),
model,
start,
};
return app;
接着看app.model
- checkModel函数:开发环境检查model对象的格式是否正确,并且namespace不能有重复
- 在checkModel这里再次发现reduces是可以传入数组,格式是[object,function],初步了解得知object是平时的写法,而function是store enhancer,在这function里可对store对象进行扩展
- 所有的model保存在了app._models中
- 函数返回的就是格式化后的model
// 所有的model保存在了app._models中
// 函数返回的就是格式化后的model
function model(m) {
if (process.env.NODE_ENV !== 'production') {
checkModel(m, app._models);/* 开发环境检查model对象的格式是否正确,并且namespace不能有重复
在这里再次发现reduces是可以传入数组,格式是[object,function],
初步了解得知object是平时的写法,而function是store enhancer,在这function里可对store对象进行扩展
*/
}
const prefixedModel = prefixNamespace({ ...m });
app._models.push(prefixedModel);
return prefixedModel;
}
然后到最重点处原始的start方法。
- 原来的start函数是没有参数传入的
首先定义可触发onError钩子的函数
在全局错误处理函数中,能发现之前plugin.apply的写法的意义,调用时可设置默认的函数,并且apply后返回的是一个函数,可传入任何参数去触发,很灵活。
const onError = (err, extension) => {
if (err) {
if (typeof err === 'string') err = new Error(err);
err.preventDefault = () => {
err._dontReject = true;
};
plugin.apply('onError', err => {
throw new Error(err.stack || err);
})(err, app._store.dispatch, extension);
}
};
然后是获得初始化store时传入的中间件
- createPromiseMiddleware: 为了dispatch时找到effect的话,返回Promise,后面的处理model.effects能看到
const sagaMiddleware = createSagaMiddleware(); // saga的提供store的中间件。
const promiseMiddleware = createPromiseMiddleware(app);
// 为了dispatch时找到effect的话,返回Promise
export default function createPromiseMiddleware(app) {
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);
}
};
}
然后开始循环app._model去收集reduces与saga
- getReducer 合并所有的reducers成一个函数。
可看出高阶的用法是把reduce的结果返回给数组第二个位置的function。
有默认的defaultHandleActions,它会对所有的reducers的key分别生成key与action.type进行对比,
相同则调用value函数,否则直接返回state的函数,最后再reduce前面所有的函数
。有一个想法,可app.use{_handleActions:fn(用some先找出来,
这样执行一次函数就好)}
for (const m of app._models) {
/* getReducer 合并所有的reducers成一个函数。
可看出高阶的用法是把reduce的结果返回给数组第二个位置的function。
有默认的defaultHandleActions,它会对所有的reducers的key分别生成key与action.type进行对比,
相同则调用value函数,否则直接返回state的函数,最后再reduce前面所有的函数
。有一个想法,可app.use{_handleActions:fn(用some先找出来,
这样执行一次函数就好)}*/
reducers[m.namespace] = getReducer(
m.reducers,
m.state,
plugin._handleActions
);
if (m.effects)
// 初始化saga传入effects对象,model,全局错误处理函数,onEffect的钩子数组
sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
}
我们重点看看getSaga函数。
- 这是初始化saga,传入effects对象,model,全局错误处理函数,onEffect的钩子数组
- 对每一个watcher开出一个并行任务
- 同时对外设置了cancel上面的任务的方法。dispatch({type:
${model.namespace}/@@CANCEL_EFFECTS
})就cancel该model的所有effects。(主要用于给app.unmodel与app.replaceModel)
function getSaga(effects, model, onError, onEffect) {
return function*() {
for (const key in effects) {
// 保证是原始的hasOwnProperty条用
if (Object.prototype.hasOwnProperty.call(effects, key)) {
// 生成一个观察者。
const watcher = getWatcher(key, effects[key], model, onError, onEffect);
// 对每一个watcher开出一个并行任务
const task = yield sagaEffects.fork(watcher);
// 同时对外设置了cancel上面的任务的方法。put(`${model.namespace}/@@CANCEL_EFFECTS`)就cancel
// 该model的所有effects。
yield sagaEffects.fork(function*() {
yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
yield sagaEffects.cancel(task);
});
}
}
};
}
明显重点在getWatcher。
- 默认type类型是takeEvery
- effect写成数组形式可以在arr[0]中传入opt,可以设置观察的type,
只能是'watcher', 'takeEvery', 'takeLatest', 'throttle'的之一
直接看默认的takeEvery
最后执行的是
return function*() {
yield takeEvery(key, sagaWithOnEffect);
}
为什么要是sagaWithOnEffect呢,因为有onEffect钩子,这是提供修改effect的钩子。
- onEffect钩子可获取的参数是effect,saga的操作集合,该model对象,effect的key
// 触发onEffect的钩子,这个可以修改初始化的saga
const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
....
....
function applyOnEffect(fns, effect, model, key) {
for (const fn of fns) {
// 传入的参数为包装好的effect,saga的操作集合,该model对象,effect的key
effect = fn(effect, sagaEffects, model, key);
}
return effect;
}
所以重点的转到了sagaWithCatch。
- 还记得createPromiseMiddleware中间件吗,是effect的action会
next({ __dva_resolve: resolve, __dva_reject: reject, ...action, })
,所以这里能取得中间件返回promise的resolve和reject。由此,对于effect的action,我们可以用dispatch({ type: 'any/any', payload: xxx, }).then(() => ...);
去在effect结束或者报错时作一些操作 - 可在reducers中设置开始
${key}${NAMESPACE_SEP}@@start
与结束的钩子${key}${NAMESPACE_SEP}@@end
。如果没报错的话,确实end可以当做resolve去使用。 - 报错的话肯定会触发包装过的全局公用onError,但如果设置钩子时执行了err.preventDefault(),则不再抛出错误,也就是
dispatch().catch()
无效
function* sagaWithCatch(...args) {
const { __dva_resolve: resolve = noop, __dva_reject: reject = noop } =
args.length > 0 ? args[0] : {};
try {
// effect开始钩子
yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
// createEffects: 把加工过的操作符集合加在参数的最后一个。
// 并对put,put.resolve,take做了包装,使得不需要传type时不需要加namespace
const ret = yield effect(...args.concat(createEffects(model)));
// effect结束钩子
yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });
resolve(ret);
} catch (e) {
onError(e, {
key,
effectArgs: args,
});
// 如果在onError钩子中执行了err.preventDefault(),则不再抛出错误
if (!e._dontReject) {
reject(e);
}
}
}
saga与reducer收集好后,就可以创建store了
- createReducer是把对combineReducer(还传入了非model里的reducers
plugin.get('extraReducers')
)后reducer传入onReducer钩子组合成的reducerEnhancer函数(plugin.get('onReducer')
) - plugin.get('extraEnhancers')获得用户设置的store增强工具
- plugin.get('onAction')获得用户设置的store中间件。
- 然后收集所有框架内中间键,dva-core中有promiseMiddleware与sagaMiddleware,dva中有routerMiddleware(history),从createOpts传入setupMiddlewares 函数
setupMiddlewares(middlewares) { return [ routerMiddleware(history), ...middlewares, ]; },
- 对于 redux 中 的 compose 函数,在数组长度为 1 的情况下返回第一个元素。compose(...enhancers) 等同于 applyMiddleware(...middlewares)
const store = (app._store = createStore({
// eslint-disable-line
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin,
createOpts,
sagaMiddleware,
promiseMiddleware,
}));
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}`
);
// 由这个初始化可以知道,onAction钩子必须在app.start前设置
const extraMiddlewares = plugin.get('onAction');
const middlewares = setupMiddlewares([
promiseMiddleware,
sagaMiddleware,
...flatten(extraMiddlewares),
]);
const composeEnhancers =
process.env.NODE_ENV !== "production" &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
: compose;
const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];
return createStore(reducers, initialState, composeEnhancers(...enhancers));
}
- 把saga中间件的监听Saga的函数赋值给store.runSaga
- 把记录运行中传入的reducers的store.asyncReducers设为空对象
store.runSaga = sagaMiddleware.run; // 开始监听Saga的函数
store.asyncReducers = {};
- 登记onStateChange钩子,其实就是调用store.subscribe去触发
// 其实就是调用store.subscribe去触发onStateChange钩子
const listeners = plugin.get('onStateChange');
for (const listener of listeners) {
store.subscribe(() => {
listener(store.getState());
});
}
监听所有的Saga
// 循环监听Saga
sagas.forEach(sagaMiddleware.run);
执行了dva传入的setupApp(app) {app._history = patchHistory(history)}
给app赋值了_history
setupApp(app) {
app._history = patchHistory(history);
}, 给app赋值了_history*/
setupApp(app);
接着处理subscriptions,遍历models去订阅
- 订阅所有model的subscriptions,并记录返回取消订阅的方法
const unlisteners = {};
for (const model of this._models) {
if (model.subscriptions) {
// 订阅所有model的subscriptions,并返回取消订阅的方法
unlisteners[model.namespace] = runSubscription(
model.subscriptions,
model,
app,
onError
);
}
}
我们暂停往下,关注runSubscription
- subscriptions中参数了dispatch只能触发当前model的action,因为会自动加prefix
- 有history对象,就是说可以切换路由
- 可触发onError钩子。
- 有可能移除或覆盖model的话,用户必须返回取消订阅的方法。不返回的话,下面unlisten方法(遍历运行而已)会发出警告
function run(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 };
}
回到主线。基本到最后了。添加工具函数app.model,app.unmodel,app.replaceModel。
先看app.model
- 此函数已bind了前3个参数:初始化合并reducers的函数,处理错误函数,记录取消订阅的对象。
app.model = injectModel.bind(app, createReducer, onError, unlisteners);
1. model(m)之前讲过,是检查及保存model。
2. 把此模块的reducers合成后赋值给store.asyncReducers[m.namespace]
3. 调用原始的store. replaceReducer,会与store.asyncReducers作比较合并,createReducer就是过得当前所有reducers的combine。
4. 监听effect与订阅subscriptions与之前一致
function injectModel(createReducer, onError, unlisteners, m) {
m = model(m);
const store = app._store;
store.asyncReducers[m.namespace] = getReducer(
m.reducers,
m.state,
plugin._handleActions
);
// 调用原始的replaceReducer,会与store.asyncReducers作比较合并
store.replaceReducer(createReducer());
if (m.effects) {
store.runSaga(
app._getSaga(m.effects, m, onError, plugin.get('onEffect'))
);
}
if (m.subscriptions) {
unlisteners[m.namespace] = runSubscription(
m.subscriptions,
m,
app,
onError
);
}
}
我们再看看 app.unmodel,移除model
- 删除reducers是直接把store.asyncReducers与reducers里的key删除,简单粗暴。然后再次执行
store.replaceReducer(createReducer())
- cancel effects前面一分析了,整个model的effect tasks移除
- unlisteners在之前订阅时已经收集过了,所以直接根据namespace取消就好
- 最后记得移除app._models里对应的model
function unmodel(createReducer, reducers, unlisteners, namespace) {
const store = app._store;
// Delete reducers
delete store.asyncReducers[namespace];
delete reducers[namespace];
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);
}
最后app.replaceModel逻辑其实就是unmodel后model。
还有就是,内部@@dva
的model,是会在replaceModel与unmodel中进行update的action:state自增1。
这样整个dva框架的流程就走完了。挺轻量巧妙的。
编外:
dva还提供有一些工具函数:fetch,dynamic
fetch只是export了isomorphic-fetch
包
dynamic动态加载model与视图
用法:
app: dva 实例,加载 models 时需要
models: 返回 Promise 数组的函数,Promise 返回 dva model
component:返回 Promise 的函数,Promise 返回 React Component
const UserPageComponent = dynamic({
app,
models: () => [
import('./models/users'),
],
component: () => import('./routes/UserPage'),
});
- 传入resolve返回一个async组件。获取所有的models和component,model可为空。
- 组件中有一个AsyncComponent的state,render函数是根据AsyncComponent是否为空去渲染的,所以只要resolve后更新state就好了,这里也了解到一点,组件挂载前不需要使用setState去更新state。
- 可使用dynamic.setDefaultLoadingComponent去设置加载时的过度组件。
function dynamic(config) {
const { app, models: resolveModels, component: resolveComponent } = config;
return asyncComponent({
resolve: config.resolve || function () {
const models = typeof resolveModels === 'function' ? resolveModels() : [];
const component = resolveComponent();
return new Promise((resolve) => {
Promise.all([...models, component]).then((ret) => {
if (!models || !models.length) {
return resolve(ret[0]);
} else {
const len = models.length;
ret.slice(0, len).forEach((m) => {
m = m.default || m;
if (!Array.isArray(m)) {
m = [m];
}
m.map(_ => registerModel(app, _));
});
resolve(ret[len]);
}
});
});
},
...config,
});
}
function asyncComponent(config) {
const { resolve } = config;
return class DynamicComponent extends Component {
constructor(...args) {
super(...args);
this.LoadingComponent =
config.LoadingComponent || defaultLoadingComponent;
this.state = {
AsyncComponent: null,
};
this.load();
}
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
load() {
resolve().then((m) => {
const AsyncComponent = m.default || m;
if (this.mounted) {
this.setState({ AsyncComponent });
} else {
this.state.AsyncComponent = AsyncComponent; // eslint-disable-line
}
});
}
render() {
const { AsyncComponent } = this.state;
const { LoadingComponent } = this;
if (AsyncComponent) return ;
return ;
}
};
}
到此,对dva源码分析完成。接着下篇学习及分析umi