这是我参与8月更文挑战的第2天,活动详情查看: 8月更文挑战
前言
之前闲的无聊,听歌之余去看了下redux-saga的官网,看到Channel章节时让我深感启发,原来redux-saga针对不同的需求场景设计了不同类型的通道 ,学习这些通道的用法之余,我还看了下redux-saga中针对这些API所设计的源码,然后总结成下面的文章。
阅读先知:
阅读下面的内容需要你有redux-saga的使用经验以及了解redux-saga的运行原理。可以简单过一下我上一篇文章redux-saga:运用 ~ 原理分析 ~ 理解设计目的。
阅读下面的内容后,你将学会:
take的设计原理channel的用法以及设计原理,以及使用场景actionChannel的用法以及设计原理,以及使用场景eventChannel的用法以及设计原理,以及使用场景
take的设计原理
这里先展示一段的代码来展示下take的用法:
import { take, fork } from 'redux-saga/effects'
function* watchRequests() {
while (true) {
const {payload} = yield take('REQUEST')
yield fork(handleRequest, payload)
}
}
function* handleRequest(payload) { ... }
watchRequests这个saga采用了非常经典的take与fork配合的代码模式:
-
调用
take生成Effect(type:'TAKE')指示sagaMiddleware去监听type为'REQUEST'的action。此时该watchRequests会陷入阻塞直至特定的action被派发(dispatch)。 -
sagaMiddleware在捕获到type为'REQUEST'的action后,watchRequests退出阻塞状态,且拿到该action。从action中取出payload后调用fork非阻塞地执行handleRequest。
懂得用法还不行,因为本章节时要说明运行原理的,接下来直接通过源码来了解原理:
首先看take的代码:
packages/core/src/internal/io.js
/**
* take的形参即可以是pattern(字符串,用于匹配action.type),
* 也可以是channel(传入的通道,用于监听通道的变化)
* 之后的章节会说到take(channel),我们这里先对take(pattern)来分析
*/
export function take(patternOrChannel = '*', multicastPattern) {
// 处理take(pattern)
if (is.pattern(patternOrChannel)) {
// 如果还带有第二个参数,则打印输出警告
if (is.notUndef(multicastPattern)) {
console.warn(`take(pattern) takes one argument but two were provided. Consider passing an array for listening to several action types`)
}
/**
* 生成对应的Effect,从下面makeEffect的代码可知,生成的Effect是一个纯对象,结构如下:
* {
* [IO]: true,
* combinator: false,
* type:'TAKE',
* payload:{pattern: patternOrChannel}
* }
*/
return makeEffect(effectTypes.TAKE, { pattern: patternOrChannel })
}
...
// 处理take(channel)
if (is.channel(patternOrChannel)) {
...
}
}
const makeEffect = (type, payload) => ({
[IO]: true,
// this property makes all/race distinguishable in generic manner from other effects
// currently it's not used at runtime at all but it's here to satisfy type systems
combinator: false,
type,
payload,
})
take生成的Effect在yield后会交给sagaMiddleware,sagaMiddleware根据Effect的type调用相应的函数(redux-saga概念中称之为EffectRunner,意为专门处理Effect的函数)来处理Effect,我们来看一下专门处理Effect(type:'TAKE')的EffectRunner:runTakeEffect的代码:
packages/core/src/internal/effectRunnerMap.js
function runTakeEffect(env, { channel = env.channel, pattern, maybe }, cb) {
const takeCb = input => {
...
cb(input)
}
try {
channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null)
} catch (err) {
cb(err, true)
return
}
cb.cancel = takeCb.cancel
}
runTakeEffect做了两件事,但这两件事我们不知道为啥要这么干:
- 声明了
takeCb,但为什么要声明一个takeCb - 调用了
channel.take处理takeCb,但channel是什么来的,channel.take又有什么作用
带着上面的疑问,我们先去了解下env,env是一个环境变量,它在sagaMiddleware被实例化时会被声明,即我们在创建Redux store,在下面的语句中:
const store = createStore(reducer, {}, applyMiddleware(sagaMiddleware()));
在sagaMiddleware()中,env.channel会被声明为stdChannel的实例,接下来看看stdChannel的代码:
packages/core/src/internal/channel.js
export function stdChannel() {
// 把chan声明为multicastChannel的实例
const chan = multicastChannel()
// 对chan.put进行增强,无论什么条件,put(input)都会被执行,所以此处增强对基本的运行逻辑没影响
const { put } = chan
chan.put = input => {
// 查看input是否是saga内部dispatch的action
if (input[SAGA_ACTION]) {
put(input)
return
}
asap(() => {
put(input)
})
}
return chan
}
stdChannel其实也就是multicastChannel的增强而已,我们直接看multicastChannel的代码:
export function multicastChannel() {
// 这里存在一个标志着通道已关闭的标志位,但此次分析中我们不需要考虑这个,下面的代码
// 中,我也会把涉及到close的代码删掉,简化一下
// let closed = false
/**
* 声明currentTakers和nextTakers,
* 至于为什么需要两个?在下面会说明
*/
let currentTakers = []
let nextTakers = currentTakers
// 此方法确保nextTakers和currentTakers指向的不是同一个数组
const ensureCanMutateNextTakers = () => {
if (nextTakers !== currentTakers) {
return
}
nextTakers = currentTakers.slice()
}
return {
[MULTICAST]: true,
/**
* 当有action被派发时,sagaMiddleware会执行channel.put(action),
* 因此这里的input即为action
*/
put(input) {
// 遍历之前,把currentTakers的指向到nextTakers
const takers = (currentTakers = nextTakers)
// 遍历取出与action.type匹配的taker
for (let i = 0, len = takers.length; i < len; i++) {
const taker = takers[i]
// 通过taker中的MATCH属性去检测是否匹配
if (taker[MATCH](input)) {
/** 先执行taker.cancel把该taker从nextTakers中移除出去
* 注意此处是从nextTakers中移除,而遍历的是currentTakers,
* 在taker.cancel里面作移除前,会调用上面的ensureCanMutateNextTakers,
* 保证currentTakers与nextTakers指向的不是同一个数组,则在nextTakers变化后,
* 对currentTakers的遍历不会受影响
*/
taker.cancel()
taker(input)
}
}
},
/**
* runTakeEffect执行时,会执行channel.take把在runTakeEffect中生成的taker传进去
*/
take(cb, matcher = matchers.wildcard) {
// 把matcher(匹配函数,用于检测action是否匹配)挂载到MATCH属性上
cb[MATCH] = matcher
// 在修改nextTakers之前,确保currentTakers和nextTakers指向的数组不一样
ensureCanMutateNextTakers()
// 把cb存入到nextTakers中
nextTakers.push(cb)
// 定义cb的cancel属性,在cb执行之前调用,从而在调用期间把cb从nextTakers中移除
cb.cancel = once(() => {
// 无论是添加还是删除,都要确保currentTakers和nextTakers指向的数组不一样
ensureCanMutateNextTakers()
// 把cb从nextTakers中移除
remove(nextTakers, cb)
})
},
}
}
从上可以看出channel中两个重要的函数take和put。前者是存储回调函数,后者是执行被存储的回调函数。
我们知道了runTakeEffect内部会调用channel.take,那channel.put会在哪里被调用呢?注释里说了是在action会派发时,sagaMiddleware内部被调用,我们看一下下面的sagaMiddlewareFactory(用于实例化sagaMiddlewareFactory的工厂函数)的部分源码:
packages/core/src/internal/middleware.js
export default function sagaMiddlewareFactory({ context = {}, channel = stdChannel(), sagaMonitor, ...options } = {}) {
...
// 以工厂模式声明sagaMiddleware然后返回出去
function sagaMiddleware({ getState, dispatch }) {
...
// redux中间件的范式
return next => action => {
if (sagaMonitor && sagaMonitor.actionDispatched) {
sagaMonitor.actionDispatched(action)
}
// 调用next把action传到下一个中间件或者store.dispatch上
const result = next(action) // hit reducers
// 来了来了来了!!!每当有一个action,都会用channel.put执行
channel.put(action)
return result
}
}
sagaMiddleware.run = (...args) => {
...
}
return sagaMiddleware
}
上面分析了一大串源码,下面我们可以总结一下:
-
当
saga中take被调用时,sagaMiddleware会调用runTakeEffect处理,runTakeEffect会生成taker且调用channel.take(taker)。channel.take把传入的回调函数放到takers数组里。此时saga会会处于阻塞直至生成的taker被执行。 -
当有
action被派发时,sagaMiddleware会调用channel.put(action)。channel.put遍历takers取出匹配的回调函数执行。此时taker执行后会让对应的saga退出take引起的阻塞,且拿到被派发的action。
用流程图来表示则如下所示:
channel
使用方式
我们继续那开头的watchRequests来说:
import { take, fork } from 'redux-saga/effects'
function* watchRequests() {
while (true) {
const {payload} = yield take('REQUEST')
yield fork(handleRequest, payload)
}
}
function* handleRequest(payload) { ... }
watchRequests存在一个隐患:fork是一个非阻塞的API。因此,在如果在短时间内有大量对应的action被捕获,则handleRequest会被不断地调用,如果handleRequest中带有网络请求的逻辑,则同一时间内会有大量的网络请求在执行。
假设我们针对上述缺点的解决方案是:同一时间内最多有三个handleRequest在执行,如果又有对应的action被派发,则等到三个正在执行的handleRequest中其中一个已结束后才能fork新的handleRequest。
可是上述的解决方案要怎么做呢?redux-saga提供了channel给我们去很方便地完成这种逻辑,直接看以下代码:
import { channel } from 'redux-saga'
import { take, fork, call } from 'redux-saga/effects'
function* watchRequests() {
// create a channel to queue incoming requests
// 创建channel去存储传入信息
const chan = yield call(channel)
// create 3 worker 'threads'
// 创建3个'工作线程',其实这里不算是线程,只是因为`fork`是非阻塞API,用于非阻塞地调用fn。
for (var i = 0; i < 3; i++) {
yield fork(handleRequest, chan)
}
while (true) {
const {payload} = yield take('REQUEST')
// 当有对应的action被派发,把action.payload存入到chan里
yield put(chan, payload)
}
}
function* handleRequest(chan) {
while (true) {
// 查看chan中的是否有信息存入,有则取出,然后运行下面的逻辑
const payload = yield take(chan)
// process the request
}
}
非常简短,而且对比于自己写限制函数。使用channel能让我们更好地测试。
channel这个API还有一个很好的特点,默认channel生成的通道会不限制数量地存储任何输入信息,但如果你想限制数量,可以在调用channel是传入redux-saga中buffer参数,如:
import { buffers,channel } from 'redux-saga'
function* watchRequests() {
// 此处设定只接受最多5个传入信息。
// 使用了buffers.sliding代表如果有新的action被派发,则舍弃最早传入的action。
// 其实就是存新弃旧。
const chan = yield call(channel, buffers.sliding(5))
...
}
其实,buffers除了sliding外有几种模式:
buffers.none(): 任何被存入的输入信息直接丢弃,不会缓存。buffers.fixed(limit): 输入信息会被缓存直到数量超过上限后,会抛出错误.此处limit的缺省值为10。buffers.expanding(initialSize): 输入信息会被缓存直到数量超过上限后会动态扩容。buffers.dropping(limit):action会被缓存直到数量超过上限后,即不会抛出错误,也不会缓存新的输入信息。buffers.sliding(limit): 输入信息会被缓存直到数量超过上限后,会移除最先缓存的输入信息,然后缓存最新的输入信息。
用法介绍的差不多了,接下来我们看一下这个channel的源码是怎样子的。
源码分析
我们先不看channel的源码,而是选择看put和take对应的源码,因为上面的例子中有两个新的用法我们没见过:
handleRequest中的yield take(chan),这里take的形参是通道。watchRequests中的yield put(chan, payload),之前用put的形参都是同步action。这里却是通道以及action.payload。
我们先依次看put以及其对应的runPutEffect:
packages\core\src\internal\io.js
export function put(channel, action) {
// 以put(action)的形式调用action时
if (is.undef(action)) {
action = channel
channel = undefined
}
/**
* 生成对应的Effect,结构如下:
* {
* [IO]: true,
* combinator: false,
* type:'PUT',
* payload:{ channel, action }
* }
*/
return makeEffect(effectTypes.PUT, { channel, action })
}
packages\core\src\internal\effectRunnerMap.js
function runPutEffect(env, { channel, action, resolve }, cb) {
/**
* 题外话,可跳过:
* asap作用在于让传入的回调函数依次执行,不出现抢占情况。
* 假设有以下情况:
* 一段程序依次dispatch了两个action,而这两个action会被两个不同的saga捕获到。
* 然后这两个saga中都有调用put这个EffectCreator。
* 此时,如果没有用asap包裹着,那可能会出现来自第一个saga的Effect正在处理时(有可能因为异步请求还没结束)
* 然后来自第二个saga的Effect被yield了后,就会在第一个Effect还在等待中时处理。
* 这样子如果上述两个action必须按顺序派发(第一次saga会影响到第二次saga的参数),没有asap包裹就会导致每次运行的结果不一样。
*/
asap(() => {
let result
try {
/**
* 根据put的调用方式分情况处理:
* 1. put(action):调用env.dispatch(就是store.dispatch)处理action
* 2. put(channel, action):调用channel.put(action)
*/
result = (channel ? channel.put : env.dispatch)(action)
} catch (error) {
cb(error, true)
return
}
// 如果result是Promise实例,则等promise状态从pending变为fulfilled后才调用cb
if (resolve && is.promise(result)) {
resolvePromise(result, cb)
} else {
cb(result)
}
})
// Put effects是不可取消的
}
从上可知,saga中调用put(chan, payload)是会触发channel.put(payload)的。
现在我们带着探索take(channel)的疑问,再次看take以及对应的runTakeEffect:
export function take(patternOrChannel = '*', multicastPattern) {
// take(pattern)时的处理
if (is.pattern(patternOrChannel)) {
if (is.notUndef(multicastPattern)) {
console.warn(`take(pattern) takes one argument but two were provided. Consider passing an array for listening to several action types`)
}
return makeEffect(effectTypes.TAKE, { pattern: patternOrChannel })
}
...
// take(channel)时的处理
if (is.channel(patternOrChannel)) {
// 如果有第二形参则打印输出警告
if (is.notUndef(multicastPattern)) {
console.warn(`take(channel) takes one argument but two were provided. Second argument is ignored.`)
}
// 生成Effect
return makeEffect(effectTypes.TAKE, { channel: patternOrChannel })
}
}
从上看出,不同的传参类型生成的Effect的payload结构会不一样:
take(pattern):生成的Effect.payload为{pattern:pattern}take(channel):生成的Effect.payload为{channel:channel}
/**
* 不同的Effect会导致此函数的channel不同,如果Effect的payload中channel存在,
* 则下面函数中,channel则取Effect.payload.channel;
* 如果不存在,则直接取环境变量里的channel,即管理take的stdChannel的实例
*/
function runTakeEffect(env, { channel = env.channel, pattern, maybe }, cb) {
const takeCb = input => {
...
cb(input)
}
try {
channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null)
} catch (err) {
cb(err, true)
return
}
cb.cancel = takeCb.cancel
}
因此,我们知道,saga中yield take(channel)会触发channel.take(takeCb,matcher(pattern))的执行。
既然已经知道了channel中主要调用的是put和take。那我们接下来就了解channel的源码,主要了解其中的take和put方法就行。
注意:这里的channel和上一章节take的设计原理中介绍的multicastChannel是不一样的。
packages/core/src/internal/channel.js
// 下面的channel我们只分析take和put以及其涉及到的代码部分,其余的省略
export function channel(buffer = buffers.expanding()) {
let takers = []
/** 用于触发执行takers中的taker
* 和multicastChannel最不一样的在于,不需要遍历匹配takers,
* 如果takers中有taker,则直接从数组开头移出且执行。
*/
function put(input) {
/**
* takers数组为空时,把信息存入到buffer里
* 什么时候takers数组为空呢?
* 已知channel.take是用于往takers中存入回调函数的,而channel.take是由runTakeEffect调用的
* 我们则推理出:
* 当channel.put已被调用后,saga退出take阻塞,此时channel.takers中有一个cb,于是取出且执行
* saga继续往下执行,假设陷入了call引起的阻塞。但此时又有需要action被派发,
* channel.put再次被调用,但此时channel.takers为空,则input会存进buffer里。
* 当channel.take被调用时,会先检查buffer是否为空,不为空则直接取出buffer中的input且执行,即cb(input)。
* 如果为空,则把cb存进channel.takers。等待下一次channel.put被调用后执行。
*/
if (takers.length === 0) {
return buffer.put(input)
}
const cb = takers.shift()
cb(input)
}
function take(cb) {
// buffer不为空,则直接取出执行
if (!buffer.isEmpty()) {
cb(buffer.take())
// 为空时,存进takers中
} else {
takers.push(cb)
// cb.cancel用于把takers从cb中移除,该方法在关闭管道时会用到
cb.cancel = () => {
remove(takers, cb)
}
}
}
return {
take,
put,
...
}
}
综合上面看过的源码,我们知道了在channel中的take与put是如何互相辅助的,以流程图的形式展示则如下所示:
带着这些总结我们再去回看开头用到channel的例子,那时候的疑问可以瞬间解决:
handleRequest中的yield take(chan),生成的Take Effect会触发channel.take的执行,channel.take会干啥,我也不用在写多了,都是上面的内容。watchRequests中的yield put(chan, payload),生成的Put Effect会触发channel.put的执行,channel.put会干啥,我也不用在写多了,都是上面的内容。
actionChannel
使用方式
我们再次看一下take的设计原理的章节中的watchRequests:
import { take, fork } from 'redux-saga/effects'
function* watchRequests() {
while (true) {
const {payload} = yield take('REQUEST')
yield fork(handleRequest, payload)
}
}
function* handleRequest(payload) { ... }
在channel.使用方式中我们说了watchRequests中的一个存在的隐患:如果匹配的action在短时间内以极高的频率被派发,则同时会存在许多handleRequest任务在执行。 而在channel.使用方式章节中,使用的解决方案是限制同一时刻中,handleRequest的执行数量。
如果我们想换一种解决方案:即串行执行handleRequest。其实就是限制同一时刻中只允许存在一个handleRequest在运行。那其实我们拿channel.使用方式中运用channel代码例子来改一下就好了,把创建的“工作线程”的数量改成1就好了,如下所示:
import { channel } from 'redux-saga'
import { take, fork, call } from 'redux-saga/effects'
function* watchRequests() {
// create a channel to queue incoming requests
// 创建channel去存储传入信息
const chan = yield call(channel)
// 创建1个'工作线程
yield fork(handleRequest, chan)
while (true) {
const {payload} = yield take('REQUEST')
// 当有对应的action被派发,把action.payload存入到chan里
yield put(chan, payload)
}
}
function* handleRequest(chan) {
while (true) {
// 查看chan中的是否有信息存入,有则取出,然后运行下面的逻辑
const payload = yield take(chan)
// process the request
}
}
但其实,针对串行处理。redux-saga提供了一个更好用的API:actionChannel。我们来看一下如果上面的需求用actionChannel来实现是怎样子的:
import { take, actionChannel, call } from 'redux-saga/effects'
function* watchRequests() {
// 1- 发出ActionChannel Effect以创建管道用于存储来不及处理的action
const requestChan = yield actionChannel('REQUEST')
while (true) {
// 2- 把action从管道中取出
const {payload} = yield take(requestChan)
// 3- 调用handleRequest处理action
// 注意这里调用的是call阻塞API。如果调用fork非阻塞API,就达不到串行处理的效果
yield call(handleRequest, payload)
}
}
function* handleRequest(payload) { ... }
怎样,对比两种实现方式的代码,使用actionChannel的简洁多了是不是。还有一点,跟channel一样的,我们可以通过传入buffer来限制生成的通道存储action的模式,在actionChannel的第二参数中传入buffer参数既可,如下所示:
import { buffers } from 'redux-saga'
import { actionChannel } from 'redux-saga/effects'
function* watchRequests() {
const requestChan = yield actionChannel('REQUEST', buffers.sliding(5))
...
}
源码分析
从上面的用法可知,actionChannel其实也是用于生成Effect的。针对每一个新的Effect,我们不仅要分析对应的EffectCreator(生成Effect的对外API,例如take、actionChannel),还要分析EffectRunner(处理Effect的内部函数)。
先看actionChannel:
packages/core/src/internal/io.js
export function actionChannel(pattern, buffer) {
/**
* 生成类型为ACTION_CHANNEL的Effect
* 生成Effect的对象结构如下:
* {
* [IO]: true,
* combinator: false,
* type: "ACTION_CHANNEL",
* payload: {pattern,buffer}
* }
*/
return makeEffect(effectTypes.ACTION_CHANNEL, { pattern, buffer })
}
接下来看actionChannel的EffectRunner:runChannelEffect
packages/core/src/internal/effectRunnerMap.js
function runChannelEffect(env, { pattern, buffer }, cb) {
// 生成channel
const chan = channel(buffer)
// 生成用于匹配pattern的函数
const match = matcher(pattern)
/**
* 生成taker,通过env.channel.take存入env.channel中,当匹配match的action被派发时,
* taker就会被执行
* 注意:env.channel与本函数生成的chan是两个不同的通道
* env.channel是在sagaMiddleware实例化时生成的,用于处理take生成的Effect,即stdChannel的实例
* 这里生成的chan是专门用于处理actionChannel的Effect,即channel的实例
*/
const taker = action => {
/**
* 如果actionChannel未被关闭,则在taker在env.channel被取出执行时,
* 再次把taker存放到env.channel中,这样子就可以保证可以每当有匹配match的action
* 被派发时,taker会被取出和执行
*/
if (!isEnd(action)) {
env.channel.take(taker, match)
}
// 把action通过put存入chan中
chan.put(action)
}
const { close } = chan
// 包装模式更改chan.close函数,执行close时,执行taker.cancel后会把taker从env.channel中移除出去
chan.close = () => {
taker.cancel()
close()
}
// 把taker存入env.channel
env.channel.take(taker, match)
// 相当于next(chan),此时saga拿到chan后继续执行
cb(chan)
}
从上看出,actionChannel是把env.channel和channel相互配合实现的。
由于上述例子中用到了take,而且是通过take(channel)的方法调用,为了方便理解,这里再次展示runTakeEffect的源码:
/**
* 当通过take(channel)的方式调用channel,runTakeEffect中的第二形参中的
* channel是take中传入的channel。
*/
function runTakeEffect(env, { channel = env.channel, pattern, maybe }, cb) {
const takeCb = input => {
...
cb(input)
}
try {
channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null)
} catch (err) {
cb(err, true)
return
}
cb.cancel = takeCb.cancel
}
总结下来,有以下流程图:
不过上面流程图中的channel.take和channel.put的调用我只画了channel.takers里存有cb的情况。如果想了解没有cb的情况下如何运行,请参考我在channel源码分析中画的流程图。
eventChannel
使用方法
eventChannel是一个工厂函数(与actionChannel不一样,eventChannel不是一个EffectCreator),用于创建一个通道,但该通道的事件源是脱离Redux store的。用一个例子来简单展示以下:
import { eventChannel, END } from 'redux-saga'
import { take, put, call } from 'redux-saga/effects'
function countdown(secs) {
/**
* eventChannel的形参为订阅函数,其范式为emitter=>(()=>{}||void)
* 每次调用emitter都会触发捕获该通道(即take(chan))的saga继续执行
*/
return eventChannel(emitter => {
const iv = setInterval(() => {
secs -= 1
if (secs > 0) {
/**
* emitter中传入的数据可以通过yield take(chan)获取到
* 官方推荐传入的数据的数据结构是纯函数,即:
* 比起emitter(number),emitter({number})更好
*/
emitter(secs)
} else {
/**
* 这种操作会让通道关闭, END是redux-saga定义的一个用于关闭通道的action
* 关闭通道后,不会在有信息传入该通道
*/
emitter(END)
}
}, 1000);
/**
* 如果返回结果为一个的函数,则该函数会用于用于注销订阅,
* 用于在关闭通道时,redux-saga内部会调用
*/
return () => {
clearInterval(iv)
}
}
)
}
export function* saga() {
const chan = yield call(countdown, 5)
try {
while (true) {
// 通道关闭后会导致saga直接跳到finally语句块
let seconds = yield take(chan)
console.log(`countdown: ${seconds}`)
}
} finally {
console.log('countdown terminated')
}
}
在上述saga被sagaMiddleware.run(saga)后,页面控制台会出现以下效果:
同样的,eventChannel也支持使用buffer控制传入信息的缓存模式。在eventChannel的第二形参可以传入buffer参数。
官网中提供了一个基于eventChannel的socket通信处理saga的示例,有兴趣的可以去看看。
源码分析
eventChannel这个API只是一个工厂函数,用于生成channel实例,下面直接看该API源码:
packages\core\src\internal\channel.js
export function eventChannel(subscribe, buffer = buffers.none()) {
let closed = false
let unsubscribe
// 初始化channel实例
const chan = channel(buffer)
// 定义关闭函数
const close = () => {
if (closed) {
return
}
closed = true
if (is.func(unsubscribe)) {
unsubscribe()
}
chan.close()
}
/**
* 在eventChannel实例化时,会调用subscribe函数
* subscribe中传入的input=>{}就是emitter
* 可以看到emitter执行时就是调用channel.put
*/
unsubscribe = subscribe(input => {
if (isEnd(input)) {
close()
return
}
chan.put(input)
})
// 使unsubscribe只能调用一次,类似lodash中的once
unsubscribe = once(unsubscribe)
/**
* 如果在执行subscribe过程中因为调用了emitter(END)把通道关闭了,
* 则closed为true
*/
if (closed) {
unsubscribe()
}
return {
take: chan.take,
flush: chan.flush,
close,
}
}
结合上面的源码以及前面章节中我们了解的channel,我们可以描绘出在eventChannel.使用方法中的例子的运行流程:
后记
写了这么多,终于写完了。写作不容易,读者们若有收获且心情好请点个赞哈。