同步错误,try catch就可以捕获。
而异步任务的错误,无论是setTimeout 还是promise.then,单独的try catch都无法捕获。

  1. // 并不会被catch所捕获,程序直接报错
  2. function func() {
  3. try {
  4. setTimeout(() => {
  5. throw new Error('opps! error')
  6. }, 1000)
  7. } catch(e) {
  8. console.log(e, 'err')
  9. }
  10. }
  11. func();

因为事件循环的机制,在执行try catch时,遇到setTimeout,视为异步任务并调用WebApis,定时器线程(另外的线程,由宿主环境提供)开始工作,1000毫秒后,将setTimeout的callback加入到任务队列中(注意,是在另外的线程中等待1000毫秒过去然后将setTimeout的callback加入到任务队列中,而不是直接将setTimeout加入到任务队列中)。当前的try catch执行完毕,执行栈退出的时刻,开始在任务队列中读取队列中的callback,同时形成callback相应的执行栈。由于try catch 的执行栈已经退出,catch是无法拿到任务队列中的callback执行栈里抛出的错误的。
同理,promise.then中的错误也是无法try catch的,因为promise.then属于微任务,将在当前执行栈为空后,才去任务队列中查找微任务,微任务形成自己的执行栈,try-catch执行栈早就退出了,所以无法捕获。

一. promise的异常捕获

1.构造函数中

  1. function main1() {
  2. try {
  3. new Promise(() => {
  4. throw new Error('promise1 error')
  5. })
  6. } catch(e) {
  7. console.log(e.message);
  8. }
  9. }
  10. function main2() {
  11. try {
  12. Promise.reject('promise2 error');
  13. } catch(e) {
  14. console.log(e.message);
  15. }
  16. }
  17. // 以上两个方法里的try catch都不能捕获error,因为promise内部的错误不会冒泡出来,直接被promise吞掉了,只能被promise.catch才可以捕获。即使是在catch中再throw error,也可以被后续的catch所捕获。

2.then 中的异常捕获

  1. function main3() {
  2. Promise.resolve(true).then(() => {
  3. try {
  4. throw new Error('then');
  5. } catch(e) {
  6. return e;
  7. }
  8. }).then(e => console.log(e.message));
  9. }
  10. // 可以在回调函数内部进行try-catch,并将error返回,这时的error会传递给下一个then中。
  11. // 或者在then中直接throw error(或者reject),并在catch中捕获
  12. function main4() {
  13. Promise.resolve(true).then(() => {
  14. throw new Error('then');
  15. }).catch(e => console.log(e.message));
  16. }

tip: 当promise的实例resolve后,错误无法被捕获

  1. var promise=new Promise(function(resolve,reject){
  2. resolve('hi');
  3. throw new Error('test');//该错误无法被捕获
  4. })
  5. promise.then(function(e){
  6. console.log('then ---', e)
  7. }).catch(function(e){
  8. console.log('error ---', e)
  9. })
  10. // catch无法捕获,也不会有报错
  11. // 因为promise内部的error都被promise吞掉了,所以无法冒泡到外层。
  12. // 但是promise一旦resolve或者reject了,状态就不会再改变,所以throw error 也无法改变promise已经被resolve (fullfilled)的事实,即不会有效果
  13. // 注意!!!在resolve或者reject后的代码依旧会继续执行的,所以要根据实际情况选择resolve/reject的位置,或者加return

总结:

  1. promise内部的错误(reject、throw)被promise内部处理,可被catch捕获。
  2. promise一旦状态完成,不会再改变。resolve后再throw error没有作用。
  3. resolve/reject后的代码仍然会被执行。

二. async/await 异常捕获

async函数可以保留运行堆栈

  1. const fetchFailure = () => new Promise((resolve, reject) => {
  2. setTimeout(() => {// 模拟请求
  3. if(1) reject('fetch failure...');
  4. })
  5. })
  6. async function main () {
  7. try {
  8. const res = await fetchFailure();
  9. console.log(res, 'res');
  10. } catch(e) {
  11. console.log(e, 'e.message');
  12. }
  13. }
  14. main();
  15. // 当promise中出现异常,会被promise捕获,然后调用generator的throw方法,抛出错误,而async方法可以吧保留运行堆栈,错误被catch捕获。
  16. // async函数可以保留运行堆栈
  17. const a = async () => {
  18. await b();
  19. c();
  20. };
  21. // 上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包括a()。

async的实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

  1. async function fn(args) {
  2. // ...
  3. }
  4. // 等同于
  5. function fn(args) {
  6. return spawn(function* () {
  7. // ...
  8. });
  9. }

所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。
下面给出spawn函数的实现,基本就是前文自动执行器的翻版。

  1. function spawn(genF) {
  2. return new Promise(function(resolve, reject) { // 返回一个promise
  3. const gen = genF(); // 生成器函数,可能调用它的next或者throw方法
  4. function step(nextF) { // 不断next,直到结束
  5. let next;
  6. try {
  7. next = nextF();
  8. } catch(e) {
  9. return reject(e);
  10. }
  11. if(next.done) {
  12. return resolve(next.value);
  13. }
  14. // 这里的next.value,即被await的promise请求等异步函数,如果这个promise失败,则进入then的失败回调中,抛错
  15. Promise.resolve(next.value).then(function(v) {
  16. step(function() { return gen.next(v); });
  17. }, function(e) {
  18. step(function() { return gen.throw(e); }); // Generator.prototype.throw()把错误抛出来,才能被外层的try-catch捕获到
  19. });
  20. }
  21. step(function() { return gen.next(undefined); });
  22. });
  23. }

总结:

  1. async返回的是promise,可以被then、catch。async函数本身是个异步的。
  2. async内部可以使用try-catch捕获异常。
  3. async内部,遇到await则暂停执行,控制权交给async函数的父级函数,等异步返回后,重新获取控制权,继续执行下方代码。

其他知识点:

宏任务、微任务

在任务队列这里,还有一个小的区分。异步任务分为宏任务(macro task)和微任务(micro task),并且会添加到不同的任务队列中。

tasks

不过查阅 html 规范中,并没有 macro task 的定义(我是看别人文章都这么写),为了严谨性,下面都称为 tasks。 一个事件循环中可能会有一个或多个 tasks,这个是根据 task 源划分的,比如事件(鼠标单击、键盘操作)就是一个 task 源,可能会放到一个任务队列中,XHR 的回调放到一个队列中,但是具体的优先级,这么多 tasks 到底先从哪个取,这个浏览器会根据情况去获取以达到更好的交互体验,先不放到本次研究范围,大概了解 tasks 会按照源去分成多个就好了。 常见的宏任务 tasks 包括:

  • XMLHttpRequest 回调
  • 事件回调(onClick)
  • setTimeout/setInterval
  • history.back

宏任务会被加入到下一个事件循环的队列的任务队列中。

micro-task

microtask queue 在每个事件循环中只有一个,跟 tasks 区分,它的本意是尽可能早的执行异步任务。常见的 microtask 包括:

  • Promise.then
  • MutationObserver
  • Object.observe
  • process.nextTick(node 中)

Promise.then 在不同的平台实现方式不同,不过大多数都参照 promise/A+ 规范,是当做 microtask 处理的。

microtask 是在一个事件循环结束后(执行栈为空时),立即执行,即 tasks 的一个任务执行后,并且会清空 microtask 队列。另外,如果 microtask 中新添加了 microtask,会放到 queue 末尾一起执行。

当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,执行完毕后,再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

视频: what is event loop

  1. yum install telnet
  2. apk add telnet
  3. apt-get install telnet
  4. 各个系统自取安装方式