专栏名称: 前端外刊评论
最新、最前沿的前端资讯,最有深入、最干前端相关的技术译文。
目录
相关文章推荐
51好读  ›  专栏  ›  前端外刊评论

构建你自己的 redux-saga(下)

前端外刊评论  · 公众号  · 前端  · 2018-05-29 06:30

正文

请到「今天看啥」查看全文


2.1 Task

proc 函数(redux-saga 的源码)用于运行一个迭代器,并返回一个 Task 对象。Task 对象描述了该迭代器的运行状态,我们首先来看看 Task 的接口(使用 TypeScript 来表示类型信息)。在 little-saga 中,我们将使用类似的 Task 接口。(注意是类似的接口,而不是相同的接口)

  1. type Callback = (result: any, isErr: boolean) => void

  2. type Joiner = { task: Task; cb: Callback }

  3. interface Task {

  4.  cancel(): void

  5.  toPromise(): Promise

  6.  result: any

  7.  error: Error

  8.  isRunning: boolean

  9.  isCancelled: boolean

  10.  isAborted: boolean

  11. }

Task 对象包含了 toPromise () 方法,该方法会返回 saga 实例对应的 promise。 cancel () 方法使得 saga 实例允许被取消; isXXX 等字段反映了 saga 实例的运行状态; result / error 字段可以记录了 saga 实例的运行结果。

async 函数被调用后,内部逻辑也许非常复杂,最后返回一个 Promise 对象来表示异步结果。而 saga 实例除了异步结果,还包含了额外的功能:「取消」以及「查询运行状态」。所以 Task 接口是要比 Promise 接口复杂的,内部实现也需要更多的数据结构和逻辑。

2.2 fork model

redux-saga 提供了 fork effect 来进行非阻塞调用, yield fork (...) 会返回一个 Task 对象,用于表示在后台执行的 saga 实例。在更普遍的情况下,一个 saga 实例在运行的时候会多次 yield fork effect,那么一个 parent-saga 实例就会有多个 child-saga。rootSaga 通过 sagaMiddleware . run () 开始运行,在 rootSaga 运行过程中,会 fork 得到若干个 child-saga,每一个 child-saga 又会 fork 得到若干个 grandchild-saga,如果我们将所有的 parent-child 关系绘制出来的话,我们可以得到类似于下图这样的一棵 saga 树。

redux-saga 的文档也对 fork model 进行了详细的说明,下面我做一点简单的翻译:

  • 完成:一个 saga 实例在满足以下条件之后进入完成状态:


  • 迭代器自身的语句执行完成


  • 所有的 child-saga 进入完成状态


当一个节点的所有子节点完成时,且自身迭代器代码执行完毕时,该节点才算完成。

  • 错误传播:一个 saga 实例在以下情况会中断并抛出错误:


  • 迭代器自身执行时抛出了异常


  • 其中一个 child-saga 抛出了错误


当一个节点发生错误时,错误会沿着树向根节点向上传播,直到某个节点捕获该错误。

  • 取消:取消一个 saga 实例也会导致以下事情的发生:


  • 取消 mainTask,也就是取消当前 saga 实例等待的 effect


  • 取消所有仍在执行的 child-saga


取消一个节点时,该节点对应的整个子树都将被取消。

2.3 类 ForkQueue

ForkQueue 是 fork model 的具体实现。redux-saga 使用了 函数 forkQueue 来实现,在 little-saga 中我们使用 class 语法定义了 ForkQueue 类。

每一个 saga 实例可以用一个 Task 对象进行描述,为了实现 fork model,每一个 saga 实例开始运行时,我们需要用一个数组来保存 child-tasks。我们来看看 forkQueue 的接口:

  1. interface ForkQueue {

  2.  constructor(mainTask: MainTask)

  3.  // cont: Callback   这是一个私有的字段

  4.  addTask(task: Task): void

  5.  cancelAll(): void

  6.  abort(err: Error): void

  7. }

ForkQueue 的构造函数接受一个参数 mainTask ,该参数代表当前迭代器自身代码的执行状态,forkQueue.cont 会在 ForkQueue 被构造之后进行设置。当所有的 child-task 以及 mainTask 都完成时,我们需要调用 forkQueue.cont 来通知其 parent-saga(对应于 2.2 fork model 中的「完成」)。

ForkQueue 对象包含三个方法。方法 addTask 用于添加新的 child-task;方法 cancelAll 用于取消所有的 child-task;而方法 abort 不仅会取消所有的 child-task,还会调用 forkQueue.cont 向 parent-task 通知错误。

little-saga 的 ForkQueue 实现如下:

  1. class ForkQueue {

  2.  tasks = []

  3.  result = undefined

  4.  // 使用 completed 变量来保证「完成」和「出错」的互斥

  5.  completed = false

  6.  // cont will be set after calling constructor()

  7.  cont = undefined

  8.  constructor(mainTask ) {

  9.    this.mainTask = mainTask

  10.    // mainTask 一开始就会被添加到数组中

  11.    this.addTask(this.mainTask)

  12.  }

  13.  // 取消所有的 child-task,并向上层通知错误

  14.  abort(err) {

  15.    this.cancelAll()

  16.    this.cont(err, true)

  17.  }

  18.  addTask(task) {

  19.    this.tasks.push(task)

  20.    // 指定 child-task 完成时的行为

  21.    task.cont = (res, isErr) => {

  22.      if (this.completed) {

  23.        return

  24.      }

  25.      // 移除 child-task

  26.      remove(this.tasks, task)

  27.      // 清空child-task完成时的行为

  28.      task.cont = noop

  29.      if (isErr) {

  30.        // 某一个 child-task 发生了错误,调用 abort 来进行「错误向上传播」

  31.        this.abort(res)

  32.      } else {

  33.        // 如果是 mainTask 完成的话,记录其结果

  34.        if (task === this.mainTask) {

  35.          this.result = res

  36.        }

  37.        if (this.tasks.length === 0) {

  38.          // 满足了 task 完成的两个条件

  39.          this.completed = true

  40.          this.cont(this.result)

  41.        }

  42.      }

  43.    }

  44.  }

  45.  cancelAll() {

  46.    if (this.completed) {

  47.      return

  48.     }

  49.    this.completed = true

  50.    // 依次调用 child-task 的 cancel 方法,进行「级联向下取消」,并清空 child-task 完成时的行为

  51.    this.tasks.forEach(t => {

  52.      t.cont = noop

  53.      t.cancel()

  54.    })

  55.    this.tasks = []

  56.  }

  57. }

2.4 task context

每一个 task 都有其对应的 context 对象,用于保存该 task 运行时的上下文信息。在 redux-saga 中我们可以使用 getContext/setContext 读写该对象。context 的一大特性是 child-task 会使用原型链的方式继承 parent-task context。当尝试访问 context 中的某个属性时,不仅会在当前 task context 对象中搜寻该属性,也会在 parent-task context 对象进行搜索,以及 parent-task 的 parent-task,依次层层往上搜索,直到找到该属性或是到达 rootSaga。该继承机制在 redux-saga 中的实现也非常简单,只有一行代码: const taskContext = Object . create ( parentContext )

context 是一个强大的机制,例如在 React 中,React context 用途非常广泛,react-redux / react-router 等相关类库都是基于该机制实现的。然而在 redux-saga 中,context 似乎很少被提起。

在 little-saga 中,我们将充分利用 context 机制,并使用该机制实现「effect 类型拓展」、「连接 redux store」等功能。这些机制的实现会在本文后面提到。

2.5 effect 类型拓展

在 1.9 proc 初步实现中,函数 runEffect 遇到未知 effect 类型便会抛出异常。这里我们对该处做一些修改,以实现 effect 的类型拓展。当遇到未知的 effect 类型时,我们将使用 ctx . translator getRunner 方法来获取该 effect 对应的 effectRunner,然后调用该 effectRunner。只要我们提前设置好 ctx . translator ,就能在后续的代码中使用拓展类型。为了方便设置 ctx . translator ,little-saga 中新增了 def 类型的 effect,用来关联拓展类型与其对应的 effectRunner。

根据 context 的特性,child-task 会继承 parent-saga 的 context,故在 parent-task 中定义的拓展类型也能用于 child-task。在 little-saga 中,race/all/take/put 等类型将使用该拓展机制进行实现。

  1. function runEffect(effect, currCb) {

  2.  const effectType = effect[0]

  3.  if (effectType === 'promise') {

  4.    resolvePromise(effect, ctx, currCb)

  5.  } else if (effectType === 'iterator') {

  6.    resolveIterator(iterator, ctx, currCb)

  7.  } else if (effectType === 'def') {

  8.    runDefEffect(effect, ctx, currCb)

  9.  /*   其他已知类型的 effect   */

  10.  /*   ....................   */

  11.  } else { // 未知类型的effect

  12.    const effectRunner = ctx.translator.getRunner(effect)

  13.    if (effectRunner == null) {

  14.      const error = new Error(`Cannot resolve effect-runner for type: ${effectType}`)

  15.      error.effect = effect

  16.      currCb(error, true)

  17.    } else {

  18.      effectRunner(effect, ctx, currCb, { digestEffect })

  19.    }

  20.  }

  21. }

  22. function runDefEffect([_, name, handler], ctx, cb) {

  23.  def(ctx, name, handler)

  24.  cb()

  25. }

  26. // def 定义在其他文件

  27. function def(ctx, type, handler) {

  28.  const old = ctx.translator

  29.  // 替换 ctx.translator

  30.  ctx.translator = {

  31.    getRunner (effect) {

  32.      return effect[0] === type ? handler : old.getRunner(effect)

  33.    }

  34.  },

  35. }

2.6 little-saga 核心部分的完整实现

little-saga 核心部分的实现代码位于 /src/core 文件夹内。完整实现的代码较多,相比于 1.9 proc 初步实现,完整实现添加了 context、task、fork model、effect 类型拓展、错误处理等功能,完善了 Task 生命周期(启动/完成/出错/取消)。

redux-saga 对应的实现代码全部都位于 proc.js 一个文件中,导致该文件很大;而 little-saga 则是将实现分成了若干个文件,下面我们一个一个来进行分析。

2.6.1 整体思路

在 2.6 后面的内容中,我们将使用以下的代码作为例子。该例子中,Parent 会 fork Child1 和 Child2,分别需要 100ms 和 300ms 来完成。Parent 迭代器本身的代码(mainTask)需要 200ms 完成。而整个 Parent Task 则需要 300ms 才能完成。

  1. function* Parent() {

  2.  const Child1 = yield fork(api.xxxx) // LINE-1 需要 100ms 才能完成

  3.  const Child2 = yield fork(api.yyyy) // LINE-2 需要 300ms 才能完成

  4.  yield delay(200) // LINE-3 需要 200 ms 才能完成

  5. }

下图展示了 Parent 运行时,各个 Task / mainTask / ForkQueue 之间的相互关系。图中的实线箭头表示两个对象之间的后继关系(cont):「A 指向 B」意味着「当 A 完成时,需要将结果传递给 B」。

在 1.7 cancellation 中我们知道 cancellation 的顺序和 cont 恰好是相反的,在具体代码实现时,我们不仅需要构建下图中的 cont 关系,还需要构建反向的 cancellation 关系。

本小节中的代码比较复杂,如果觉得理解起来比较困难的话,可以和 2.7 Task 状态变化举例 对照着看。

2.6.2 函数 proc

函数 proc 是运行 saga 实例的入口函数。结合上图,函数 proc 的作用是创建图中各个 Task/mainTask 对象,并建立对象之间的后继关系(cont)和取消关系(cancellation)。代码如下:

  1. // /src/core/proc.js

  2. function proc(iterator, parentContext, cont) {

  3.  // 初始化当前 task 的 context

  4.  const ctx = Object.create(parentContext)

  5.  // mainTask 用来跟踪当前迭代器的语句执行状态

  6.  const mainTask = {

  7.    // cont: **will be set when passed to ForkQueue**

  8.    isRunning: true,

  9.    isCancelled: false,

  10.    cancel() {

  11.      if (mainTask.isRunning && !mainTask.isCancelled) {

  12.        mainTask.isCancelled = true

  13.        next(TASK_CANCEL)

  14.      }

  15.    },

  16.  }

  17.  // 创建 ForkQueue 对象和 Task 对象,这两个类的代码在后面会写出来

  18.  const taskQueue = new ForkQueue(mainTask)

  19.  const task = new Task(taskQueue)

  20.  // 设置后继关系

  21.  taskQueue.cont = task.end

  22.  task.cont = cont

  23.  // 设置取消关系

  24.  cont.cancel = task.cancel

  25.   next()

  26.  return task

  27.  // 以下代码均为函数定义

  28.  // 在图中驱动函数只和 mainTask 有联系

  29.  // 然后我们也可以发现下面 next 函数的代码中,也只调用了 mainTask 的接口

  30.  // 即 next 函数中的代码不会引用 task 和 taskQueue 对象

  31.  function next(arg, isErr) {

  32.    console.assert(mainTask.isRunning, 'Trying to resume an already finished generator')

  33.    try {

  34.      let result

  35.      if (isErr) {

  36.        result = iterator.throw(arg)

  37.      } else if (arg === TASK_CANCEL) {

  38.        mainTask.isCancelled = true

  39.        next.cancel() // 取消当前执行的 effect

  40.        // 跳转到迭代器的 finally block,执行清理逻辑

  41.        result = iterator.return(TASK_CANCEL)

  42.      } else {

  43.        result = iterator.next(arg)

  44.      }

  45.      if (!result.done) {

  46.        digestEffect(result.value, next)

  47.      } else {

  48.        mainTask.isRunning = false

  49.        mainTask.cont(result.value)

  50.      }

  51.    } catch (error) {

  52.      if (!mainTask.isRunning) {

  53.        throw error

  54.      }

  55.      if (mainTask.isCancelled) {

  56.        // 在执行 cancel 逻辑时发生错误,在 3.4 其他问题与细节 中说明

  57.        console.error(error)

  58.      }

  59.      mainTask.isRunning = false

  60.      mainTask.cont(error, true)

  61.    }

  62.   }

  63.  // function digestEffect(rawEffect, cb) { /* ...... */ }

  64.  // function runEffect(effect, currCb) { /* ...... */ }

  65.  // function resolvePromise([effectType, promise], ctx, cb) { /* ... */ }

  66.  // function resolveIterator([effectType, iterator], ctx, cb) { /* ... */ }

  67.  // ...... 各种内置类型的 effect-runner

  68.  // fork-model 中新增了 fork/spawn/join/cancel/cancelled

  69.  // 这五种类型的 effec-runner 代码见下方

  70. }

2.6.3 fork-model 相关的 effect-runner

函数 runForkEffect 用来执行 fork 类型的 effect,并向调用者返回一个 subTask 对象。该函数需要注意的是,有的时候调用者会使用 fork 来执行一些同步的任务,所以调用 proc ( iterator , ctx , noop ) 可能会返回一个已经完成或是已经发生错误的 subTask,此时我们不需要将 subTask 放入 fork-queue 中,而是需要执行其他操作。

  1. // /src/core/proc.js

  2. function runForkEffect([effectType, fn, ...args], ctx, cb) {

  3.  const iterator = createTaskIterator(fn , args)

  4.  try {

  5.    suspend() // 见 3.4 scheduler

  6.    const subTask = proc(iterator, ctx, noop)

  7.    if (subTask.isRunning) {

  8.      task.taskQueue.addTask(subTask)

  9.      cb(subTask)

  10.    } else if (subTask.error) {

  11.      task.taskQueue.abort(subTask.error)

  12.    } else {

  13.      cb(subTask)

  14.    }

  15.   } finally {

  16.    flush() // 见 3.4 scheduler

  17.  }

  18. }

剩下四个类型(spawn / join / cancel / cancelled)的 effect-runner 比较简单,这里就不再进行介绍。

2.6.4 类 Task

Task 是 2.1 Task 的具体实现。

  1. // /src/core/Task.js

  2. class Task {

  3.  isRunning = true

  4.  isCancelled = false

  5.  isAborted = false

  6.  result = undefined

  7.  error = undefined

  8.  joiners = []

  9.  // cont will be set after calling constructor()

  10.  cont = undefined

  11.  constructor (taskQueue) {

  12.    this.taskQueue = taskQueue

  13.  }

  14.  // 调用 cancel 函数来取消该 Task,这将取消所有当前正在执行的 child-task 和 mainTask

  15.  // cancellation 会向下传播,意味着该 Task 对应的 saga-tree 子树都将会被取消

  16.  // 同时 cancellation 也会传递给该 Task 的所有 joiners

  17.  cancel = () => {

  18.    // 如果该 Task 已经完成或是已经被取消,则跳过

  19.    if (this.isRunning && !this.isCancelled) {

  20.      this.isCancelled = true

  21.      this.taskQueue.cancelAll()

  22.      // 将 TASK_CANCEL 传递给所有 joiners

  23.      this.end(TASK_CANCEL)

  24.    }

  25.   }

  26.  // 结束当前 Task

  27.  // 设置 Task 的 result/error,然后调用 task.cont,最后将结果传递给 joiners

  28.  // 当该 Task 的 child-task 和 mainTask 都完成时(即 fork-queue 完成时),该函数将被调用

  29.  end = (result, isErr) => {

  30.    this.isRunning = false

  31.    if (!isErr) {

  32.      this.result = result

  33.    } else {

  34.      this.error = result

  35.      this.isAborted = true

  36.    }

  37.    this.cont(result , isErr)

  38.    this.joiners.forEach(j => j.cb(result, isErr))

  39.    this.joiners = null

  40.  }

  41.  toPromise() {

  42.    // 获取 task 对应的 promise 对象,这里省略了代码

  43.  }

  44. }

2.6.5 小节

这一节中代码较多,而且代码的逻辑密度很高。想要完全理解 little-saga 的实现思路,还是需要仔细阅读源代码才行。

2.7 Task 状态变化举例

下面的代码是 2.6 中的例子。

  1. function* Parent() {

  2.  const Child1 = yield fork(api.xxxx) // LINE-1 需要 100ms 才能完成

  3.  const Child2 = yield fork(api.yyyy ) // LINE-2 需要 300ms 才能完成

  4.  yield delay(200) // LINE-3 需要 200 ms 才能完成

  5. }

下表展示了这个例子在一些关键时间点的执行情况与相应的状态变化。注意在下表中,如果没有指定 task/forkQueue/mainTask 是属于 Parent 还是 Child1/Child2,默认都是属于 Parent 的。下表只展示了这个例子正常完成的过程,我们也可以思考一下在 t=50 / t=150 / t=250 / t=350 等不同时间点,如果 Parent 被取消了,代码又会怎么执行。

2.8 类 Env

Env 的作用是在运行 rootSaga 之前,对 root Task 的运行环境进行配置。Env 采用了链式调用风格的 API,方便将多个配置串联起来。

我们可以利用 Env 来预先添加一些常见的 effect 类型,例如 all/race/take/put 等,这样后续所有的 saga 函数都可以直接使用这些 effect 类型。例如下面的代码在运行 rootSaga 之前定义了 delay 和 echo 两种 effect。

  1. new Env()

  2.  .def('delay', ([_, timeout], _ctx, cb) => setTimeout(cb, timeout))

  3.  .def('echo', ([_, arg], _ctx, cb) => cb(arg))

  4.  .run(rootSaga)

  5. function* rootSaga() {

  6.  yield ['delay', 500] // 500ms之后 yield 才会返回

  7.  yield ['echo', 'hello'] // yield 返回字符串 'hello'

  8. }

2.9 第二部分小节

至此,little-saga 的核心部分实现完毕。核心部分实现了 fork model,实现了 fork/join/cancel/promise/iterator 等内置类型的 effect-runner,并预留了拓展接口。第三部分中,我们将使用该拓展接口来实现 redux-saga 中剩下的那些 effect 类型(all/race/put/take 等)。

3.1 commonEffects 拓展

all-effect 的行为与 Promise#all 非常类似:all-effect 在构造时接受一些 effects 作为 sub-effects,当所有 sub-effects 完成时,all-effect 才算完成;当其中之一 sub-effect 抛出错误时,all-effect 会立即抛出错误。

有了 def effect,拓展 effect 就简单多了。redux-saga 中 runAllEffect 用于运行 all 类型的 effect,我们拷贝该代码,并简单修改,使其符合 effectRunner 接口,即可在 little-saga 中实现 all effect。little-sage 中实现 all effect 的代码如下:

  1. // 这里该函数是一个简化版,省略了 all-effect 被取消的处理代码

  2. // 这里假设 effects 是一个对象,实际版本中还需要考虑 effects为数组的情况

  3. function all([_, effects], ctx, cb, { digestEffect }) {

  4.  const keys = Object.keys(effects)

  5.  let completedCount = 0

  6.  const results = {}

  7.  const childCbs = {}

  8.  keys.forEach(key => {

  9.    const chCbAtKey = (res, isErr) => {

  10.      if (isErr || res === TASK_CANCEL) {

  11.        // 其中一个 sub-effect 发生错误时,立刻调用 cb 来结束 all-effect

  12.        cb.cancel()

  13.        cb(res, isErr)

  14.      } else {

  15.        results[key] = res

  16.        completedCount++

  17.        if (completedCount === keys.length) {

  18.          cb(results )

  19.        }

  20.      }

  21.    }

  22.    childCbs[key] = chCbAtKey

  23.  })

  24.  keys.forEach(key => digestEffect(effects[key], childCbs[key]))

  25. }

race 和其他一些常见的 effect 也是通过相同的方式实现的。 little - saga / commonEffects 提供了 7 种来自 redux-saga 的常用类型拓展,包括:all / race / apply / call / cps / getContext / setContext。当我们想要在代码中使用这些类型时,我们可以使用 Env 来加载 commonEffects:

  1. import { Env, io } from 'little-saga'

  2. import commonEffects from 'little-saga/commonEffects'

  3. // 调用 use(commonEffects) 之后,就能在代码中使用 commonEffects 提供的拓展类型了

  4. new Env().use(commonEffects). run(function* rootSaga() {

  5.  yield io.race({

  6.    foo: io.cps(someFunction),

  7.    foo: io.call(someAPI),

  8.  })

  9. })

3.2 channelEffects 拓展

little - saga / channelEffects 提供了 5 种和 channel 相关的类型拓展(take / takeMaybe / put / actionChannel / flush),并从 redux-saga 拷贝了 channel / buffers 的代码。

env . use ( channelEffects ) 不仅会添加类型拓展,还会在 ctx.channel 上设置一个默认 channel。当使用 put/take effect 时,如果没有指定 channel 参数,则默认使用 ctx.channel。

使用 little-saga 中的 channel,可以实现任意两个 Task 之间的通信。不过 channel 又是一个很大的话题,本文就不再详细介绍了。channel 相关源码的可读性相当不错,欢迎直接阅读源码。

3.3 compat 拓展

compat 拓展使得 little-saga 可以和 redux 进行集成,并提供与 redux-saga 一致的 API。不过 little-saga 因为 normalizedEffect 的关系,无法和其他 redux 中间件(例如 redux-thunk)共存,最终是无法完全兼容 redux-saga API 的。

little-saga 的 createSagaMiddleware 也是比较有意思的一个函数,其实现思路如下:首先使用 channelEffects 添加 channel 相关拓展;然后用 store.dispatch 替换掉 ctx.channel.put,这样一来 put effect 会转换为对 dispatch 函数的调用;另一方面, createSagaMiddleware 返回一个 redux 中间件,该中间件会将所有的 action(回想一下,redux 中 action 只能来自于 dispatch)put 回原来的 channel 中,这样所有 action 又能够重新被 take 到了;当然,中间件也使用了 getState 来实现 select effect。代码如下:

  1. function createSagaMiddleware(cont) {

  2.  function middleware({ dispatch, getState }) {

  3.    let channelPut

  4.    const env = new Env(cont)

  5.      .use(commonEffects)

  6.      .use(channelEffects)

  7.      .use(ctx => {

  8.        // 记录「真实」的 channel.put

  9.        channelPut = ctx.channel.put

  10.        // 使用 dispatch 替换掉 channel 上的 put 方法

  11.        ctx.channel.put = action => {

  12.          action[SAGA_ACTION] = true

  13.          dispatch(action)

  14.        }

  15.        // 使用 def 方法来定义 select 类型的 effect-runner

  16.        def(ctx, 'select', ([_effectType, selector = identity, ...args], _ctx, cb) =>

  17.          cb(selector(getState(), ...args)),

  18.        )

  19.      })

  20.    // 当 middleware 函数执行时,说明 store 正在创建

  21.    // 此时我们给 middleware.run 设置正确的函数

  22.    middleware.run = (...args) => env.run(...args)

  23.    return next => action => {

  24.      const result = next(action) // hit reducers

  25.      // 下面的 if-else 主要是为了保证 channelPut(action) 恰好被包裹在一层 asap 中

  26.      // asap 的介绍见 3.4

  27.      if (action[SAGA_ACTION]) {

  28.        // SAGA_ACTION 字段为 true 表示该 action 来自 saga

  29.        // 而在 saga 中,我们在 put 的时候已经使用了函数asap

  30.        // 所以在这里就不需要再次调用 asap 了

  31.        channelPut(action)

  32.      } else {

  33.        // 表示该 action 来自 store.dispatch

  34.        // 例如某个 React 组件的 onClick 中调用了 dispatch 方法

  35.        asap(() => channelPut(action))

  36.      }

  37.      return result

  38.    }

  39.  }

  40.  middleware.run = (...args) => {

  41.    throw new Error('运行 Saga 函数之前,必须使用 applyMiddleware 将 Saga 中间件加载到 Store 中')

  42.  }

  43.  return middleware

  44. }

3.4 scheduler

asap / suspend / flush 是来自于 scheduler.js 的方法。asap 被用在 put effect 中,而后面两个函数被用在 fork/spawn effect 中。

scheduler 主要是处理 「嵌套 put」问题。考虑下面的代码,rootSaga 会 fork genA 和 genB,genA 会先 put-A 然后 take-B,而 genB 会先 take-A 然后 put-B。

  1. function* rootSaga() {

  2.  yield fork(genA) // LINE-1

  3.  yield fork(genB) // LINE-2

  4. }

  5. function* genA() {

  6.  yield put({ type: 'A' })

  7.  yield take('B')

  8. }

  9. function* genB() {

  10.  yield take('A')

  11.  yield put({ type: 'B' })

  12. }

在使用 scheduler 的情况下,这两次 take 都是可以成功的,即 genA 可以 take 到 B,而 genB 可以 take 到 A,这也是所我们期望的情况。

假设在不使用 scheduler 的情况下,put-A 唤醒了 take-A。因为这里的 put/take 的执行都是同步的,所以 take-A 被唤醒之后执行的下一句是 genB 中的 put-B,而此时 genA 还处于执行 put-A 的状态,genA 将丢失 B。也就是说在不使用 scheduler 的情况下,嵌套的 put 很有可能导致部分 action 的丢失。

使用函数 asap 包裹 put 的过程,可以保证「内层的 put」延迟到「外层的 put 执行结束时」才开始执行,从而避免嵌套 put。asap 是 as soon as possible 的缩写, asap ( fn ) 的意思可以理解为「当外层的 asap 任务都执行完之后,尽可能快地执行 fn」。

我们再考虑上面代码中的 LINE-1 和 LINE-2,在不使用 scheduler 的情况下,这两行代码的前后顺序会影响运行结果:因为默认 channel 用的是 multicastChannel,multicastChannel 没有缓存(buffer),所以为了能够成功 take-A,take-A 必须在 put-A 之前就开始执行。

使用函数 suspend/flush 包裹 fork/spawn 的过程,可以保证「fork/spawn 中的同步 put」延迟到「fork/spawn 执行结束时」才开始执行。这样一来,take-A 总是能比 put-A 先执行,LINE-1 和 LINE-2 的前后顺序就不会影响运行结果了。

3.5 其他细节问题

本小节记录了 redux-saga/little-saga 中仍存在的一些细节问题,不过这些问题在平时编程中较为少见,影响也不大。

Task 的「取消」和「完成」是互斥的。Task 被取消时代码会直接跳转进入 finally 语句块,但此时仍有可能发生错误,即发生了「执行 cancel 逻辑时发生错误」的现象。此时 Task 的状态已经为「被取消」,我们不能将 task 的状态修改为「完成(出错)」。对于这类错误,little-saga 只是简单地使用 console.error 进行了打印,并没有较为优雅的处理方式。所以我们在使用 redux-saga/little-saga 写代码的时候,尽量避免过于复杂的 cancel 逻辑,以防在 cancel 逻辑中发生错误。

当一个往 channel 中 put 一个 END 的时候,正在 take 该 channel 的该怎么办?redux-saga 中的处理比较奇怪,我询问了一下作者,他表示这是一个用在服务端渲染的 hack。而 little-saga 中做了简化,如果 take 得到了 END,那么就将 END 看作是 TASK_CANCEL。

有很多内容本文没有提到,例如「调用迭代器的 throw/return 方法时代码的执行顺序」,「异常的捕获与处理」等。另外,redux-saga 的源码中也有多处用 TODO 进行了标记,所以还有许多问题等待这去解决。

3.6 总结

fork model 是一个非常优秀的异步逻辑处理模型,在阅读 redux-saga 源码和测试,进而实现 little-saga 的过程中,我也学到了非常多新知识。如果大家有什么问题或建议的话,欢迎一起探讨。








请到「今天看啥」查看全文


推荐文章
新生大学  ·  君子爱财,取之有道
7 年前
笑点研究所  ·  我是哺乳动物却享受了卵生动物的待遇
7 年前
阿尔法工场研究院  ·  马斯克与特朗普决裂,特斯拉的补贴会被取消?
7 年前