一个action可以被Redux-Saga和Reducer同时响应,比如上面的FETCH_USER_INFO发出后我还想让页面转个圈,可以直接在reducer里面加一个就行:
... case 'FETCH_USER_INFO': return { ...state, isLoading: true }; ... 手写源码通过上面这个例子,我们可以看出,Redux-Saga的运行是通过这一行代码来实现的:
sagaMiddleware.run(rootSaga);整个Redux-Saga的运行和原本的Redux并不冲突,Redux甚至都不知道他的存在,他们之间耦合很小,只在需要的时候通过put发出action来进行通讯。所以我猜测,他应该是自己实现了一套完全独立的异步任务处理机制,下面我们从能感知到的API入手,一步一步来探寻下他源码的奥秘吧。本文全部代码参照官方源码写成,函数名字和变量名字尽量保持一致,写到具体的方法的时候我也会贴出对应的代码地址,主要代码都在这里:https://github.com/redux-saga/redux-saga/tree/master/packages/core/src
先来看看我们用到了哪些API,这些API就是我们今天手写的目标:
createSagaMiddleware:这个方法会返回一个中间件实例sagaMiddleware
sagaMiddleware.run: 这个方法是真正运行我们写的saga的入口
takeEvery:这个方法是用来控制并发流程的
call:用来调用其他方法
put:发出action,用来和Redux通讯
从中间件入手,一个中间件大概就长这个样子:
function logger(store) { return function(next) { return function(action) { console.group(action.type); console.info('dispatching', action); let result = next(action); console.log('next state', store.getState()); console.groupEnd(); return result } } }这其实就相当于一个Redux中间件的范式了:
一个中间件接收store作为参数,会返回一个函数
返回的这个函数接收老的dispatch函数作为参数(也就是上面的next),会返回一个新的函数
返回的新函数就是新的dispatch函数,这个函数里面可以拿到外面两层传进来的store和老dispatch函数
依照这个范式以及前面对createSagaMiddleware的使用,我们可以先写出这个函数的骨架:
// sagaMiddlewareFactory其实就是我们外面使用的createSagaMiddleware function sagaMiddlewareFactory() { // 返回的是一个Redux中间件 // 需要符合他的范式 const sagaMiddleware = function (store) { return function (next) { return function (action) { // 内容先写个空的 let result = next(action); return result; } } } // sagaMiddleware上还有个run方法 // 是用来启动saga的 // 我们先留空吧 sagaMiddleware.run = () => { } return sagaMiddleware; } export default sagaMiddlewareFactory; 梳理架构现在我们有了一个空的骨架,接下来该干啥呢?前面我们说过了,Redux-Saga很可能是自己实现了一套完全独立的异步事件处理机制。这种异步事件处理机制需要一个处理中心来存储事件和处理函数,还需要一个方法来触发队列中的事件的执行,再回看前面的使用的API,我们发现了两个类似功能的API:
takeEvery(action, callback):他接收的参数就是action和callback,而且我们在根saga里面可能会多次调用它来注册不同action的处理函数,这其实就相当于往处理中心里面塞入事件了。
put(action):put的参数是action,他唯一的作用就是触发对应事件的回调运行。
可以看到Redux-Saga这种机制也是用takeEvery先注册回调,然后使用put发出消息来触发回调执行,这其实跟我们其他文章多次提到的发布订阅模式很像。
手写channelchannel是Redux-Saga保存回调和触发回调的地方,类似于发布订阅模式,我们先来写个:
export function multicastChannel() { const currentTakers = []; // 一个变量存储我们所有注册的事件和回调 // 保存事件和回调的函数 // Redux-Saga里面take接收回调cb和匹配方法matcher两个参数 // 事实上take到的事件名称也被封装到了matcher里面 function take(cb, matcher) { cb['MATCH'] = matcher; currentTakers.push(cb); } function put(input) { const takers = currentTakers; for (let i = 0, len = takers.length; i < len; i++) { const taker = takers[i] // 这里的'MATCH'是上面take塞进来的匹配方法 // 如果匹配上了就将回调拿出来执行 if (taker['MATCH'](input)) { taker(input); } } } return { take, put } }