try catch 包裹一个 call 调用的 fn ,那么这个 fn 里抛出错误时,是 被捕获到的

function* errorSaga() {
  throw new Error('模拟异常');
function* saga() {
  try {
    yield fork(errorSaga);
  } catch(e) {
    console.err(e);
// 调用saga

try catch包裹一个fork调用的fn,那么这个fn里抛出错误时,是不会被捕获到的。
原因在于,fork 出的 childtask,与执行 fork 的 parentTask 之间虽然是父子关系,但 childtask 是异步的,所以没办法捕获到。如果想捕获 fork 的错误,需要从 parentTask 入手

function* errorSaga() {
  throw new Error('模拟异常');
function* saga() {
  try {
    yield fork(errorSaga);
  } catch(e) { --> 这里不会捕获到,其效果与直接调用 yield fork(errorSaga); 一致
    console.err(e); -->
function* root() {
  try {
    yield call(saga)
  } catch(e) { --> 这里会捕获到 errorSaga 的错误
    console.err(e); 
// 调用root

也就是说,阻塞调用 parentTask 时,是能够捕获到 非阻塞的 childtask错误的。那如果非阻塞调用 parentTask 呢?

function* errorSaga() {
  throw new Error('模拟异常');
function* saga() {
  try {
    yield fork(errorSaga);
  } catch(e) { --> 这里不会捕获到,其效果与直接调用 yield fork(errorSaga); 一致
    console.err(e); -->
function* root() {
  try {
    yield fork(saga)
  } catch(e) { --> 这里不会捕获到 errorSaga 的错误!!!
    console.err(e); 
// 调用root

Fork vs Spawn

Spawn 类似 fork,是非阻塞的,但Spawn出的 task1是一个独立的任务,与执行Spawntask2 之间不是父子关系,task1 属于顶层任务

function* errorSaga() {
  throw new Error('模拟异常');
function* saga() {
  yield spawn(errorSaga);
function* root() {
  try {
    yield call(saga)
  } catch(e) { --> 这里不会捕获到 errorSaga 的错误
    console.err(e); 
// 调用root

未捕获 Error 场景

function* saga1 () { /* ... */ }
function* saga2 () { throw new Error('模拟异常'); }
function* saga3 () { /* ... */ }
function* rootSaga() {
  yield fork(saga1);
  yield fork(saga2);
  yield fork(saga3);
// 启动 saga
sagaMiddleware.run(rootSaga);

假设 saga2 出现代码异常了,且没有进行异常捕获,这样的异常会导致整个 Web App 崩溃么?答案是:肯定的!

解释如下: redux-saga 中执行 sagaMiddleware.run(rootsaga)fork(saga) 时,均会返回一个 task 对象(上文中说到),嵌套的 task 之间会存在 父子关系, 就比如上述代码:

  • rootSaga 生成了 rootTask。
  • saga1,saga2 和 saga3,在 rootSaga 内部执行,生成的 task,均被认为是 rootTask 的 childTask。
  • 现在某一个 childTask 异常了(比如这里的: saga2),那么它的 parentTask(如:rootTask)收到通知先会执行自身的 cancel 操作,再通知其他 childTask(如:saga1,saga3) 同样执行 cancel 操作。(这其实正是 Saga Pattern 的思想)

    但这就意味着,用户可能会因为一个按钮点击引发的异常,而导致整个 Web 应用的功能均无法使用!!

    那么,面对这样的问题,如何优化呢?隔离 childTask 是首先想到的一种方案。

    export default function* root() {
      yield spawn(saga1);
      yield spawn(saga2);
      yield spawn(saga3);
    

    使用 spawn 替换 fork,它们的区别在于 spawn 返回 isolate task,是一个顶层 task,不存在 父子关系,也就是说,即使 saga2 挂了,rootSaga 也不受影响,saga1 和 saga3 自然更不会受影响,依然可以正常工作。

    Error 捕获方案

    通过在最上层为每一个 childSaga 添加异常捕获,并通过 while(true) {} 循环自动创建新的 childTask 取代 异常 childTask,以保证功能依然可用(这就类似于 Egg 中某一个 woker 进程 挂了,自动重启一个新的 woker 进程一样)

    function* rootSaga () {
      const sagas = [ saga1, saga2, saga3 ]; 
      yield sagas.map(saga =>
        spawn(function* () {
          while (true) {
            try {
              yield call(saga);
            } catch (e) {
              console.log(e);
    复制代码
    分类:
    前端
    标签: