adodo0829 / blog

搭建知识体系
29 stars 4 forks source link

JS中异步任务错误和中断处理 #106

Open adodo0829 opened 2 years ago

adodo0829 commented 2 years ago
// 一次异步任务中断引起的思考
// 单个异步请求的异常捕获: 在promise.catch()中获取, 做对应的逻辑处理
// 上一个请求异常, 中断后面的异步任务, 在promise.catch()能中断吗? 不能, 这个属于微任务队列里内部逻
// 问题
async function getData() {
  const data1 = await getData1();
  // 我想在data1出错的时候中断后面的任务
  const data2 = await getData2();
  handleData(data1, data2);
}

// 1.异步任务异常处理和外层中断: setTimeout, setInterval,
// setTimeout, setInterval对应的是事件循环中的宏任务
try {
  setTimeout(() => {
    let a = c;
  }, 100);
} catch (e) {
  console.log("能获取到错误么??", e);
  // 不能, why?
}

// 这样能吗?
try {
  setTimeout(() => {
    try {
      let a = c;
    } catch (e) {
      throw new Error("some variable is not defined");
    }
  }, 100);
} catch (e) {
  console.log("能获取到错误么??", e);
  // 不能, why?
}

// 根本原因还是js的事件循环, 一个事件循环包含一个或多个任务队列,
// 每一个任务队列里的任务是严格按照先进先出的顺序执行的,但是不同任务队列的任务的执行顺序是不确定的。
// 类似这种队列结构: [[宏任务1,微任务1,微任务2...], [宏任务2,..], [宏任务3,...]]
/**
 * 任务类型
 * macrotasks script(整体代码), setTimeout, setInterval,
 * microtasks Promise
 */

/**
 * 事件循环的进程模型
 * // 备注: 下面任务的执行是要放到执行栈里去的, 对应了不同上下文
 *
 * 1.选择当前要执行的任务队列,选择一个最先进入任务队列的任务,如果没有任务可以选择,则会跳转至5,microtask的执行步骤。
 * 2.将事件循环的当前运行任务设置为已选择的任务。
 * 3.运行当前任务。
 * 4.将事件循环的当前运行任务设置为null。
 *   4.1-将运行完的任务从任务队列中移除
 * 5.检查microtasks:进入microtask检查点(performing a microtask checkpoint )。
 *   5.1-设置进入microtask检查点的标志为true
 *   5.2-当事件循环的微任务队列不为空时:选择一个最先进入microtask队列的microtask
 *   5.3-设置事件循环的当前运行任务为已选择的microtask
 *   5.4-运行microtask;
 *   5.5-设置事件循环的当前运行任务为null
 *   5.6-将运行结束的microtask从microtask队列中移除
 * 6.更新界面渲染。
 * 7.返回第一步
 */

// 回顾上面的问题
/**
 * 最外层的try catch是在一个task中,我们定义它为我们js文件的同步主任务,从上到下执行到这里了, 然后,会把里面的setTimeout推到一个任务队列中, 这个队列是存储在内存中的。
 * 然后主task就继续向下执行, 一直到结束。
 * 当该setTimeout时间到了,且没有其它的task执行了, 那么,就将这个setTimeout的代码推入执行栈开始执行。 当执行到错误代码的时候,也就是这个 let a = c, 因为c未定义,所以就会报错。
 * 但问题的本质是,这个错误跟最外层的try catch并不在一个执行栈中,当里面执行的时候,外边的这个task早已执行完, 他们的context(上下文)已经完全不同了
 */

// 2.异步任务异常处理和外层中断: promise,
// Promise 也是一个异步的处理过程,它对应事件循环中的微任务
try {
  new Promise((resolve, reject) => {
    reject("promise error");
  });
} catch (e) {
  console.log("异步错能catch到吗?", e);
  // 不能, 执行栈上下文已经不同了
}

try {
  new Promise((resolve, reject) => {
    reject("promise error");
  }).catch((e) => {
    // 这样能把错误传到外面吗
    throw new Error(e);
  });
} catch (e) {
  console.log("异步错能catch到吗?", e);
  // 也是不行的
}

// 3.Async await, 通过异步等待的方式,用try catch可以吗
// 本质上也是微任务, 但是await使用生成器函数 对上下文进行暂停
const asyncFn = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("asyncFn执行时出现错误了");
    }, 100);
  });
};

const executedFn = async () => {
  try {
    await asyncFn();
  } catch (e) {
    // 能捕获错误吗?
    console.log("捕获错误", e);
    // 能
  }
  // asyncFn里面是有 Promise的,为什么外边就能catch到了呢
  // async-await 是使用生成器、promise 和协程实现的,wait操作符还存储返回事件循环之前的执行上下文,以便允许promise操作继续进行。
  // 当内部通知解决等待的承诺时,它会在继续之前恢复执行上下文。 所以说,能够回到最外层的上下文, 那就可以用try catch
};

async function asyncLogic() {
  try {
    const data1 = await getData1();
    breakLoop();
    const data2 = await getData2();
    breakLoop();
    const data3 = await getData3();
    breakLoop();
    // 处理数据
    handle(data1, data2, data3);
  } catch (err) {
    if (err === "interrupt") {
      return;
    }
    // 处理其他业务错误
    throw err;
  }

  function checkAbort() {
    // 满足某个条件
    if (condition) {
      // 抛出的功能在 catch 中检查出来就行
      throw "interrupt";
    }
  }
}