根 Saga 模式
根 Saga 将多个 Saga 聚合到一个单一入口点,供 sagaMiddleware 运行。
在 初学者教程 中,展示了您的根 Saga 应该类似于以下代码
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
// code after all-effect
}
这是实现根 Saga 的几种方法之一。在这里,all
效果与数组一起使用,您的 Saga 将并行执行。其他根实现可能有助于您更好地处理错误和更复杂的数据流。
非阻塞 fork 效果
贡献者 @slorber 在 issue#760 中提到了其他几种常见的根实现。首先,有一种流行的实现,其行为类似于教程根 Saga 的行为
export default function* rootSaga() {
yield fork(saga1)
yield fork(saga2)
yield fork(saga3)
// code after fork-effect
}
使用三个唯一的 yield fork
将三次返回任务描述符。您应用程序中的结果行为是,所有子 Saga 都将以相同的顺序启动和执行。由于 fork
是非阻塞的,因此 rootSaga
可以在子 Saga 继续运行并被其内部效果阻塞时完成。
一个大型 all 效果与多个 fork 效果之间的区别在于,all
效果是阻塞的,因此all 效果后的代码(参见上面代码中的注释)在所有子 Saga 完成时执行,而 fork
效果是非阻塞的,因此fork 效果后的代码在生成 fork 效果后立即执行。另一个区别是,您可以在使用 fork 效果时获得任务描述符,因此在后续代码中,您可以通过任务描述符取消/加入分叉的任务。
在 all 效果中嵌套 fork 效果
const [task1, task2, task3] = yield all([ fork(saga1), fork(saga2), fork(saga3) ])
在设计根 Saga 时,还有另一种流行的模式:在 all
效果中嵌套 fork
效果。通过这样做,您可以获得一个任务描述符数组,并且 all
效果后的代码将立即执行,因为每个 fork
效果都是非阻塞的,并且同步返回一个任务描述符。
请注意,虽然fork
效果嵌套在all
效果中,但它们始终通过底层的forkQueue连接到父任务。来自分叉任务的未捕获错误会冒泡到父任务,从而中止父任务(及其所有子任务) - 它们无法被父任务捕获。
避免将fork效果嵌套在race效果中
// DO NOT DO THIS. The fork effect always wins the race immediately.
yield race([
fork(someSaga),
take('SOME-ACTION'),
somePromise,
])
另一方面,race
效果中的fork
效果很可能是一个错误。在上面的代码中,由于fork
效果是非阻塞的,它们将始终立即赢得比赛。
保持根任务存活
实际上,这些实现并不十分实用,因为您的rootSaga
将在任何单个子效果或saga中的第一个错误时终止,并使您的整个应用程序崩溃!特别是Ajax请求会使您的应用程序受制于您应用程序发出的任何端点的状态。
spawn
是一个效果,它将断开您的子saga与其父级的连接,允许它失败而不会使父级崩溃。显然,这并不能让我们免除作为开发人员的责任,我们仍然需要处理出现的错误。事实上,这可能会掩盖开发人员视角中的某些错误,并导致以后出现问题。
spawn
效果可能被认为类似于React中的spawn
效果,因为它可以用作saga树中某一级的额外安全措施,切断失败的功能,而不让整个应用程序崩溃。不同之处在于,没有像React错误边界中存在的componentDidCatch
这样的特殊语法。您仍然需要编写自己的错误处理和恢复代码。
export default function* rootSaga() {
yield spawn(saga1)
yield spawn(saga2)
yield spawn(saga3)
}
在此实现中,即使一个saga失败,rootSaga
和其他saga也不会被杀死。但是,这也可能存在问题,因为失败的saga在应用程序的生命周期内将不可用。
保持所有任务存活
在某些情况下,可能希望您的saga能够在发生故障时重新启动。好处是您的应用程序和saga可能在失败后继续工作,例如,一个yield takeEvery(myActionType)
的saga。但我们不建议将其作为保持所有saga存活的通用解决方案。让您的saga以理智和可预测的方式失败并处理/记录您的错误更有意义。
例如,@ajwhite 提供了以下场景,在这种场景中,保持您的saga存活会导致比解决的问题更多的问题
function* sagaThatMayCrash () {
// wait for something that happens _during app startup_
yield take('APP_INITIALIZED')
// assume it dies here
yield call(doSomethingThatMayCrash)
}
如果sagaThatMayCrash重新启动,它将重新启动并等待一个仅在应用程序启动时发生一次的操作。在这种情况下,它会重新启动,但永远不会恢复。
但对于从启动中受益的特定情况,用户@granmoe 在issue#570中提出了类似于此的实现
function* rootSaga () {
const sagas = [
saga1,
saga2,
saga3,
];
yield all(sagas.map(saga =>
spawn(function* () {
while (true) {
try {
yield call(saga)
break
} catch (e) {
console.log(e)
}
}
}))
);
}
此策略将我们的子saga映射到生成的生成器(将它们与根父级分离),这些生成器在try
块中启动我们的saga作为子任务。我们的saga将一直运行到终止,然后自动重新启动。catch
块无害地处理可能由我们的saga抛出并终止的任何错误。