关于EventLoop中宏任务是否会等待队列空后才被放入队尾的检验

464 阅读2分钟

先说结论:会!

关于事件循环的机制,很多文章都有比较详细的介绍。

一、 事件循环的大概流程

大概意思是:

  1. 调用栈执行代码时,如果是同步代码则立即执行;
  2. 如果是微任务(promise等)则放入队列尾部,等待执行;
  3. 如果是宏任务(setTimeout等)则交给Web API处理,Web API会在“相应时机”将任务移交到队列尾部,等待执行;
  4. 循环就是一旦函数调用栈的同步代码执行完后就会从队列头部摘下一个事件来执行,执行完后再去队列头部取...一直这么循环

关于第4点,《你不知道的JavaScript》中提供了这么一段伪代码:

// eventLoop是一个用作队列的数组
// (先进,先出)
var eventLoop = []
var event

// 永远执行
while (true) {
    // 一次tick
    if (eventLoop.length > 0) {
        // 拿到队列中的下一个事件
        event = evenLoop.shift()
        
        // 现在,执行下一个事件
        try {
            event()
        }
        catch (error) {
            reportError(error)
        }
    }
}

这里,有一个用while循环实现的持续运行的循环,循环的每一轮称为一个tick。对每个tick而言,如果在队列中有等待的事件,那么就会从队列中摘下一个事件并执行。

二、 我的疑点

我们知道调用栈空的时候才会取队列首部的任务。

那对于宏任务,Web API何时真正将它们放入队列尾部?很多文章只说了要等待,这里的等待是等待队列中已有的任务(立即放入队列,但排在已有任务后面,即队尾),还是要等待队列真正清空才会放入队列中?

MDN上有这么一句话:

如果队列中没有其他消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。但是,如果有其他消息,setTimeout 消息必须等待其他消息处理完。

由此可见,对于上述疑问,答案是后者:Web API在理论时间到后不会立马将任务放入队列中,而是要等待队列中的所有任务被执行栈取完。

这也是setTimeout往往不会在设置的时间到后立马执行的原因。

三、我的验证

首先看代码:

setTimeout(() => {
    throw Error('宏任务')
}, 0);
// 创建100000个微任务
for(let i = 0; i < 100000; i++) {
    Promise.resolve().then(() => {
        console.log('微任务:', i)
    })
}

执行效果:

Code_d8J80KwaXI.gif

四、结论

即使立即创建了一个宏任务,它也会等待不断增加的的微任务队列执行完毕后,才会执行。