redux-saga中关于channel的那片学问

2,518 阅读15分钟

这是我参与8月更文挑战的第2天,活动详情查看: 8月更文挑战

前言

之前闲的无聊,听歌之余去看了下redux-saga的官网,看到Channel章节时让我深感启发,原来redux-saga针对不同的需求场景设计了不同类型的通道 ,学习这些通道的用法之余,我还看了下redux-saga中针对这些API所设计的源码,然后总结成下面的文章。

阅读先知

阅读下面的内容需要你有redux-saga的使用经验以及了解redux-saga的运行原理。可以简单过一下我上一篇文章redux-saga:运用 ~ 原理分析 ~ 理解设计目的

阅读下面的内容后,你将学会:

  1. take的设计原理
  2. channel的用法以及设计原理,以及使用场景
  3. actionChannel的用法以及设计原理,以及使用场景
  4. 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采用了非常经典的takefork配合的代码模式:

  1. 调用take生成Effect(type:'TAKE')指示sagaMiddleware去监听type'REQUEST'action。此时该watchRequests会陷入阻塞直至特定的action被派发(dispatch)。

  2. 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生成的Effectyield后会交给sagaMiddlewaresagaMiddleware根据Effecttype调用相应的函数(redux-saga概念中称之为EffectRunner,意为专门处理Effect的函数)来处理Effect,我们来看一下专门处理Effect(type:'TAKE')EffectRunnerrunTakeEffect的代码:

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做了两件事,但这两件事我们不知道为啥要这么干:

  1. 声明了takeCb,但为什么要声明一个takeCb
  2. 调用了channel.take处理takeCb,但channel是什么来的,channel.take又有什么作用

带着上面的疑问,我们先去了解下envenv是一个环境变量,它在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中两个重要的函数takeput。前者是存储回调函数,后者是执行被存储的回调函数。

我们知道了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
}

上面分析了一大串源码,下面我们可以总结一下:

  1. sagatake被调用时,sagaMiddleware会调用runTakeEffect处理,runTakeEffect会生成taker且调用channel.take(taker)channel.take把传入的回调函数放到takers数组里。此时saga会会处于阻塞直至生成的taker被执行。

  2. 当有action被派发时,sagaMiddleware会调用channel.put(action)channel.put遍历takers取出匹配的回调函数执行。此时taker执行后会让对应的saga退出take引起的阻塞,且拿到被派发的action

用流程图来表示则如下所示:

redux-saga take 流程图 (1).jpg

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-sagabuffer参数,如:

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的源码,而是选择看puttake对应的源码,因为上面的例子中有两个新的用法我们没见过:

  1. handleRequest中的yield take(chan),这里take的形参是通道。
  2. 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 })
  }
}

从上看出,不同的传参类型生成的Effectpayload结构会不一样:

  • 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
}

因此,我们知道,sagayield take(channel)会触发channel.take(takeCb,matcher(pattern))的执行。

既然已经知道了channel中主要调用的是puttake。那我们接下来就了解channel的源码,主要了解其中的takeput方法就行。

注意:这里的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中的takeput是如何互相辅助的,以流程图的形式展示则如下所示:

image.png

带着这些总结我们再去回看开头用到channel的例子,那时候的疑问可以瞬间解决:

  1. handleRequest中的yield take(chan),生成的Take Effect会触发channel.take的执行,channel.take会干啥,我也不用在写多了,都是上面的内容。
  2. 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提供了一个更好用的APIactionChannel。我们来看一下如果上面的需求用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,例如takeactionChannel),还要分析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 })
}

接下来看actionChannelEffectRunnerrunChannelEffect

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.channelchannel相互配合实现的。

由于上述例子中用到了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
}

总结下来,有以下流程图:

image.png

不过上面流程图中的channel.takechannel.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')
  }
}

在上述sagasagaMiddleware.run(saga)后,页面控制台会出现以下效果:

eventchannel-test.gif

同样的,eventChannel也支持使用buffer控制传入信息的缓存模式。在eventChannel的第二形参可以传入buffer参数。

官网中提供了一个基于eventChannelsocket通信处理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.使用方法中的例子的运行流程:

image.png

后记

写了这么多,终于写完了。写作不容易,读者们若有收获且心情好请点个赞哈。