跳至主要内容

redux-saga 的 fork 模型

redux-saga 中,您可以使用 2 种 Effects 动态地 fork 在后台执行的任务。

  • fork 用于创建附加的 fork
  • spawn 用于创建分离的 fork

附加的 fork(使用 fork

附加的 fork 通过以下规则保持与其父级相连

完成

  • Saga 仅在以下情况下终止:
    • 它终止自己的指令主体
    • 所有附加的 fork 本身都已终止

例如,假设我们有以下内容:

import { fork, call, put, delay } from 'redux-saga/effects'
import api from './somewhere/api' // app specific
import { receiveData } from './somewhere/actions' // app specific

function* fetchAll() {
const task1 = yield fork(fetchResource, 'users')
const task2 = yield fork(fetchResource, 'comments')
yield delay(1000)
}

function* fetchResource(resource) {
const {data} = yield call(api.fetch, resource)
yield put(receiveData(data))
}

function* main() {
yield call(fetchAll)
}

call(fetchAll) 将在以下情况下终止:

  • fetchAll 本身终止,这意味着执行了所有 3 个 Effects。由于 fork Effects 是非阻塞的,因此任务将在 delay(1000) 上阻塞。

  • 两个 fork 的任务终止,即在获取所需资源并发出相应的 receiveData 操作后。

因此,整个任务将阻塞,直到延迟 1000 毫秒过去并且task1task2 完成了它们的工作。

例如,假设延迟 1000 毫秒过去,而这两个任务尚未完成,那么 fetchAll 仍然会等待所有 fork 的任务完成,然后再终止整个任务。

细心的读者可能已经注意到 fetchAll saga 可以使用并行 Effect 重写。

function* fetchAll() {
yield all([
call(fetchResource, 'users'), // task1
call(fetchResource, 'comments'), // task2,
delay(1000)
])
}

实际上,附加的 fork 与并行 Effect 具有相同的语义。

  • 我们正在并行执行任务。
  • 父级将在所有启动的任务终止后终止。

这同样适用于所有其他语义(错误和取消传播)。您可以通过将其视为动态并行 Effect 来理解附加的 fork 的行为。

错误传播

遵循相同的类比,让我们详细检查一下并行 Effects 中如何处理错误。

例如,假设我们有以下 Effect:

yield all([
call(fetchResource, 'users'),
call(fetchResource, 'comments'),
delay(1000)
])

上述 Effect 将在 3 个子 Effect 中的任何一个失败时立即失败。此外,未捕获的错误将导致并行 Effect 取消所有其他挂起的 Effect。例如,如果 call(fetchResource, 'users') 抛出未捕获的错误,则并行 Effect 将取消另外两个任务(如果它们仍在挂起),然后使用来自失败调用的相同错误中止自身。

类似地,对于附加的 fork,Saga 将在以下情况下中止:

  • 其主要指令体抛出错误

  • 其附加的分支之一引发了未捕获的错误

所以在前面的例子中

//... imports

function* fetchAll() {
const task1 = yield fork(fetchResource, 'users')
const task2 = yield fork(fetchResource, 'comments')
yield delay(1000)
}

function* fetchResource(resource) {
const {data} = yield call(api.fetch, resource)
yield put(receiveData(data))
}

function* main() {
try {
yield call(fetchAll)
} catch (e) {
// handle fetchAll errors
}
}

例如,如果在某一时刻,fetchAll 被阻塞在 delay(1000) Effect 上,并且假设 task1 失败了,那么整个 fetchAll 任务将失败,导致

  • 取消所有其他挂起的任务。这包括

    • 主任务fetchAll 的主体):取消它意味着取消当前的 Effect delay(1000)
    • 其他仍然挂起的派生任务。例如,我们例子中的 task2
  • call(fetchAll) 将会自身抛出一个错误,该错误将在 maincatch 体中被捕获

注意,我们能够在 main 中捕获来自 call(fetchAll) 的错误,仅仅因为我们使用的是阻塞调用。并且我们无法直接从 fetchAll 中捕获错误。这是一个经验法则,**你无法捕获来自派生任务的错误**。附加分支中的失败将导致派生父级中止(就像无法捕获并行 Effect 中的错误一样,只能通过阻塞并行 Effect 从外部捕获)。

取消

取消 Saga 会导致取消

  • 主任务,这意味着取消 Saga 被阻塞的当前 Effect

  • 所有仍在执行的附加分支

WIP

分离分支(使用 spawn

分离分支在它们自己的执行上下文中运行。父级不会等待分离分支终止。来自派生任务的未捕获错误不会向上冒泡到父级。取消父级不会自动取消分离分支(你需要显式地取消它们)。

简而言之,分离分支的行为就像使用 middleware.run API 直接启动的根 Saga。

WIP