对于事件循环机制,我在初学JavaScript的时候了解了宏队列微队列便是事件循环的全部,但最近看了《深入浅出nodeJS》才发现我真是young and naive了。

前置知识——调用栈

JS 中会存在一个调用栈,它会负责跟踪所有待执行的操作。每当一个函数执行完成时,它就会从栈的顶部弹出。比如下面这段代码:

  1. const bar = () => console.log('bar')
  2. const baz = () => console.log('baz')
  3. const foo = () => {
  4. console.log('foo')
  5. bar()
  6. baz()
  7. }
  8. foo()

事件循环机制 Event Loop - 图1

浏览器中的事件循环

待更新。。。。

NodeJS中的事件循环

nodejs的事件循环机制是由底层libuv提供的异步IO方案。事件循环整体架构图如下图所示:
事件循环机制 Event Loop - 图2
图中有8个矩形框,其中6个构成一个环,先从构成环的6个说起。

6大阶段(宏任务)

Timer

这是事件循环的开始阶段。此阶段的队列中包含setTimeout()setInterval()的回调函数。具体而言,在代码执行过程中产生的延时函数回调,在进入队列中会按照延时的大小从小到大排序,当事件循环到本阶段时,会将到时间了的回调函数出队并执行。没有到时间的继续呆在队列中。

Pending I/O callbacks(不作为关注重点)

此阶段队列中的回调是系统相关的回调

Idle, Prepare phase(不作为关注重点)

在这个阶段,事件循环什么都不做。它处于空闲状态,准备进入下一阶段。

poll

在这个阶段,事件循环留意新的异步 I/O 回调。除了 setTimeout、setInterval、setImmediate 和关闭回调之外,几乎所有的回调都被执行。poll阶段的流程如下所示:
事件循环机制 Event Loop - 图3

Check

check 阶段会检测setImmediate()回调函数并在这个阶段进行执行。

close callbacks(不作为关注重点)

这个阶段执行一些列关闭的回调函数socket.on('close', callbacks)

Promise()的回调(微任务)

类似的,事件循环还存在一个微任务队列,微任务队列执行时机是:当前调用栈产生了微任务,微任务进入微任务队列,当调用栈清空后,且Process.nextTick()队列为空,则立马清空微任务队列。

process.nextTick()

Process.nextTick()的执行时机即是在同步任务执行完毕后,立即Process.nextTick()队列中的回调函数依次推入调用栈中进行执行。
换句话说,所谓的Process.nextTick()简单来说就是表示当前调用栈清空后立即执行的逻辑,其实你完全可以将它理解成一个 micro (虽然官方并不将它认为是 EventLoop 中的一部分)。

案例学习

案例1 setTimeout 和 nextTick

  1. function tick() {
  2. console.log('tick');
  3. }
  4. function timer() {
  5. console.log('timer');
  6. }
  7. setTimeout(() => {
  8. timer();
  9. }, 0);
  10. process.nextTick(() => {
  11. tick();
  12. });
  13. // 输出: tick timer

上述代码的调用过程其实非常简单,当代码依次执行时遇到 process.nextTick 和 timer 时会分别将他们推入对应的 Queue 中。
当调用栈中的代码执行完毕时,会先检查 nextTick 中存在对应的 tick 函数,那么会拿出 tick 进入调用栈中进行执行。
当 nextTick 中的任务清空时,会进入所谓的 timers 阶段依次将Timer队列中任务推入调用栈中。直至Timer队列可执行的回调清空。

案例2 setTimeout 和 setImmediate

  1. function timer() {
  2. console.log('timer');
  3. }
  4. function immediate() {
  5. console.log('immediate');
  6. }
  7. setTimeout(() => {
  8. timer();
  9. }, 0);
  10. setImmediate(() => {
  11. immediate();
  12. });
  13. /**
  14. 输出:
  15. timer 或者 immediate
  16. immediate timer
  17. */

分析执行过程:

  1. 同步代码执行时,这段代码会在 timer 以及 check 阶段的队列中分别推入对应的 timer 函数和 immediate 函数。此时代码跑完即调用栈为空。
  2. 检查Process.nextTick()回调队列,发现是空。
  3. 进入事件循环
  4. 进入Timers阶段,发现回调函数有可能到时间了。为什么说有可能呢?因为ndoejs中setTimout(callbacks, delay)delay在0~2147483647 之外时,会将delay设置为1。因此,不确定进入Timer阶段回调函数是否到时间。
  5. 进入check阶段,发现队列不为空,执行其回调函数。

    案例3 如何保证 setImmediate 一定早于 setTimeout 执行回调函数

    ```javascript const fs = require(‘fs’); const path = require(‘path’);

fs.readFile(‘./a.js’, (err) => { if (err) { console.log(err, ‘err’); }

setTimeout(() => { console.log(‘timer’); });

setImmediate(() => { console.log(‘immediate’); }); }); // 输出: // immediate // timer

  1. 分析:<br />在 poll 阶段会执行该 fs.readFile 的回调函数,回调中存在 setImmediate 那么EventLoop的下一个阶段一定会进入check阶段 。而setTimeout就得到下一轮事件循环才能执行了。
  2. <a name="KIyhI"></a>
  3. #### 案例4 理解宏任务 微任务 执行过程
  4. ```javascript
  5. setImmediate(() => {
  6. console.log('immediate1 开始')
  7. Promise.resolve().then(() => console.log('immediate' + 1, '微任务执行'));
  8. Promise.resolve().then(() => console.log('immediate' + 2, '微任务执行'));
  9. console.log('immediate1 结束');
  10. });
  11. setImmediate(() => {
  12. console.log('immediate2 开始');
  13. Promise.resolve().then(() => console.log('immediate' + 3, '微任务执行'));
  14. Promise.resolve().then(() => console.log('immediate' + 4, '微任务执行'));
  15. console.log('immediate2 结束');
  16. });
  17. /**
  18. 实际输出:
  19. immediate1 开始
  20. immediate1 结束
  21. immediate1 微任务执行
  22. immediate2 微任务执行
  23. immediate2 开始
  24. immediate2 结束
  25. immediate3 微任务执行
  26. immediate4 微任务执行
  27. 原本预想的输出:
  28. immediate1 开始
  29. immediate1 结束
  30. immediate2 开始
  31. immediate2 结束
  32. immediate1 微任务执行
  33. immediate2 微任务执行
  34. immediate3 微任务执行
  35. immediate4 微任务执行
  36. */

正确的步骤:

  1. 执行同步代码,发现两个setTimeout,将其回调函数放到Timer阶段中。调用栈清空
  2. 检查Process.nextTick()回调队列,发现是空。
  3. 进入事件循环Timer阶段队列有两个任务。第一个回调函数出队进入调用栈中执行。
  4. 执行打印immediate1 开始遇到两个promise回调函数,加入到微任务队列中。之后打印immediate1 结束当前调用栈清空。
  5. 微任务队列不为空,有两个任务。立马依次送入调用栈。依次打印immediate1 微任务执行immediate2 微任务执行。调用栈再次为空。
  6. TImer阶段队列第二个回调函数进入调用栈执行。
  7. 后续步骤同上。

参考链接

关于Node.js EventLoop的poll阶段该如何理解?
Node.js event loop workflow & lifecycle in low level