Event Loop 我感觉我又行了(Node.js篇)

前端开发
2022年07月09日
0

我们现在只讨论 Node.js v11+ 版本。

相对于浏览器的 Event Loop 来说,在 Node.js 中的 Event Loop 反而更加简洁。

The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.

Node.js 的事件循环,它会把一些操作放到其他相关的线程来处理,当处理完毕之后,会通知主线程,由主线程决定什么时候来执行。

也就是说:

  1. Node.js 是通过事件循环机制来运行 JS 代码的;
  2. 提供了线程池处理 I/O 操作任务;
  3. 两种线程:
    1. 事件循环线程:负责安排任务(require、同步执行回调、注册新任务);
    2. 线程池(libuv实现),负责处理任务(I/O操作、CPU密集型任务)。

image-20220531105648481

事件循环阶段 phase

  1. Timers:setTimeout / setInterval;
  2. Pending callbacks:执行延迟到下一个事件环迭代的 I/O 回调(内部机制使用);
  3. Idle,prepare:系统内部机制使用;
  4. Poll:轮循,检查新的 I/O 事件;执行 I/O 的回调;(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  5. Check:setImmediate;
  6. Close callbacks:关闭回调函数(内部机制使用)。

在每次运行的事件循环之间,Node.js 检查它是否在等待任何异步 I/O 或计时器,如果没有的话,则完全关闭。

举个粟子

js
// promise1.then Promise.resolve().then(() => { console.log(1) }) // nextTick1 process.nextTick(() => { console.log(2) }) console.log('start') // readFile readFile('1.txt', 'utf-8', () => { // setTimeout1 setTimeout(() => { console.log(3) }, 0) // nextTick2 process.nextTick(() => { console.log(4) }) // setImmediate1 setImmediate(() => { console.log(5) }) console.log(6) }) console.log(7) // setTimeout2 setTimeout(() => { console.log(8) }, 0) // setImmediate2 setImmediate(() => { console.log(9) }) console.log('end')
  1. 代码执行;

    image-20220531140615577

  2. promise1.then 的回调放入微任务队列;

    image-20220531140752299

  3. nextTick1 的回调放入微任务队列(这里稍候会有解释);

    image-20220531140914677

  4. console.log('start') 执行,输出 start;

    image-20220531141202515

  5. readFile 放入 Poll;

    image-20220531141233407

  6. console.log(7) 执行,输出 7;

    image-20220531141314851

  7. setTimeout2 放入 Timers;

    image-20220531141401936

  8. setImmediate2 放入 Check;

    image-20220531141455247

  9. console.log('end') 执行,输出 end

    image-20220531141616356

  10. 主执行栈清空,准备进入 Timers 阶段,先执行 nextTick1 的回调,输出 2;

    image-20220531141707482

  11. nextTick1 的回调执行完毕,清空微任务 promise1.then,输出 1;

    image-20220531141743608

  12. 进入 Timers 阶段,setTimeout2 执行,setTimeout2 的回调放入回调队列;

    image-20220531141853157

  13. 进入 Poll 阶段;

    1. 注意这个阶段它有短暂的等待时间,如果在等待时间内 Timers 到时,会先回去执行 Timers setTimeout2 的回调,输出 8;然后再回到 Poll,再进行 Check 执行 setImmediate2 的回调,输出 9;

      image-20220531142229084

    2. 它也有可能在等待时间结束后,Timers 还没到时,会先进入 Check 阶段,执行 setImmediate2 的回调,输出 9;然后回去执行 setTimeout2 的回调,输出 8;

      image-20220531142320872

  14. 下一轮没有了微任务, Timers,为空,直接进入 Poll,readFile 的回调执行;setTimeout1 放入 Timers;nextTick2 的回调放入微任务队列setImmediate1 放入 Check;console.log(6) 执行,输出 6;

    image-20220531142526486

  15. 主执行栈执行完毕,nextTick2 的回调执行,输出4

    image-20220531142756257

  16. Poll 阶段完毕,注意,在 I/O 操作里面,会直接进入 Check 阶段,setImmediate1 执行,输出 5;setTimeout1 执行,输出 3;

    image-20220531142930882

  17. 完事。

所以这个例子的执行结果有两种:start 7 end 2 1 9 8 6 4 5 3 或者 start 7 end 2 1 8 9 6 4 5 3

再举个粟子

js
console.log('start') readFile('1.txt', 'utf-8', () => { setTimeout(() => { // nextTick1 process.nextTick(() => { console.log('nextTick1') }) console.log('setTimeout') }) Promise.resolve().then(() => { // nextTick2 process.nextTick(() => { console.log('nextTick2') }) console.log('promise') }) setImmediate(() => { // nextTick3 process.nextTick(() => { console.log('nextTick3') }) console.log('setImmediate') }) // nextTick4 process.nextTick(() => { console.log('nextTick4') }) console.log('readFile') }) console.log('end')

我们看一下这个例子:

  1. start 和 end 先输出,这应该没什么问题;

    <主执行栈>:

    start

    end

    <微任务>:

    readFile

    <回调队列>:

  2. readFile 回调执行,setTimeout 放入 Timers;promise.then 的回调放入微任务队列;setImmediate 放入 Check;nextTick4 的回调挂起;console.log('readFile') 执行,输出 readFile;

    <主执行栈>:

    start

    end

    readFile

    nextTick4 的回调

    <微任务>:

    promise.then 的回调

    setTimeout

    readFile

    setImmediate

    <回调队列>:

  3. nextTick4 回调执行,输出 nextTick4;

    <主执行栈>:

    start

    end

    readFile

    nextTick4

    nextTick4 的回调

    <微任务>:

    promise.then 的回调

    setTimeout

    readFile

    setImmediate

    <回调队列>:

  4. 清空微任务,promise.then 回调执行,nextTick2 的回调挂起,console.log('promise') 执行,输出 promise;

    <主执行栈>:

    start

    end

    readFile

    nextTick4

    promise

    <nextTick 的回调>:

    nextTick4 的回调

    nextTick2 的回调

    <微任务>:

    promise.then 的回调

    setTimeout

    readFile

    setImmediate

    <回调队列>:

  5. nextTick2 回调执行,输出 nextTick2;

    <主执行栈>:

    start

    end

    readFile

    nextTick4

    promise

    nextTick2

    nextTick4 的回调

    nextTick2 的回调

    <微任务>:

    promise.then 的回调

    setTimeout

    readFile

    setImmediate

    <回调队列>:

  6. Timer 阶段,setTimeout 执行,回调挂起;

    <主执行栈>:

    start

    end

    readFile

    nextTick4

    promise

    nextTick2

    nextTick4 的回调

    nextTick2 的回调

    <微任务>:

    promise.then 的回调

    setTimeout

    readFile

    setImmediate

    <回调队列>:

    setTimeout 的回调

  7. Poll 阶段,去检查 Check,setImmediate 回调执行,nextTick3 的回调挂起;console.log('setImmediate') ,输出 setImmediate;

    <主执行栈>:

    start

    end

    readFile

    nextTick4

    promise

    nextTick2

    setImmediate

    nextTick4 的回调

    nextTick2 的回调

    nextTick3 的回调

    <微任务>:

    promise.then 的回调

    setTimeout

    readFile

    setImmediate

    <回调队列>:

    setTimeout 的回调

  8. nextTick3 的回调执行,输出 nextTick3;

    <主执行栈>:

    start

    end

    readFile

    nextTick4

    promise

    nextTick2

    setImmediate

    nextTick3

    nextTick4 的回调

    nextTick2 的回调

    nextTick3 的回调

    <微任务>:

    promise.then 的回调

    setTimeout

    readFile

    setImmediate

    <回调队列>:

    setTimeout 的回调

  9. setTimeout 的回调执行,nextTick1 的回调挂起;console.log('setTimeout') 执行,输出 setTimeout;

    <主执行栈>:

    start

    end

    readFile

    nextTick4

    promise

    nextTick2

    setImmediate

    nextTick3

    setTimeout

    nextTick4 的回调

    nextTick2 的回调

    nextTick3 的回调

    nextTick1 的回调

    <微任务>:

    promise.then 的回调

    setTimeout

    readFile

    setImmediate

    <回调队列>:

    setTimeout 的回调

  10. nextTick1 的回调执行,输出 nextTick1;

    <主执行栈>:

    start

    end

    readFile

    nextTick4

    promise

    nextTick2

    setImmediate

    nextTick3

    setTimeout

    nextTick1

    nextTick4 的回调

    nextTick2 的回调

    nextTick3 的回调

    nextTick1 的回调

    <微任务>:

    promise.then 的回调

    setTimeout

    readFile

    setImmediate

    <回调队列>:

    setTimeout 的回调

  11. 完事。

理解 process.nextTick()

需要注意的是,在 Node.js 的文档中指出,process.nextTick() 从技术上来讲,它不属于事件循环的一部分。

它会在当前阶段的操作完成之后处理 nextTick队列

任何在给定阶段中调用的 process.nextTick(),它的回调都会在事件循环继续之前执行。可以这样理解,它在当前阶段的所有同步代码执行完毕之后,立即执行。

就如同我们第2个例子,每个 process.nextTick() 的回调都会在执行完当前阶段的代码,进入下个阶段之前被执行。

我们再分析一下下面的例子:

js
console.log(1) // nextTick1 process.nextTick(() => { console.log(2) // nextTick2 process.nextTick(() => { console.log(3) }) }) new Promise((resolve, reject) => { // nextTick3 process.nextTick(() => { console.log(4) // nextTick4 process.nextTick(() => { console.log(5) }) }) console.log(6) resolve() }).then(() => { console.log(7) })
  1. 代码执行,console.log(1) 执行,输出1;
  2. nextTick1 放入 nextTick队列
  3. Promise 的 Executor 执行,nextTick3 放入 nextTick队列console.log(6) 执行,输出6;Promise 状态变更,promise.then 的回调放入微任务队列;
  4. 主执行栈完毕,nextTick1 的回调执行,console.log(2) 执行,输出 2;nextTick2 放入 nextTick队列
  5. nextTick3 的回调执行,console.log(4) 执行,输出 4;nextTick4 放入 nextTick队列
  6. nextTick2 的回调执行,console.log(3) 执行,输出 3;
  7. nextTick4 的回调执行,console.log(5) 执行,输出 5;
  8. 清空微任务,console.log(7) 执行,输出 7;
  9. 完事。

以上,Node.js 的事件环相对来讲还是比较简单的,可能理解起来会比较绕。