关于事件循环机制我见过最恶心的一题

81 阅读3分钟

前言

之前我写过关于事件循环机制的文章,总的来说代码可以分为同步代码(不耗时)和异步代码(需要耗时) 异步代码又分为宏任务和微任务

  • 异步代码:
    • 宏任务队列:script,setTimeout,setInterval,setImmediate,I/O,UI渲染,requestAnimationFrame(下一帧渲染)

    • 微任务队列:promise.then,process.nextTick,MutationObserver

事件循环机制有下面几个步骤:

  1. 执行同步代码(也叫宏任务)
  2. 执行微任务队列中的任务
  3. 有必要的话渲染
  4. 执行宏任务队列中的任务,开启下一次事件循环
console.log(1);

async function A() {
  await B()
  console.log(2);
}
async function B() {
  console.log(3);
}
A()

setTimeout(() => {
  console.log(4);
}, 0);

new Promise((resolve, reject) => {
  console.log(5);
  resolve()  
})
.then(() => {
  console.log(6);
})
.then(() => {
  console.log(7);
})
console.log(8);

因此这段代码的输出顺序应该是这样的:第一行为同步代码直接输出1;定义了函数A和B,到第10行A函数调用,await B(),await会将B当同步代码执行,并将后续代码推入微任务队列,所以会打印3;到12行的定时器进入宏任务队列;16行是同步代码,会打印5;后续的两个.then是微任务进入微任务队列,26行的是同步任务打印8;同步代码执行完后按照微任务队列执行微任务,打印2,6,7;微任务执行完毕后按照宏任务队列执行宏任务打印4,同时开启下一次事件循环.因此结果是1,3,5,8,2,6,7,4

image.png

接下来就是恶心的题目了,希望大家能看懂后,遇到类似的问题不会被坑

var a
var b = new Promise((resolve) => {
 console.log(1);
 setTimeout(() => {
 resolve(2)
 }, 1000)
}).then(() => {
 console.log(3);
}).then(() => {
 console.log(4);
}).then(() => {
 console.log(5);
})

a = new Promise(async (resolve) => {
 console.log(a)
 await b
 console.log(a)
 await (a)
 resolve(true)
 console.log(6);
})

console.log('end');

结果是这样的:

image.png

接下来我们好好说一说什么情况

首先第一行var a全局定义了一个a值为undefined,到了第三行打印1;定时器进入宏任务队列;后面三个.then进入微任务队列,到了16行打印a,因为15行给a赋值里的promise对象的回调函数还没执行完所以a还是undefined打印;await b后的代码进入微任务队列,24行打印end.开始执行微任务队列的微任务,所以打印3,4,5;第18行的打印a,因为15行给a赋值里的promise对象的回调函数执行完了所以会是一个状态为pending的promise对象.此时问题来了,为什么不会打印6呢?这就和async和await的一个机制有关了

-async/await

  • async函数返回一个promise对象
  • await后面的接的代码会作为同步代码执行
  • await会将后续代码挤入微任务队列中
  • await后面接的promise状态无法变更为fullfilled或rejected的话会发生阻塞(死锁),代码会一直停留在await上,出现这种情况await会让出主线程执行其他可行的任务

第17行的await b其实就发生了死锁情况,resolve(2)在定时器宏任务当中还没改变promsie状态,但因为别的线程还有其他任务所以await让出主线程执行其他可行的任务,因此await b会卡了1s执行宏任务计时器将promise状态进行变更,脱离卡死,继续执行微任务,到了await (a),同理应该让出主线程执行其他可行的任务,但此时已经没有其他任务了.所以会一直卡死,不会打印6.