理解 Node.js 中的事件循环( 三 )

  • 第三个是检查队列(check queue),它保存与 setImmediate 函数相关的回调函数,这是特定于Node 的功能 。
  • 第四个是关闭队列(close queue),它保存与异步任务关闭事件相关联的回调函数 。
  • 最后,有两个不同队列组成微任务队列(microtask queue) 。
    • nextTick 队列保存了与 process.nextTick 函数关联的回调函数 。
    • Promise 队列则保存了JavaScript 中本地 Promise 相关联的回调函数 。
    需要注意的是计时器、I/O、检查和关闭队列都属于 libuv 。然而,两个微任务队列并不属于 libuv 。尽管如此,它们仍然是 Node 运行时环境中扮演着重要角色,并且在执行回调顺序方面发挥着重要作用 。说到这里, 让我们来理解一下事件循环是如何工作的 。
    事件循环是如何工作的?图中箭头是一个提示,但可能还不太容易理解 。让我来解释一下队列的优先级顺序 。首先要知道,所有用户编写的同步 JavaScript 代码都比异步代码优先级更高 。这表示只有在调用堆栈为空时,事件循环才会发挥作用 。
    在事件循环中 , 执行顺序遵循某些规则 。需要掌握的规则还是有一些的,我们逐个的了解一下:
    1. 执行微任务队列(microtask queue)中的所有回调函数 。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务 。
    2. 执行计时器队列(timer queue)内的所有回调函数 。
    3. 如果微任务队列中存在回调函数,则在计时器队列内每执行完一次回调函数之后执行微任务队列中的所有回调函数 。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务 。
    4. 执行 I/O 队列(I/O queue)内的所有回调函数 。
    5. 如果微任务队列中存在回调函数,按照先 nextTick 队列后 Promise 队列的顺序依次执行微任务队列中的所有回调函数 。
    6. 执行检查队列(check queue)内的所有回调函数 。
    7. 如果微任务队列中存在回调函数 , 则在检查队列内每个回调之后执行微任务队列中的所有回调函数  。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务 。
    8. 执行关闭队列(close queue)内的所有回调函数 。
    9. 在同一循环的最后,再执行一次微任务队列 。首先是 nextTick 队列中的任务,然后是 Promise 队列中的任务 。
    此时,如果还有更多的回调需要处理,那么事件循环再运行一次(译注:事件循环在程序运行期间一直在运行 , 在当前没有可供处理的任务情况下,会处于等待状态,一旦有新任务就会执行),并重复相同的步骤 。另一方面,如果所有回调都已执行并且没有更多代码要处理(译注:也就是程序执行结束),则事件循环退出 。
    这就是 libuv 事件循环在 Node.js 中执行异步代码的作用 。有了这些规则,我们可以重新审视之前提出的问题 。
    当一个异步任务在 libuv 中完成时,什么时候 Node 会在调用栈上运行相关联的回调函数?
    答案:只有当调用栈为空时才执行回调函数 。
    Node 是否会等待调用栈为空后再运行回调函数?还是打断正常执行流来运行回调函数?
    答案:运行回调函数时不会打断正常执行流 。
    像 setTimeout 和 setInterval 这类延迟执行回调函数的方法又是何时执行回调函数呢?
    答案:*setTimeout 和 setInterval 的所有回调函数中第一优先级执行的(不考虑微任务队列) 。*
    如果两个异步任务(例如 setTimeout 和 readFile)同时完成 , Node 如何决定那个回调函数先在调用栈中执行?其中一个会比另一个有更高优先权吗?
    答案:在同时完成的情况下,计时器回调会先于 I/O 回调执行 。
    到此为止我们学了很多,但我希望大家可以把下面这张图片展现的执行顺序铭记于心 , 因为它完整的表现了 Node.js 在幕后是如何执行异步代码的 。
    理解 Node.js 中的事件循环

    文章插图
    图片
    结论事件循环是 Node.js 的基本组成部分,通过确保主线程不被阻塞来实现异步编程 。了解事件循环的工作原理可能具有挑战性,但对于构建高效应用程序至关重要 。


    推荐阅读