写在前面

在 JS 引擎处理任务的过程中会碰到两种任务,分别是同步任务和异步任务。JS 引擎是单线程处理任务的,就是说在同一时间只能处理一个任务,当 JS 碰到异步任务时如果停下来一直等待异步任务执行完再进行下一步的任务,那 JS 引擎的处理效率就会大大地降低,用户体验也非常不好。

因此,JS 引擎就将这些异步任务放到一个 “任务队列” 中,所有同步任务都放在一个 “执行栈” 中,JS 引擎在执行任务时,先将 “执行栈” 中的任务全部执行完后,此时 JS 引擎处于空闲状态,其就会去看看 “任务队列” 中有没有任务可以执行,有任务可以执行时,就将任务依次出队进行执行。

按照上述的思路,询问查看 “任务队列” 是否有可执行的任务的事情是由 JS 做的,那 JS 是会处于一个轮询的状态。但实际上不是的,异步任务的具体可执行时间是不确定的,JS 不可能留出多余的精力给 “任务队列” 的询问查看。因此就交给了 Event Loop 去实现。

因此,Event Loop 是专门用于处理异步任务的设计。

Event Loop 会实时记录 “任务队列” 中的异步任务的信息,如异步任务中有 setTimeout 计时器的存在,Event Loop 会去计时,当时间到的时候就会将计时器对应的函数体交给 JS 引擎去执行。

Event Loop 会对事件处理顺序进行管理。当多个异步任务同时到期时应该优先执行哪一个,这就需要事先规定好,事件是要分优先级的。也是 Event Loop 中规定好的。

因此 Event Loop 是负责 “任务队列” 的实时运行的。

1. Node.js 中的 Event Loop 的循环结构


截屏2023-07-15 17.29.22.png

  1. Timers 阶段
    setTimeout 就存放在该阶段的队列中,当轮到 Timers 阶段执行的时候,需要查看任务队列中等待的时间是否到了,若没到,就继续往下走 Event Loop。
  2. poll 阶段
    大部分时间都停留在 poll 阶段等待,大部分事件也在该阶段被处理,比如文件,网络请求。其当没有任务可以执行时,只能停留在该阶段。
  3. check 阶段
    setImmediate 就是放在该阶段的队列中,当轮到 check 阶段执行的时候,该任务队列中有就立马执行,不会等待。

process.nextTick() 不属于 Event Loop 的一部分,process.nextTick方法指定的回调函数,总是放在当前阶段的后面执行。是在同步任务执行栈执行完毕后开始就立马执行 。

为什么说 setImmediate 总是比 setTimeout 先执行呢?那是因为当 Event Loop 开启的时候,事件循环就会停留在 poll 阶段,当开始执行的时候,就会从 poll 阶段开始往下执行,check 阶段在 Timers 阶段前面,所以才导致 setImmediate 总是比 setTimout 先执行。但是如果 JS 执行的时候,Event Loop 还没开启,就会从 Timers 阶段开始往下执行 Event Loop,此时的 setTimeout 就会比 setImmediate 先执行了。

2. 浏览器中的 Event Loop 的阶段:宏任务和微任务

2.1 MacroTask 宏任务(一会儿)包含:

  1. setTimeout
  2. setInterval

2.2 MicroTask 微任务(马上)包含:

  1. Promise.then
  2. await / async

当 Event Loop 中的同一阶段中既有宏任务队列,又有微任务队列时,总是先执行完微任务队列的全部任务后,再去执行宏任务队列中的任务。

参考链接

JavaScript 运行机制详解:再谈Event Loop
js中的宏任务与微任务
Event Loop、计时器、nextTick
理解事件循环二(macrotask和microtask)