Read Dva
XXHolic opened this issue · comments
目录
引子
看了下 Dva 源码,整理一下个人理解。
源码版本 dva@2.6.0-beta.20 。
简介
dva 是一个基于 redux 和 redux-saga 的数据流方案,为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。更多信息见这里。
了解 redux 和 redux-saga 原理有助于更快理解其中的逻辑。相关解读可见 Read Redux、Read Redux-Saga 。
下面以计数器的例子进行相关介绍。
源码
计数器示例
import React from 'react';
import dva, { connect } from 'dva';
// 1. Initialize
const app = dva();
// 2. Model
app.model({
namespace: 'count',
state: 0,
reducers: {
add (count) { return count + 1 },
minus(count) { return count - 1 },
},
});
// 3. View
const App = connect(({ count }) => ({
count
}))(function(props) {
return (
<div>
<h2>{ props.count }</h2>
<button key="add" onClick={() => { props.dispatch({type: 'count/add'})}}>+</button>
<button key="minus" onClick={() => { props.dispatch({type: 'count/minus'})}}>-</button>
</div>
);
});
// 4. Router
app.router(() => <App />);
// 5. Start
app.start('#root');
接下来按照计数器示例中的步骤分别介绍。
Initialize
初始化时执行的 dva
方法,所在源文件为 index.js 。主要逻辑如下:
相关主要源码
import React from 'react';
import { createHashHistory } from 'history';
import { create } from 'dva-core';
import * as routerRedux from 'connected-react-router';
const { connectRouter, routerMiddleware } = routerRedux;
export default function(opts = {}) {
const history = opts.history || createHashHistory();
const createOpts = {
initialReducer: {
router: connectRouter(history),
},
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
const app = create(opts, createOpts);
const oldAppStart = app.start;
app.router = router;
app.start = start;
return app;
function router(router) {} // 路由注册
function start(container) {} // 开始渲染
}
从代码中可以发现:
- 创建了路由对象
history
,并将history
与router
结合,并重写了listen
方法。 - 创建的
app
对象主要方法 create ,提供了初始化的_model
数据,_store
默认为null
,并定义了主要方法modal
和start
。 - 创建了
app
对象后,添加了新的router
方法,并重新定义了create()
返回的start
方法。
Model
执行对象 app
所拥有的 model
方法,所在源文件为 index.js 。
相关主要源码
function model(m) {
const prefixedModel = prefixNamespace({ ...m });
app._models.push(prefixedModel);
return prefixedModel;
}
prefixNamespace 方法:
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;
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
方法将传入的对象,根据特定的字段namespace
、reducers
、effects
分别进行归类,并放入到统一的字段_modal
中。- 注意这里重新组装的时候,
reducers
和effects
都在原有的基础上,添加了namespace/
的格式前缀。
View
这步是主要的显示逻辑,计数器示例中使用了 connect 方法,该方法从全局的 store
中匹配对应的 state
数据,然后传入到组件中。
Router
router
方法在第一步 Initialize 中就已经定义好了:
function router(router) {
invariant(
isFunction(router),
`[app.router] router should be function, but got ${typeof router}`,
);
app._router = router;
}
计数器示例中传入了一个函数,作用是进行路由注册,并放入到另外一个变量 _router
中,在后面就会用到。
Start
start
方法也在第一步 Initialize 中就已经定义好了:
function start(container) {
// 允许 container 是字符串,然后用 querySelector 找元素
if (isString(container)) {
container = document.querySelector(container);
}
// 并且是 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);
}
}
从代码中可以发现:
- 首先是找到渲染的容器。
- 在前面初始化中
_store
为null
,于是第一次会执行create()
返回对象中的 start 方法,该方法将 redux 和 redux-saga 进行了初始化并关联,强化了原有的model
方法,添加了unmodel
和replaceModel
方法,为后续做好了数据和方法准备。 - 最后使用了 Provider 进行包裹渲染,这个是跟前面
connect
方法相呼应。