内容简介:dva源码解析(一)
写在前面
dva 是蚂蚁金服推出的一个单页应用框架,对 redux , react-router , redux-saga 进行了上层封装,没有引入新的概念,但是极大的程度上提升了开发效率;下面内容为本人理解,如有错误,还请指出,不胜感激。
redux的痛苦
redux 的优点很多,痛点也有,比如异步控制, redux-saga 的出现使得异步操作变得优雅,但是基于 redux-saga 不得不承认的一点就是开发过程实在是太麻烦了,假若增加一个操作,不得不操作 actions , reducers , sagas ,对于 sagas 可以还需要进行 watch ,而后还要进行 fork ;(PS: 本来就够麻烦了,再加上一个 sagas );在添加一个操作时,不得不操作这么多的文件,实在是麻烦,而 dva 的出现在一定程度上解决了这个问题。
dva基本概念
未使用 dva 下的目录经常是这样的:
actions --/ user.js --/ team.js reducers --/ user.js --/ team.js sagas/ --/ user.js --/ team.js
dva 将其合并:
models --/ user.js --/ team.js
dva 中有着几个概念:
namespace => combineReducers中对应的key值 state => 对应初始的state,也就是initialState effects => saga的处理函数 reducers => 对应reducers,不同的是,写法上将switch...case转化为对象
除了这些以外, dva 中还有 subscriptions ,这一概念来源于 elm ,
dva的实现
初始化
const app = dva({
history: browserHistory
});
上面的过程发生了什么?
dva 本质上调用了下面函数:
function dva(hooks = {}) {
const history = hooks.history || defaultHistory;
const initialState = hooks.initialState || {};
delete hooks.history;
delete hooks.initialState;
const plugin = new Plugin();
plugin.use(hooks);
const app = {
// properties
_models: [],
_router: null,
_store: null,
_history: null,
_plugin: plugin,
_getProvider: null,
// methods
use,
model,
router,
start,
};
return app;
}
hooks 为传入的一些配置,例如可以通过传入 history 来改变路由的实现, dva 默认采用的是 hashHistory ;从这里可以看出 dva 暴露出来的方法:
-
app.router():指定路由,需要传入一个函数,一般类似于({ history }) => (<Router>...</Router>) -
app.use():添加插件,这个稍后来看~ -
app.model():添加model,也就是对应的添加一个store下的数据,该方法做的就是对传入的model进行检查,对reducers添加命名空间,而后将其push到_models中。-
namespace必须且唯一,因为内置了react-redux-router,所以namespace也不能为routing -
subscriptions与effects均为可选参数,传入的话必须为对象 -
reducers为可选,支持对象和数组两种传入方式(传入数组的方式,往往伴随着高阶reducer的应用,具体稍后再看~)
-
-
app.start():初始化应用,接受参数为选择器或者DOM节点
需要注意的是:
-
reducers和effects的key不需要用namespace/action的形式了,因为dva会自动将其加上,dispatch的时候,saga需要加上namespace,而saga中的put不需要加入namespace,原因是dva对put进行了重载 -
dva同时支持rn应用,引入dva/mobile即可,这时react-router不在需要,利用rn中的Navigator即可,不会引用react-router与react-redux-router,namespace可以命名为routing;正是由于这点差异,作者将路由相关的内容作为参数传入了进去,具体可以参见这个文件。
创建
将一些配置项初始化好后,就可以 app.start 就是来创建一个应用,下面就一点点的看看 start 的过程(以下基于默认情况,也就是使用了 react-router ):
-
参数校验,是否为
DOM元素或者检查是否可以根据传入的选择器字符串找到对应的DOM,这个DOM对应的就是ReactDOM.render的第二个参数。 -
错误处理,使得发生错误时,不至于应用奔溃,当然需要传入自定义
hooks.onError来处理:
// 传入hooks.onError则调用,反之调用默认函数处理,抛出异常即可
const onError = plugin.apply('onError', (err) => {
throw new Error(err.stack || err);
});
// 目的是出现错误时,也可以进行dispatch操作
const onErrorWrapper = (err) => {
if (err) {
if (typeof err === 'string') err = new Error(err);
onError(err, app._store.dispatch);
}
};
-
遍历
_models,初始化reducers,sagas
const sagas = [];
// initalReducer为{ routing: routerReducer }
const reducers = { ...initialReducer }; // 为rootReducer
for (const m of this._models) {
// 得到默认的state
reducers[m.namespace] = getReducer(m.reducers, m.state);
if (m.effects) sagas.push(getSaga(m.effects, m, onErrorWrapper));
}
处理reducers
对于 redux 的 reducers 最常见的是基于 switch..case 的,而 dva 做出了一些改变,将每一个 case 分支变作了一个函数:
(PS: 本人认为,这个可以块可以更改,利用 some 操作来尽可能少的调用无意义的 reducer ,于是我提了一个pr)
每一个 reducer 的实现如下:
// actionType对应的是dva的reducers中的key值
(state, action) => {
const { type } = action;
if (type && actionType !== type) {
return state;
}
return reducer(state, action);
};
处理sagas
看完了对于 reducers 的处理,下面来看一下对于 sagas 的处理:
function getSaga(effects, model, onError) {
return function *() {
for (const key in effects) {
if (Object.prototype.hasOwnProperty.call(effects, key)) {
const watcher = getWatcher(key, effects[key], model, onError);
const task = yield sagaEffects.fork(watcher);
// 为了移除时可以将saga任务注销
yield sagaEffects.fork(function *() {
yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
yield sagaEffects.cancel(task);
});
}
}
};
}
getWatcher 返回一个 saga 监听函数,也就是通常写的 watchXXX , model.effects[key] 可以是一个任务函数;也可以是个数组,第一个参数为任务函数,第二为配置对象,可以传入 type , type 有4个可选值, takeEvery (默认), takeLatest , throttle , watcher 四种, dva 对 effects 做了一个错误处理:
effect => function *(...args) {
try {
yield effect(...args.concat(createEffects(model)));
} catch (e) {
onError(e); // 为之前的onErrorWrapper
}
}
注意:
-
watcher是指传入的任务函数就是一个watcher直接fork就好 -
throttle还要传入一个ms配置,这个ms代表着在多少毫秒内只触发一次同一类型saga任务,而takeEvery是不会限制同一类型执行次数,takeLatest只能执行一个同一类型任务,有执行中的再次执行就会取消 -
由
getSaga可以看出,${namespace}/@@CANCEL_EFFECTS可以取消对应的任务监听 -
可以通过配置
hooks.onEffect来增加saga的watcher
增强 redux
-
redux中间件,由sagaMiddware,routerMiddware(启用react-router时),hooks.onAction传入的其它中间件,如redux-logger等 -
其它增强,如
redux-devtools,内置了redux-devtools,另需的话在hooks.extraReducers传入
const enhancers = [
applyMiddleware(...middlewares),
devtools(),
...extraEnhancers,
];
const store = this._store = createStore( // eslint-disable-line
createReducer(),
initialState,
compose(...enhancers),
);
设置redux的回调函数
通过配置 hooks.onStateChange 可以指定 redux 的 state 改变后所触发的回调函数:
const listeners = plugin.get('onStateChange');
for (const listener of listeners) {
store.subscribe(() => {
listener(store.getState());
});
}
}
新概念subscriptions
subscriptions 是一个新概念,会在 dom ready 之后执行,在这里面可以做一些基础数据的获取:
一般会将初始数据的获取放在 react 的生命周期中,比如 componentWillMount ,但是假设我们做了代码分割,实现了按需加载,那么我们开始获取数据的时间为:获取相应的 js +解析 js +执行 react 生命周期,但是 redux 的数据加载和 ui 组件没有太大关系,可以将数据获取的时间点提前, subscriptions 提供了解决方法,其意义为订阅,对于上面的场景,我们可以订阅路由,到了执行的路由执行相应的 dispatch() ,如:
setup({ dispatch, history }) {
return history.listen(({ pathname, query }) => {
if (pathname === '/users') {
dispatch({ type: 'fetch', payload: query });
}
});
}
(PS: 对于这个新概念,我也不是很清楚,后面的文章会有专门的描述,大家先有一个概念就好)
挂载
上述过程均为初始化的过程,就是获取到需要的 reducers , sagas 以及对于一些中间件和插件的配置,下面要进行的就是挂载了,也就熟悉的 render(<Provider>, container) 。
动态处理model
dva.model 与 dva.unmodel ,封装了在运行时的 store 进行一类增加和删除的操作,例如可以再切换到某一路由时动态的加入一个 model (个人猜测,热更新很有可能也利用了这个两个 api 与 hooks.onHmr )。
未完结
关于 redux 还有一个利器,那就是高阶 reduce ,当然在 dva 中也有体现,这篇文章已经很长了,这些内容留在下一篇中介绍。以上是本人对于 dva 的粗略的理解,内容如有错误,还请大家指出。 dva 的确简化了开发的流程,而且在蚂蚁金服的很多业务线也有着应用,是一个很值得大家一试!
以上所述就是小编给大家介绍的《dva源码解析(一)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- ReactNative源码解析-初识源码
- Spring源码系列:BeanDefinition源码解析
- Spring源码分析:AOP源码解析(下篇)
- Spring源码分析:AOP源码解析(上篇)
- 注册中心 Eureka 源码解析 —— EndPoint 与 解析器
- 新一代Json解析库Moshi源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。