首先这是一篇找妈妈的故事, model中的state,reducers,effects,是如何找到各自的妈妈呢?23333~
dva 是对redux、react-redux、react-router、redux-saga的整合,所以在看dva源码之前建议先要熟悉这些库的用法。
废话不多说,那我们就开始。先看一下生成的 index.js 文件,在这里我加入了dva-loading,用来分析plugin。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import dva from 'dva';
import createLoading from 'dva-loading';
// 1. Initialize
const app = dva();
// 2. Plugins
app.use(createLoading());
// 3. Model
app.model(require('./models/example').default);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root');
可以看到,dva生成app实例,并且绑定plugin、models、router,最后启动项目。
先去看一下dva的源码,位置是dva/src/index.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47export default function (opts = {}) {
const history = opts.history || createHashHistory();
const createOpts = {
initialReducer: {
routing,
},
setupMiddlewares(middlewares) {
return [
routerMiddleware(history),
...middlewares,
];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
const app = core.create(opts, createOpts);
const oldAppStart = app.start;
app.router = router;
app.start = start;
return app;
function router(router) {
app._router = router;
}
function start(container) {
// 允许 container 是字符串,然后用 querySelector 找元素
if (isString(container)) {
container = document.querySelector(container);
}
if (!app._store) {
oldAppStart.call(app);
}
const store = app._store;
// 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);
}
}
}
上面代码重点在两句
const app = core.create(opts, createOpts);
利用dva-core生成数据层appapp.start = start;
由于dva-core仅仅是数据层,所以这里需要代理start方法,建立view层,比如react、router
接下来我想会重点分析model方法(这是整个dva的核心部分) 位置是dva-core/index.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26export 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;
// 注册model,将model里的reducers与effects方法增加namespace,并且保存在app._models中
function model(m) {
if (process.env.NODE_ENV !== 'production') {
checkModel(m, app._models);
}
const prefixedModel = prefixNamespace({ ...m });
app._models.push(prefixedModel);
return prefixedModel;
}
}
在一个项目中我们会注册多个model,现在models都已经被存在了app_models中。dva是如何将每个model里的state,reducers, effects,
放在redux与redux-saga中的呢?答案就在 app.start 里。start里主要是redux和redux-saga的初始化1
2
3
4
5
6
7
8
9
10const reducers = { ...initialReducer };
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')));
}
上面代码遍历models分别将reducers和effects存下来,先看一下reducer的处理1
2
3
4
5
6
7
8
9
10
11export default function getReducer(reducers, state, handleActions) {
// Support reducer enhancer
// e.g. reducers: [realReducers, enhancer]
if (Array.isArray(reducers)) {
return reducers[1](
(handleActions || defaultHandleActions)(reducers[0], state)
);
} else {
return (handleActions || defaultHandleActions)(reducers || {}, state);
}
}
一般情况下都不会是数组,话说看到这里我才知道原来还可以加一个enhancer。在去看一下defaultHandleActions
1
2
3
4
5
6
7function handleActions(handlers, defaultState) {
const reducers = Object.keys(handlers).map(type =>
handleAction(type, handlers[type])
);
const reducer = reduceReducers(...reducers);
return (state = defaultState, action) => reducer(state, action);
}
在这里遍历了reducers里的每一个key值,并把一个models下的reduers合并为一个。1
2
3
4
5
6
7
8
9
10function handleAction(actionType, reducer = identify) {
return (state, action) => {
const { type } = action;
invariant(type, 'dispatch: action should be a plain Object with type');
if (actionType === type) {
return reducer(state, action);
}
return state;
};
}
关键在于 if (actionType === type)
, 判断类型不同就返回state,类似于 switch…case。
下面看一下合并reducer的函数1
2
3
4function reduceReducers(...reducers) {
return (previous, current) =>
reducers.reduce((p, r) => r(p, current), previous);
}
这里的合并方式我是有点不太理解的,
这里采用的是reducer1 -> reducer2 -> reducer3,假如有一个action,models里的handleAction要全跑一遍。
为什么不直接找到相应键值的reducer运行呢?,reducer已经分析完了然后就是effects1
2
3
4
5
6
7
8
9
10
11
12
13
14export default function getSaga(effects, model, onError, onEffect) {
return function*() {
for (const key in effects) {
if (Object.prototype.hasOwnProperty.call(effects, key)) {
const watcher = getWatcher(key, effects[key], model, onError, onEffect);
const task = yield sagaEffects.fork(watcher);
yield sagaEffects.fork(function*() {
yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
yield sagaEffects.cancel(task);
});
}
}
};
}
fork每一个effect,然后是 getWatcher 函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// onEffect就是plugin中的OnEffect, 就如开头的那个例子dva-loading中就有一个onEffect
function getWatcher(key, _effect, model, onError, onEffect) {
// 这里给每一个effect增加了开始、结束的事件和错误处理,并且__dva_resolve, __dva_reject是PromiseMiddleware的参数。
// PromiseMiddleware的作用是当你dispatch effect时,返回一个promise。this.props.dispatch.then这样的写法你一定熟悉。
function* sagaWithCatch(...args) {
const { __dva_resolve: resolve = noop, __dva_reject: reject = noop } =
args.length > 0 ? args[0] : {};
try {
yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });
// createEffects 封装了redux-saga的effects,主要是改变了type值,这就是你为什么可以在当前model里省略namespace的原因
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);
}
}
}
// 如果插件中有onEffects,则再封装一层
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);
};
default:
return function*() {
yield takeEvery(key, sagaWithOnEffect);
};
}
}
到目前为止我们已经处理好了reduer与effects,接下来就是redux与redux-saga的初始化工作了
首先是redux1
2
3
4
5
6
7
8
9const store = (app._store = createStore({
// eslint-disable-line
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin,
createOpts,
sagaMiddleware,
promiseMiddleware,
}));
redux-saga1
sagas.forEach(sagaMiddleware.run);
ola, dva的源码差不多就到这里了,再去看一个dva-loading是如何工作的。
先想一下dva-loading作用是监听effects的开始和结束,并把状态存在store里,那么他肯定需要一个reducer用来存储状态,和effect用来抛出action1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46const initialState = {
global: false,
models: {},
effects: {},
};
const extraReducers = {
[namespace](state = initialState, { type, payload }) {
const { namespace, actionType } = payload || {};
let ret;
switch (type) {
case SHOW:
ret = {
...state,
global: true,
models: { ...state.models, [namespace]: true },
effects: { ...state.effects, [actionType]: true },
};
break;
case HIDE: // eslint-disable-line
res = {...}
break;
default:
ret = state;
break;
}
return ret;
},
};
function onEffect(effect, { put }, model, actionType) {
const { namespace } = model;
if (
(only.length === 0 && except.length === 0)
|| (only.length > 0 && only.indexOf(actionType) !== -1)
|| (except.length > 0 && except.indexOf(actionType) === -1)
) {
return function*(...args) {
yield put({ type: SHOW, payload: { namespace, actionType } });
yield effect(...args);
yield put({ type: HIDE, payload: { namespace, actionType } });
};
} else {
return effect;
}
}
onEffect在之前getWatcher中已经讲过,而extraReducers最后会通过combineReducers合并
总结
dva的源码已经分析完了,看完这篇文章相信你已经对dva的工作方式有了大致的了解。下期我会分享react源码。