上述代码中有一个奇怪的点,就是将matcher作为属性放到了回调函数上,这么做的原因我想是为了让外部可以自定义匹配方法,而不是简单的事件名称匹配,事实上Redux-Saga本身就支持好几种匹配模式,包括字符串,Symbol,数组等等。
内置支持的匹配方法可以看这里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/matcher.js。
channel对应的源码可以看这里:
有了channel之后,我们的中间件里面其实只要再干一件事情就行了,就是调用channel.put将接收的action再发给channel去执行回调就行,所以我们加一行代码:
// ... 省略前面代码 const result = next(action); channel.put(action); // 将收到的action也发给Redux-Saga return result; // ... 省略后面代码 sagaMiddleware.run前面的put是发出事件,执行回调,可是我们的回调还没注册呢,那注册回调应该在什么地方呢?看起来只有一个地方了,那就是sagaMiddleware.run。简单来说,sagaMiddleware.run接收一个Generator作为参数,然后执行这个Generator,当遇到take的时候就将它注册到channel上面去。这里我们先实现take,takeEvery是在这个基础上实现的。Redux-Saga中这块代码是单独抽取了一个文件,我们仿照这种做法吧。
首先需要在中间件里面将Redux的getState和dispatch等参数传递进去,Redux-Saga使用的是bind函数,所以中间件方法改造如下:
function sagaMiddleware({ getState, dispatch }) { // 将getState, dispatch通过bind传给runSaga boundRunSaga = runSaga.bind(null, { channel, dispatch, getState, }) return function (next) { return function (action) { const result = next(action); channel.put(action); return result; } } }然后sagaMiddleware.run就直接将boundRunSaga拿来运行就行了:
sagaMiddleware.run = (...args) => { boundRunSaga(...args) }注意这里的...args,这个其实就是我们传进去的rootSaga。到这里其实中间件部分就已经完成了,后面的代码就是具体的执行过程了。
中间件对应的源码可以看这里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/middleware.js
runSagarunSaga其实才是真正的sagaMiddleware.run,通过前面的分析,我们已经知道他的作用是接收Generator并执行,如果遇到take就将它注册到channel上去,如果遇到put就将对应的回调拿出来执行,但是Redux-Saga又将这个过程分为了好几层,我们一层一层来看吧。runSaga的参数先是通过bind传入了一些上下文相关的变量,比如getState, dispatch,然后又在运行的时候传入了rootSaga,所以他应该是长这个样子的:
import proc from './proc'; export function runSaga( { channel, dispatch, getState }, saga, ...args ) { // saga是一个Generator,运行后得到一个迭代器 const iterator = saga(...args); const env = { channel, dispatch, getState, }; proc(env, iterator); }可以看到runSaga仅仅是将Generator运行下,得到迭代器对象后又调用了proc来处理。
runSaga对应的源码看这里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/runSaga.js
procproc就是具体执行这个迭代器的过程,Generator的执行方式我们之前在另一篇文章详细讲过,简单来说就是可以另外写一个方法next来执行Generator,next里面检测到如果Generator没有执行完,就继续执行next,然后外层调用一下next启动这个流程就行。
export default function proc(env, iterator) { // 调用next启动迭代器执行 next(); // next函数也不复杂 // 就是执行iterator function next(arg, isErr) { let result; if (isErr) { result = iterator.throw(arg); } else { result = iterator.next(arg); } // 如果他没结束,就继续next // digestEffect是处理当前步骤返回值的函数 // 继续执行的next也由他来调用 if (!result.done) { digestEffect(result.value, next) } } } digestEffect上面如果迭代器没有执行完,我们会将它的值传给digestEffect处理,那么这里的result.value的值是什么的呢?回想下我们前面rootSaga里面的用法
yield takeEvery("FETCH_USER_INFO", fetchUserInfo);result.value的值应该是yield后面的值,也就是takeEvery("FETCH_USER_INFO", fetchUserInfo)的返回值,takeEvery是再次包装过的effect,他包装了take,fork这些简单的effect。其实对于像take这种简单的effect来说,比如:
take("FETCH_USER_INFO", fetchUserInfo);这行代码的返回值直接就是一个对象,类似于这样:
{ IO: true, type: 'TAKE', payload: {}, }所以我们这里digestEffect拿到的result.value也是这样的一个对象,这个对象就代表了我们的一个effect,所以我们的digestEffect就长这样:
function digestEffect(effect, cb) { // 这个cb其实就是前面传进来的next // 这个变量是用来解决竞争问题的 let effectSettled; function currCb(res, isErr) { // 如果已经运行过了,直接return if (effectSettled) { return } effectSettled = true; cb(res, isErr); } runEffect(effect, currCb); } runEffect