由于dva的版本是2.4.1,对应的是redux-saga@0.16.2,所以就解读该版本的代码,本文围绕examples/shopping-cart展开
注册
使用方式
main.js
import rootSaga from './sagas'为了减少篇幅,这里就不贴出shopping-cart的代码
const sagaMiddleware = createSagaMiddleware({sagaMonitor})
const store = createStore(
rootReducer,
applyMiddleware(sagaMiddleware)// 注册saga中间件
)
sagaMiddleware.run(rootSaga) // 启动rootSaga
sagas/index.js
export default function* root() {
const ret = yield all([为了减少篇幅,这里就不贴出shopping-cart的代码
fork(getAllProducts),
fork(watchGetProducts),
fork(watchCheckout),
]);
}
这两种写法等效,all并发类似于Promise.all
export default function* root() {
yield fork(getAllProducts);
yield fork(watchGetProducts);
yield fork(watchCheckout);
}
fork为非阻塞调用,被fork的三个generator都会被执行,直到遇到take,会被阻塞,即使watchGetProducts或者watchCheckout中再去fork其他任务,这些任务也会被执行,这种执行是调用proc.js中的next函数,next函数就是generator的执行器,类似于tj/co,截取一段代码
else {
result = iterator.next(arg) // 遍历iterator,将上次迭代的结果arg传递给下次迭代
}
if (!result.done) {
runEffect(result.value, parentEffectId, '', next) // iterator没有迭代完,继续执行effect,
// result.value是yield右边的值,将next执行器传递下去,其实就是个递归
}
用while可以消费迭代器,但是只能消费同步迭代器,异步迭代器还是需要递归来消费,构建你自己的 redux-saga做了解释。也就是runSaga会执行所有迭代器,只有遇到take这种才会阻塞迭代器的执行,比如getAllProducts
会被执行完watchCheckout
执行到take就会被阻塞,该generator后面的语句就不会被执行,watchGetProducts
中takeEvery后面的代码会被执行,因为takeEvery相当于这样:
const takeEvery = (patternOrChannel, saga, ...args) => fork(function*() {
while (true) {
const action = yield take(patternOrChannel)
yield fork(saga, ...args.concat(action))
}
})
fork是不会阻塞后续代码的执行,但是takeEvery第二个参数saga,就不会被执行了,直到有匹配的patternOrChannel被发起。注册的时候为什么没有take的generator会被执行完,有take的generator会被中断?
- runTakeEffect
proc.js runEffect函数中一段对effect密密麻麻的判断
let data
return (
// Non declarative effect
is.promise(effect) ? resolvePromise(effect, currCb)
: is.helper(effect) ? runForkEffect(wrapHelper(effect), effectId, currCb)
: is.iterator(effect) ? resolveIterator(effect, effectId, name, currCb)
// declarative effects
: is.array(effect) ? runParallelEffect(effect, effectId, currCb)
: (data = asEffect.take(effect)) ? runTakeEffect(data, currCb)
: (data = asEffect.put(effect)) ? runPutEffect(data, currCb)
: (data = asEffect.all(effect)) ? runAllEffect(data, effectId, currCb)
: (data = asEffect.race(effect)) ? runRaceEffect(data, effectId, currCb)
: (data = asEffect.call(effect)) ? runCallEffect(data, effectId, currCb)
: (data = asEffect.cps(effect)) ? runCPSEffect(data, currCb)
: (data = asEffect.fork(effect)) ? runForkEffect(data, effectId, currCb)
: (data = asEffect.join(effect)) ? runJoinEffect(data, currCb)
: (data = asEffect.cancel(effect)) ? runCancelEffect(data, currCb)
: (data = asEffect.select(effect)) ? runSelectEffect(data, currCb)
: (data = asEffect.actionChannel(effect)) ? runChannelEffect(data, currCb)
: (data = asEffect.flush(effect)) ? runFlushEffect(data, currCb)
: (data = asEffect.cancelled(effect)) ? runCancelledEffect(data, currCb)
: (data = asEffect.getContext(effect)) ? runGetContextEffect(data, currCb)
: (data = asEffect.setContext(effect)) ? runSetContextEffect(data, currCb)
: /* anything else returned as is */ currCb(effect)
)
做个测试
let data;
const variable = 8;
const ret = variable === 9 ? "99" : (data = variable === 8) ? "88" : "100";
console.info("ret:", ret); // true
console.info("data:", data); // 88
先做判断,然后将判断结果赋值给data,然后将data作为参数传递给后面的runXxxEffect函数,这里的effect是什么?
effect由next函数对迭代器进行迭代的结果,runEffect(result.value, parentEffectId, '', next)
,
- 比如shopping-cart中
yield take(actions.CHECKOUT_REQUEST)
take函数执行的结果为{ "@@redux-saga/IO": true, TAKE: { pattern: "CHECKOUT_REQUEST" } }
(data = asEffect.take(effect))
对上述结果判断,data被赋值为{ pattern: "CHECKOUT_REQUEST" }
,隐式类型转换为true执行问号后面的runTakeEffect(data, currCb)
- 接着执行
runTakeEffect
function runTakeEffect({ channel, pattern, maybe }, cb) {// 解构后的pattern为'CHECKOUT_REQUEST'
channel = channel || stdChannel // 参数传递的channel为undefined,所以使用stdChannel
// 对回调cb进行包装
const takeCb = inp => (inp instanceof Error ? cb(inp, true) : isEnd(inp) && !maybe ? cb(CHANNEL_END) : cb(inp))
try {
// 建立
channel.take(takeCb, matcher(pattern))
} catch (err) {
return cb(err, true)
}
cb.cancel = takeCb.cancel
}
- matcher是什么?
const matchers = {
wildcard: () => kTrue,
default: (pattern) =>
typeof pattern === "symbol"
? (input) => input.type === pattern
: (input) => input.type === String(pattern), // 最后返回该函数
array: (patterns) => (input) => patterns.some((p) => matcher(p)(input)),
predicate: (predicate) => (input) => predicate(input),
};
function matcher(pattern) { // pattern为'CHECKOUT_REQUEST'
return (
pattern === '*' ? matchers.wildcard
: is.array(pattern) ? matchers.array
: is.stringableFunc(pattern) ? matchers.default
: is.func(pattern) ? matchers.predicate
: matchers.default // pattern为'CHECKOUT_REQUEST',执行default
)(pattern)
}
所以matcher('CHECKOUT_REQUEST')
返回(input) => input.type === String(pattern)
也就是(input) => input.type === String('CHECKOUT_REQUEST')
function runTakeEffect({ channel, pattern, maybe }, cb) {}
cb是什么?
梳理下调用栈:next->runEffect->runTakeEffect->stdChannel->eventChannel->channel
将包含next(generator执行器)的回调函数放进channel函数中的takers数组。这边相当于对next执行器进行了注册,遍历器在这个地方停住了,该generator内的代码不会继续向下执行。
- runCallEffect
runCallEffect
function runCallEffect({ context, fn, args }, effectId, cb) {
let result
try {
result = fn.apply(context, args)
} catch (error) {
return cb(error, true)
}
return is.promise(result)
? resolvePromise(result, cb)
: is.iterator(result) ? resolveIterator(result, effectId, fn.name, cb) : cb(result)
}
将const products = yield call(api.getProducts);
带入到runCallEffect
fn为api.getProducts,api.getProducts返回一个promise,resolvePromise函数对其解析
function resolvePromise(promise, cb) {
promise.then(cb, error => cb(error, true))
}
对runTakeEffect分析知道,cb是包含着对next执行器的调用的回调函数,在这里调用cb就相当于调用了next执行器,这样一来就保证了迭代器继续向下遍历,(runPutEffect跟runCallEffect类似,在其内部对cb直接就调用了,这里不做分析)。但是runTakeEffect并没有立即执行包含next的回调,而是做了channel.take(takeCb, matcher(pattern))
将take的第一个参数pattern跟回调做了关联。那么该generator的迭代器在这里就停了下来,无法往下继续遍历了。怎么才能让该迭代器继续向下遍历呢?看下文
触发
注册完成后,在哪触发的,middleware。
function sagaMiddleware({ getState, dispatch }) {
const sagaEmitter = emitter()
sagaEmitter.emit = (options.emitter || ident)(sagaEmitter.emit)
sagaMiddleware.run = runSaga.bind(null, {
context,
subscribe: sagaEmitter.subscribe,// 将emitter的subscribe提前注入到runSaga的第一个参数中
dispatch,
getState,
sagaMonitor,
logger,
onError,
})
return next => action => {
if (sagaMonitor && sagaMonitor.actionDispatched) {
sagaMonitor.actionDispatched(action)
}
const result = next(action) // hit reducers
sagaEmitter.emit(action) // 触发action
return result
}
}
channel.js emmiter函数
export function emitter() {
const subscribers = []
function subscribe(sub) {
subscribers.push(sub)
return () => remove(subscribers, sub)
}
function emit(item) {
const arr = subscribers.slice()
for (var i = 0, len = arr.length; i < len; i++) {
arr[i](item)
}
}
return {
subscribe,
emit,
}
}
emitter实现了观察者模式,中间件调用subscribers中的函数,这些函数是什么时候存到subscribers中的?
是注册的时候,也就是runSaga执行的时候,emitter的subscribers注入顺序,runsSaga->proc->_stdChannel
上图可以看出通过subscribe将回调函数放进了emitter中的subscribers数组,
middleware emit -> emitter emit遍历调用subscribers的函数 -> stdChannel的eventChannel的subscribe回调函数参数 -> eventChannel的subscribe的回调函数参数 -> chan.put(input) -> 遍历调用takes中的函数
- emitter的subscribers数组中的函数在项目启动的时候被注入进来,(proc.js
const stdChannel = _stdChannel(subscribe)
)fork也会注入,因为调用了proc函数 - channel的takers数组中的函数在
yield take
的时候被注入进来
在stdChannel函数中将channel的观察者跟emitter观察者实现了关联。我称之为回调再回调,通过回调再回调实现两个函数或者两个组件的相互通信。
chrome performance面板火焰图可以直观的看到调用栈,下图展示shopping-chart页面加载的redux-saga执行过程
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。