由于dva的版本是2.4.1,对应的是redux-saga@0.16.2,所以就解读该版本的代码,本文围绕examples/shopping-cart展开

graph.jpeg

注册

使用方式
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
saga.drawio.png
将包含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);带入到runCallEffectfn为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

image.png

上图可以看出通过subscribe将回调函数放进了emitter中的subscribers数组,

eventChannel

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执行过程

火焰图


assassin_cike
1.3k 声望74 粉丝

生活不是得过且过


« 上一篇
git 常用命令
下一篇 »
typescript 梳理

引用和评论

0 条评论