js事件循环机制(浏览器端Event Loop) 以及async/await的理解

之前面试国美的时候碰到这样的一个面试题:

console.log(1);
async function fn(){
    console.log(2)
    await console.log(3)
    console.log(4)    //最重要的是这一步不明白
}
setTimeout(()=>{
    console.log(5)
},0)
fn();
new Promise((resolve)=>{
    console.log(6)
    resolve();
}).then(()=>{
    console.log(7)
})
console.log(8)

最后的输出结果为:1 2 3 6 8 4 7 5

一开始我以为输出结果为:1 2 3 4 6 8 7 5 ,因为不明白 4 为什么会在这个位置输出出来,我本来以为 4 会在 3 之后输出出来的。

为什么 await 后面的代码会被放到任务队列里面?

所以上网搜,看到一篇文章,挺不错,摘抄下来。

=============== 分割线 ===============

原文链接:https://segmentfault.com/a/1190000017554062

注意原文的最后一个例子有误,在这里我会做修改。

事件循环机制

理解js的事件循环机制,能够很大程度的帮我们更深层次的理解平时遇到的一些很疑惑的问题

简单版本

下面来看一段代码,想想它的结果和你的结果是否一样

setTimeout(function() {
    console.log(1)
}, 0)
    
console.log(2)

//  执行结果是 2 1

我们可以将js的任务分为同步任务异步任务, 按照这种分类js的执行机制如下

  • 任务执行队列分为同步任务队列异步任务队列
  • 代码执行时,遇到同步代码,会被直接推入同步任务队列并依次执行
  • 遇到异步代码(如setTimeout、setInterval), 会被直接推入异步任务队列
  • 同步任务队列执行完毕,这个时候异步任务队列的任务会被依次推入同步任务队列并依次执行

所以上面的代码执行的时候, setTimeout()不会被立即执行,会被推到异步任务队列里面, 之后再执行console.log(2), 同步任务队列任务执行完毕之后,会去异步任务队列的任务会被依次推到 同步任务队列并执行

终极版本

下面来看一段代码,想想它的结果和你的结果是否一样

setTimeout(function() {
    console.log(1)
}, 0)
new Promise(function(resolve, reject) {
    console.log(2)
    resolve()
}).then((res) => {
    console.log(3)
})
console.log(4)

// 执行结果是 2 4 3 1

js异步任务按照准确的划分,应该将任务分为

  • 宏任务: setTimeoutsetInterval
  • 微任务: 例如Promise.then方法。注意new Promsie()的时候是同步,立即执行。

注意: 现在有三个队列: 同步队列(也称执行栈)、宏任务队列、微任务队列

所以针对这种机制,js的事件循环机制应该是这样的

  • 遇到同步代码,依次推入同步队列并执行
  • 当遇到setTimeout、setInterval,会被推到宏任务队列
  • 如果遇到.then,会被当作微任务,被推入微任务队列
  • 同步队列执行完毕,然后会去微队列取任务,直到微队列清空。然后检查宏队列,去宏队列取任务,并且每一个宏任务执行完毕都会去微队列跑一遍,看看有没有新的微任务,有的话再把微任务清空。这样依次循环
console.log(1);
    
setTimeout(() => {
    console.log('setTimeout');
}, 0);

let promise = new Promise(resolve => {
    console.log(3);
    resolve();
}).then(data => {
    console.log(100);
}).then(data => {
    console.log(200);
});

console.log(2);

//执行结果是:1 3 2 100 200 setTimeout

所以对于以上的代码执行流程如下:

  1. 遇到同步任务先输出1。
  2. setTimeout是宏任务,会先放到宏任务队列中。
  3. new Promise是立即执行的,所以会先输出3。
  4. Promise.then是微任务,会依次排列到微任务队列中,继续向下执行输出2。
  5. 现在执行栈中的任务已经清空,再将微任务队列清空,依次输出100和200。
  6. 然后每次取出一个宏任务,因为现在只有一个宏任务,所以最后输出setTimeout

async/await (重点)

(个人注解:async/await 底层依然是 Promise,所以是微任务,只是 await 比较特殊)

async

当我们在函数前使用async的时候,使得该函数返回的是一个Promise对象

async function test() {
    return 1   // async的函数会在这里帮我们隐士使用Promise.resolve(1)
}
// 等价于下面的代码
function test() {
   return new Promise(function(resolve, reject) {
       resolve(1)
   })
}

可见async只是一个语法糖,只是帮助我们返回一个Promise而已

await

await表示等待,是右侧「表达式」的结果,这个表达式的计算结果可以是 Promise 对象的值或者一个函数的值(换句话说,就是没有特殊限定)。并且只能在带有async的内部使用

使用await时,会从右往左执行,当遇到await时,  ★★★★★会阻塞函数内部处于它后面的代码,去执行该函数外部的同步代码,当外部同步代码执行完毕,再回到该函数内部执行剩余的代码★★★★★, 并且当await执行完毕之后,会先处理微任务队列的代码

下面来看一个栗子:

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' )

/**
 * 执行结果为:
 * script start
 * async1 start
 * async2
 * promise1
 * script end
 * async1 end
 * promise2
 * setTimeout
 */

使用事件循环机制分析:

  1. 首先执行同步代码,console.log( 'script start' )
  2. 遇到setTimeout,会被推入宏任务队列
  3. 执行async1(), 它也是同步的,只是返回值是Promise,在内部首先执行console.log( 'async1 start' )
  4. 然后执行async2(), 然后会打印console.log( 'async2' )
  5. 从右到左会执行, 当遇到await的时候,阻塞后面的代码,去外部执行同步代码
  6. 进入new Promise,打印console.log( 'promise1' )
  7. .then放入事件循环的微任务队列
  8. 继续执行,打印console.log( 'script end' )
  9. 外部同步代码执行完毕,接着回到async1()内部, 继续执行 await async2() 后面的代码,执行 console.log( 'async1 end' ) ,所以打印出 async1 end(个人理解:async/await本质上也是Promise,也是属于微任务的,所以当遇到await的时候,await后面的代码被阻塞了,应该也是被放到微任务队列了,当同步代码执行完毕之后,然后去执行微任务队列的代码,执行微任务队列的代码的时候,也是按照被压入微任务队列的顺序执行的)
  10. 执行微任务队列的代码, 打印 console.log( 'promise2' )
  11. 进入第二次事件循环,执行宏任务队列, 打印console.log( 'setTimeout' )

为了证实自己的验证(蓝色加粗字体),特意修改国美的面试题。

修改方式一:

console.log(1);
async function fn(){
    console.log(2)
    new Promise((resolve)=>{
        resolve();
    }).then(()=>{
        console.log("XXX")
    })
    await console.log(3)
    console.log(4)
}
fn();
new Promise((resolve)=>{
    console.log(6)
    resolve();
}).then(()=>{
    console.log(7)
})
console.log(8)

// 执行结果为:1 2 3 6 8 XXX 4 7 

分析:

  前面的 1 2 3 6 8 不再解析,重点是后面的 XXX 4 7,由此可见 await console.log(3) 之后的代码 console.log(4) 是被放入到微任务队列了,代码 console.log("XXX") 也是被压入微任务队列了,console.log("XXX")  是在 console.log(4) 之前,所以当同步任务执行完毕之后,执行微任务队列代码的时候,优先打印出来的是 XXX ,然后才是 4 。

修改方式二:

console.log(1);
new Promise((resolve)=>{
    resolve();
}).then(()=>{
    console.log("XXX")
})
async function fn(){
    console.log(2)
    await console.log(3)
    console.log(4)
    new Promise((resolve)=>{
        resolve();
    }).then(()=>{
        console.log("YYY")
    })
}
fn();
new Promise((resolve)=>{
    console.log(6)
    resolve();
}).then(()=>{
    console.log(7)
})
console.log(8)

// 执行结果为:1 2 3 6 8 XXX 4 7 YYY

分析:

  依然分析后面的 XXX 4 7 YYY 。 代码console.log("XXX") await console.log(3) 之前,所以 console.log("XXX") 被压入微任务队列的时机要比await console.log(3) 之后的代码早。 同步队列的代码执行完毕之后,执行微任务队列的代码时,console.log("XXX") 的输出要早于 console.log(4) 。而 console.log("YYY") 的代码又是一个 Promise.then 的的微任务,会继续被压入新的微任务队列。当本轮的微任务代码执行完毕之后,再去执行新的微任务队列的代码,所以 YYY 会在最后输出。


回到文章最初的面试题: 

个人猜想:是不是在 console.log(4) 前面加上 await4 是不是就可以在 3 之后打印出来了?

个人猜想修改一:

console.log(1);
async function fn(){
    console.log(2)
    await console.log(3)
    await console.log(4)    
}
setTimeout(()=>{
    console.log(5)
},0)
fn();
new Promise((resolve)=>{
    console.log(6)
    resolve();
}).then(()=>{
    console.log(7)
})
console.log(8)
// 执行结果为:1 2 3 6 8 4 7 5 

可见,个人猜想的不对,代码执行的时候,只要是碰见 await,执行完当前的 await 的代码(即 await console.log(3))之后,在 await 之后的代码(即 await console.log(4))都会被放到微任务队列里面。

如果在 await console.log(4) 后面再加上 await 的其他代码呢?

个人猜想修改二:

console.log(1);
async function fn(){
    console.log(2)
    await console.log(3)
    await console.log(4)
    await console.log("await之后的:",11)
    await console.log("await之后的:",22)
    await console.log("await之后的:",33)
    await console.log("await之后的:",44)
}
setTimeout(()=>{
    console.log(5)
},0)
fn();
new Promise((resolve)=>{
    console.log(6)
    resolve();
}).then(()=>{
    console.log(7)
})
console.log(8)

/**
 * 执行结果为:
 * 1
 * 2
 * 3
 * 6
 * 8
 * 4
 * 7
 * await之后的: 11
 * await之后的: 22
 * await之后的: 33
 * await之后的: 44
 * 5
 */

由此可见,代码执行的时候,只要碰见 await ,都会执行完当前的 await 之后,把 await 后面的代码放到微任务队列里面。但是定时器里面的 5 是最后打印出来的,可见当不断碰见 await ,把 await 之后的代码不断的放到微任务队列里面的时候,代码执行顺序是会把微任务队列执行完毕,才会去执行宏任务队列里面的代码。

原文地址:https://www.cnblogs.com/smile-fanyin/p/14622432.html