一篇文章解决Promise...then,async/await执行顺序类型题

5,587 阅读11分钟

最近我在沸点里摸鱼的时候,发现了一些掘友在不约而同的讨论一种类型的题,即Promise...then, async/await执行顺序类型的题。

截屏2021-06-10 下午10.18.25.png

截屏2021-06-10 下午10.16.31.png 恰好,在前一段时间我对此种类型的题有所研究📃。因此,我也想趁这个机会看看是否能把这种类型的题讲述清楚。

废话就不多说了,先出几道题,大伙试试看,能不能把这几道题做出来! (注:以下题目都是从网上以及掘友发的沸点里获取的)

题目一:
console.log(100);
setTimeout(() => {
  console.log(200);
})
Promise.resolve().then(() => {
  console.log(300);
})
console.log(400);
求打印结果:
题目二:
Promise.resolve().then(() => {
  console.log(1);
  throw new Error('error1')
}).catch(() => {
  console.log(2);
}).then(() => {
  console.log(3);
})
求打印结果:
题目三:
async function async1 () {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2 () {
  console.log('async2');
}
console.log('script start');
setTimeout(function () {
  console.log('setTimeout');
}, 0)
async1()
new Promise (function (resolve) {
  console.log('promise1')
  resolve()
}).then(function () {
  console.log('promise2')
})

console.log('script end')
求打印结果:
题目四:
setTimeout(() => {
  console.log('0');
}, 0)
new Promise((resolve, reject) => {
  console.log('1');
  resolve();
}).then(() => {
  console.log('2');
  new Promise((resolve, reject) => {
    console.log('3');
    resolve();
  }).then(() => {
    console.log('4');
  }).then(() => {
    console.log('5');
  })
}).then(() => {
  console.log('6');
})

new Promise((resolve, reject) => {
  console.log('7');
  resolve()
}).then(() => {
  console.log('8');
})
求打印结果:

不知道大家对于上面四道题是否感觉有点恶心?哈哈,如果感到恶心,就对了!跟着这篇文章走,相信大家在文章看完之后能轻松解决上面的几道题!

废话不多说,如果有兴趣就接着往下看吧!


首先我们需要知道这类题型实质是在考察以下几个内容

  • 异步
  • event loop 执行机制
  • Promise语法
  • async/await 语法
  • 宏任务、微任务

其实,掌握了上述的五个内容,无论这类题型怎么变,你都能将题玩于股掌之间(他强任他强,清风拂山岗)。

异步

由于能开始思考这道题的兄弟,想必还是有一定的JS基础。所以我也不再细讲啥是异步、以及event loop执行机制了(这类博文网上一抓一大把~)。我只简单分享一下我对于异步的一些感悟:

1、异步是用来解决JS单线程等待这种问题的
2、异步是基于回调函数的形式来实现的
3、常见的异步有:setTimeout、ajax、Promise……then、async/await、图片加载、网络请求资源
4、牢记5个版块  Call StackWeb APIsBrowser consoleCallback Queue
、 micro task queue 这五个版块透露出异步的执行过程
5、宏任务是在DOM渲染后触发,微任务是在DOM渲染前触发

Promise、 Async/await

很多讲Promise的文章都说过,Promise的出现是为了解决臭名昭著的callback hell。由于异步是基于回调函数的形式来实现的,那么异步就离不开回调函数。但在上古时期,反人类的嵌套回调让老一辈的程序员们苦不堪言...而Promise搭配then展现的管道式回调函数,让异步更直观、更优雅的展现出来,广受大家好评!

async/await 是ECMAScript 2017提出的内容。但事实上它们只是Promise的语法糖,但这颗🍬贼甜!

针对Promise以及async/await的骚用法,我想尝试用千层饼的套路来讲讲~

第一层:

Promise是一个类(函数),接受一个回调函数作为参数,并且这个回调函数的参数也有两个,这两个参数约定俗成被命名为 resolve, reject

Promise((resolve, reject) => {
    ...
})
第二层:

resolve, reject这两个参数其实也都是函数

Promise((resolve, reject) => {
    if (...) {
        resolve(...);     // 执行resolve函数
    } else {
        reject(...);      // 执行reject函数
    }
})
第三层:

对于紧跟Promise实例的then,其参数等于resolve接受的参数;紧跟Promise实例的catch,其参数等于reject接受的参数。

const p1 = new Promise((resolve) => {    // p为Promise实例
    const a = 100;
    resolve(a);
})

p1.then((param) => {
    console.log('param:', param);      // param: 100
})

const p2 = new Promise((resolve, reject) => {
    const a = 100;
    reject(a);
})

p2.catch((param) => {
    console.log('param', param);
})

第四层:

Promise的实例有三种状态: pending(加载中)fulfilled(执行成功)rejected(执行错误)。其实我们很好理解这三种状态,因为它们恰好对应了异步正在执行异步执行完的结果(无非成功或失败两种状态)这三种状态

三种状态的表现:

  • pending: Promise实例处于pending状态时,不会触发then和catch。
  • fufilled: Promise实例处于fulfilled状态时,只会触发then(不会触发catch)
  • rejected: Promise实例处于rejected状态时,只会触发catch(不会触发then)

(无论是then还是catch,它们里面都是回调函数)

那么问题来了,如何判断Promise实例对象是处于那种状态呢?

很简单,看下面代码:

const p = new Promise((resolve, reject) => {
  // 啥也没有做~
})

console.log('p', p);

Chrome控制台显示结果如下图:

截屏2021-06-12 下午9.00.33.png

截图说明了此时的Promise实例是处于pending状态的。其实,当Promise内部的回调不执行resolvereject的时候,Promise实例就处于pending状态!

再看下面的代码👇🏻

 const p1 = new Promise((resolve) => {    // p1为Promise实例
     const a = 100;
     resolve(a);
 })
 
console.log('p1', p1)    // fulfilled
 
 const p2 = new Promise((resolve, reject) => {
     const a = 100;
     reject(a);
 })
 
console.log('p2', p2)    // rejected

Chrome控制台显示结果如下图: 截屏2021-06-12 下午9.03.58.png

这里的代码是借用的第三层的代码,其实看到这儿就解释了第三层的套路。对于第一个Promise实例执行参数resolve就代表了,当前Promise实例对象的状态为fulfilled,因此接下来可以触发then,以及对应的回调。如果Promise实例对象执行了reject,则实例对象的状态变为了 rejected,可以触发catch~ 有兴趣的小伙伴可以试试,在fulfilled状态执行下catch或者在rejected状态下执行下then,试试能否执行代码成功。

第五层:

在第五层有3句至理名言需要知道(不需要去死记硬背,下面我会用例子帮助你理解)

无论是then还是catch里的回调内容只要代码正常执行或者正常返回,则当前新的Promise实例为fulfilled状态。如果有报错或返回Promise.reject()则新的Promise实例为rejected状态。

fulfilled状态能够触发then回调

rejected状态能够触发catch回调

举例之前我先补充一个小知识!

Promise.resolve()  表示一个fulfilled状态的Promise实例
Promise.reject()   表示一个rejected状态的Promise实例

好,我将放码过来!

题目一:
Promise.resolve().then(() => {
    console.log(1);
}).catch(() => {
    console.log(2);
}).then(() => {
    console.log(3);
})

初看此题,感觉似乎此题有点难度~ 不过,结合我的三句至理名言,我们一起来分析一下此题!!

Promise.resolve() 是一个状态为fulfilled状态的Promise实例。fulfilled状态能够触发then回调。因此,第一个then会被执行,并且能够顺利打印结果而不报错!故Promise.resolve().then(() => {console.log(1)}) 这个新的Promise实例为fulfilled状态,所以可以触发下一个then,但是无法触发catch,故catch的内容忽略,从而执行第二个then的内容。 所以,此题的打印结果为: 1 3

好了,下面再来看看第二题(此题也是文章开头出的第二题):

Promise.resolve().then(() => {
    console.log(1);
    throw new Error('error1');
}).catch(() => {
    console.log(2);
}).then(() => {
    console.log(3)
})

有了上一题的经验,做这一道题就轻松很多了~ Promise.resolve()是一个fulfilled状态的实例,所以可以触发then,而第一个then之中有throw new Error('error1') 这种报错操作,则Promise.resolve.then(() => { console.log(1); throw new Error('error1') })就是一个rejected状态的Promise实例,所以可以触发catch,catch的内容是可以正常执行的,没有报错误,则Promise.resolve().then(...).catch(...)是一个fulfilled状态的Promise实例,可以触发then,故then的内容能够被执行。

故代码结果为: 1 2 3

第六层:

做了前面两道题,不知道大家有没有一点困惑。感觉我似乎有意回避了then、catch的返回值即(return ...),而且也没有在then、catch里写具体的参数。导致我在第五层的第一句至理名言“无论是then或者catch里的内容只要正常执行或者正常返回...”也没有完全体现出来。 大家别慌,其实这正是我要讲的第六层套路~

then、catch参数的来头其实就是我在第三层套路里就讲过“紧跟Promise实例的then的参数等于resolve接受的参数;紧跟Promise实例的catch的参数等于reject接受的参数。”。如果then、catch里的回调,没有写返回内容,则then或catch后面即将被触发的then或catch是无法接受到参数的;而如果有返回内容,即return... 那么无论返回的是普通值还是是Promise实例,其实都会对应被转化为Promise的实例(Promise.resolve(...)或者Promise.reject(...))

结合代码再来理解一下~

Promise.reject('我想出错').catch((err) => {
  console.log(err);
  return '我不想出错';  // 会被自动封装成return Promise.resolve('我不想出错')
}).then(data => {
  console.log(data);
  return Promise.resolve('我不想出错')
}).then(data => {
  console.log(data);
})

代码打印结果: 我想出错 我不想出错 我不想出错

第七层:

第七层套路,我来谈谈 async/await。 因为Promise的出现主要是为了解决异步的回调地狱问题。将噩梦般的嵌套回调变为了优雅的管道式回调。但这始终是逃不掉“回调”二字。而async/await虽说只是Promise的语法糖,但让你“脱离”了回调,拥抱了同步代码~

下面我再分享大家五句经典语录~

执行async函数,返回的是Promise对象

await必须在async包裹之下执行

await相当于Promise的then并且同一作用域下await下面的内容全部作为then中回调的内容

try……catch可捕获异常,代替了Promise的catch

异步中先执行微任务,再执行宏任务

且看下面的代码分析:

async function fn() {
  return '我是async函数';
}

console.log('async:', fn());

执行结果:

截屏2021-06-13 下午10.23.56.png

这里的fn() 相当于 Promise.resolve('我是async函数'),验证了第一条语录!

(async function() {
  const p = Promise.resolve('帅得乱七八糟');
  const data = await p; // await就相当于Promise.then, 故data就是then的参数
  console.log(data);    // 这里的代码为then中回调的内容
})()

上面的这段代码,大家可以试着把async删掉,结果一定会报错!这就验证了第二条语录!上面代码的注释结合第三条语录,大家应该能够体会到await的作用!

再看下面一段代码:

(async function() {
  const p = Promise.reject('err');
  // await + try...catch 相当于 Promise.catch
  try {
    const res = await p;
    console.log(res);
  } catch(ex) {  // ex 来源于reject()里面的数
    console.error(ex); 
  }
})()    

打印结果为err,验证了第四条语录。

在此补充一点内容:

  • 常见的微任务: Promise……then、 async/await
  • 常见的宏任务: setTimeout、setInterval

考虑过在文章里谈谈宏任务和微任务的执行机制,但限于篇幅以及本篇文章的侧重点在于解题于是就不过多赘述其他内容了,有兴趣的朋友可以在评论区讨论一下。

其实看到这儿,文章开始出的所有题目都能够解决了。不信就试试💪🏻

题目三:
async function async1 () {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2 () {
  console.log('async2');
}
console.log('script start');
setTimeout(function () {
  console.log('setTimeout');
}, 0)
async1()
new Promise (function (resolve) {
  console.log('promise1')
  resolve()
}).then(function () {
  console.log('promise2')
})

console.log('script end')

此题需要注意两个点

  1. new Promise()内部的回调函数是当成同步函数执行
  2. 执行到await code时,会先执行code,再执行await
代码分析:
1. 先执行同步代码。  
2. 所以首先执行 console.log('script start');
3. setTimeout为宏任务,先不执行
4. 执行async1函数 console.log('async1 start'); 以及 async2(); await由于是Promise.then的语法糖是异步代码,先不执行
5. new Promise() 内部代码要执行,后面的then的内容为微任务先不执行
6.执行console.log('script end')
7.同步代码执行结束
8.开始按代码顺序执行微任务   
9.先执行 console.log('async1 end'); 前面说过,await下面的代码相当于then里回调的内容
10.new Promise.then里面的内容 console.log('promise2')
11. 最后执行 宏任务代码,即setTimeout里的内容

执行结果:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
题目四:
setTimeout(() => {
  console.log('0');
}, 0)
new Promise((resolve, reject) => {
  console.log('1');
  resolve();
}).then(() => {
  console.log('2');
  new Promise((resolve, reject) => {
    console.log('3');
    resolve();
  }).then(() => {     // 📌
    console.log('4');
  }).then(() => {
    console.log('5');
  })
}).then(() => {
  console.log('6');   // 📌
})

new Promise((resolve, reject) => {
  console.log('7');
  resolve()
}).then(() => {        
  console.log('8');
})
代码分析:
1.先执行同步代码
2.setTimeout 为宏任务,先不执行
3.new Promise里的代码作为同步代码,要执行 console.log('1'); 而then作为微任务,先不执行
4.又是一个new Promise,所以和第三步同理。只执行 console.log('7');
5.开始执行异步代码
6.执行第一个new Promise里的then 即console.log('2');以及new Promise的同步代码 console.log('3');
7.这步有点意思,这里不是执行console.log('4'); 而是执行console.log('8'); 
8.注释为📌的两个then是同层级的,所以按照执行顺序来打印
9.执行第三个层级的then,所有微任务代码完成
10.执行宏任务代码,即console.log('0');

代码结果:
1
7
2
3
8
4
6
5 
0

说有千层饼套路,其实只有7层套路,但这每一层套路都是我对于Promise、async/await的感悟,有些内容并未细讲,比如微任务、宏任务与DOM之间的执行顺序这种相对底层的内容本文并未谈及,毕竟这篇文章的侧重点是在解决Promise、async/await执行顺序类型题。本文也可能有内容错误或逻辑错误,也请大家多多包涵,多多指出,谢谢!!

好了,大功已告成!想必通过这些题,大家已经领悟到了解决此类题型的奥义!