getSaga
概述
getSaga 是 dva 内部方法,主要作用是根据 model.effects 生成对应的 saga 函数,在 model ummodel replaceModel 等 api 里面都有调用,具体可以去看core.create。
源码地址
dva/packages/dva-core/src/getSaga.js
解析
getSaga
可以看到 getSaga 函数的返回值是一个 Generator 方法,在这个方法里面对传入的 effects 对象进行遍历,然后判断 key 是否是 effects 的自身属性,如果不是则什么都不做,如果是会做如下几件事:
- 调用 getWatcher 生成 watcher,这个 getWatcher 方法会在下面讲
- 调用 fork 传入 watcher
- 调用 fork 传入一个匿名 Generator 方法,这个方法 take 了一个
${model.namespace}/@@CANCEL_EFFECTSaction,当这个 action 触发的时候说明调用了 unmodel 或 replaceModel 卸载了 model,此时就会触发这个 action 接着调用 cancel 取消这个 saga
提示
这块涉及到了一些 redux-saga 相关的知识点,如果对 redux-saga 的源码感兴趣可以看我的这篇文章。
export 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);
});
}
}
};
}
getWatcher
getWatcher 方法的目的是根据 effects 生成 watcher generator,effects 中每一项的 key 就是监听的 action。 因为 getWatcher 方法比较大,所以我将其分成了四部分进行讲解。
function getWatcher(key, _effect, model, onError, onEffect) {
let effect = _effect;
let type = 'takeEvery';
let ms;
// --------- 1. 校验参数 ----------
if (Array.isArray(_effect)) {
// ......
}
function noop() {}
// --------- 3. sagaWithCatch --------
function* sagaWithCatch(...args) {
// ......
}
// --------- 4. sagaWithOnEffect ------------
const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
// --------- 2. switch ----------
switch (type) {
// ......
}
}
1. 校验参数
首先是对参数进行校验,这个比较简单,effects 的 item 可以是一个方法或是一个数组,所以首先判断 effect 类型,其次如果是数组类型,数组里面第二项的值表示 watcher 的类型一共有四种:watcher takeEvery takeLatest throttle,如果是 throttle 则必须要有参数 ms;最后判断 type 是否属于上述四种类型之一。
if (Array.isArray(_effect)) {
effect = _effect[0];
const opts = _effect[1];
if (opts && opts.type) {
type = opts.type;
if (type === 'throttle') {
invariant(
opts.ms,
'app.start: opts.ms should be defined if type is throttle'
);
ms = opts.ms;
}
}
invariant(
['watcher', 'takeEvery', 'takeLatest', 'throttle'].indexOf(type) > -1,
'app.start: effect type should be takeEvery, takeLatest, throttle or watcher'
);
}
2. switch 虽然 switch 是放在最后面执行的,但是内部调用了第三步和第四步定义的方法,所以为了方便后续理解,先讲述 switch 方法,switch 方法对你传入的 type 进行判断:
- watcher: 返回 sagaWithCatch 方法,下面我们会讲到
- takeLatest: 返回一个 Generator 方法里面执行 takeLatest,参数是 key(effect 的 key)和 sagaWithOnEffect,sagaWithOnEffect 方法我们下面会讲到
- throttle: 同 takeLatest 只不过多了 ms 参数
- default: 对于没有 type 或是以对象形式定义的 effect 默认用 takeEvery 处理
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);
};
}
3. sagaWithCatch
sagaWithCatch 的作用是处理 watcher,老实说我并没有在官方文档上找到 watcher 类型的作用是什么,我们只好通过代码来理解了。
- 在调用 effect 方法之前会先 put 一个 start action
- 接着调用 effect 方法,参数这里调用了 createEffects 这个我们会在下面讲述
- effect 执行完毕之后会 put 一个 end action
- 如果有错误则调用 onError 全局错误处理方法
提示
__dva_resolve 和 __dva_reject 和 promiseMiddleware 有关,主要作用是当 dispatch 一个 effect 时返回一个 promise。
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` });
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);
}
}
}
4. sagaWithOnEffect sagaWithOnEffect 是 takeLatest throttle takeEvery 的 saga 参数,我们发现它是 applyOnEffect 的返回值,我们接下来去看看这个方法。 applyOnEffect 方法是处理 onEffect 这个 hook 的,如果 onEffect 数组里面有值则会一次调用钩子函数,否则返回 effect 也就是 sagaWithCatch,所以本质上无论是 watcher 类型还是其它类型最终都是调用的 sagaWithCatch。
提示
onEffect 的值是 plugin.get('onEffect') 如果想了解 plugin 请看这篇文章
const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
function applyOnEffect(fns, effect, model, key) {
for (const fn of fns) {
effect = fn(effect, sagaEffects, model, key);
}
return effect;
}
5. createEffects
createEffects 是 sagaWithCatch 方法内部调用 effect 方法时生成参数的函数,所以它的作用就是将 redux-saga 的 effect creators 传入 effect 方法中,这样你才能这样使用:
下方 { put call } 就是通过 createEffects 生成的
effects: {
*addRemote({ payload: todo }, { put, call }) {
yield call(addTodo, todo);
yield put({ type: 'add', payload: todo });
},
},
- assertAction: 首先定义了一个 assertAction 方法,这个方法用于对 type 做一些校验
- put: put 方法是对于 redux-saga 的 put 方法做了一层封装,它首先调用 assertAction 对 type 做校验,接着才调用 saga 的 put 方法,注意它会对 type 用 prefixType 方法做一个处理,这个方法比较简单,所以代码我就不贴了,简单说一下就是如果 put 的 action type 在 model 的 effects 或是 reducers 里面就加一个前缀:
${model.namespace}${NAMESPACE_SEP}${type}否则就返回原有的 type - putResolve: putResolve 是对 saga 的 put.resolve 做了一层封装,做的事情和 put 一样,所以就不赘述了,然后
put.resolve = putResolve;用 putResolve 覆盖原方法 - take: take 方法是对 saga 的 take 方法做了一层封装,作用和上面一样
- 最后将新的 put take 和其它 effects 一起返回
提示
- 为何只对 take 和 put 方法做处理,我认为是因为它俩的参数有 action,而 dva 需要对 action.type 做添加前缀的处理,所以才会对这两个方法进行封装
- sagaEffects 是所有 redux-saga 导出的 effects creator,这个通过 import 整体加载实现的,详情可以看源码
function createEffects(model) {
function assertAction(type, name) {
invariant(type, 'dispatch: action should be a plain Object with type');
warning(
type.indexOf(`${model.namespace}${NAMESPACE_SEP}`) !== 0,
`[${name}] ${type} should not be prefixed with namespace ${
model.namespace
}`
);
}
function put(action) {
const { type } = action;
assertAction(type, 'sagaEffects.put');
return sagaEffects.put({ ...action, type: prefixType(type, model) });
}
function putResolve(action) {
const { type } = action;
assertAction(type, 'sagaEffects.put.resolve');
return sagaEffects.put.resolve({
...action,
type: prefixType(type, model),
});
}
put.resolve = putResolve;
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 };
}