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


// index.jsconsole.log("First");console.log("Second");console.log("Third");以下是 Node 运行时执行同步代码的可视化展示 。

理解 Node.js 中的事件循环

文章插图
图片
理解 Node.js 中的事件循环

文章插图
图片
执行的主线程始终从全局作用域开始 。全局函数(如果我们可以这样称呼它)被推入堆栈中 。然后,在第 1 行,我们有一个控制台日志语句 。这个函数被推入堆栈中 。假设这个发生在 1 毫秒时,“First” 被记录在控制台上 。然后,这个函数从堆栈中弹出 。
执行到第 2 行时 。假设到第 2 毫秒了,log 函数再次被推入堆栈中 。“Second”被记录在控制台上,并弹出该函数 。
最后 , 执行到第 3 行了 。第 3 毫秒时,log 函数被推入堆栈,“Third”将记录在控制台上,并弹出该函数 。此时已经没有代码要执行,全局也被弹出 。
异步代码执行接下来,让我们看一下异步代码执行 。有以下代码片段:包含三个日志语句,但这次第二个日志语句传递给了fs.readFile() 作为回调函数 。
理解 Node.js 中的事件循环

文章插图
图片
理解 Node.js 中的事件循环

文章插图
执行的主线程始终从全局作用域开始 。全局函数被推入堆栈 。然后执行到第 1 行,在第 1 毫秒时,“First”被记录在控制台中 , 并弹出该函数 。然后执行移动到第 2 行,在第 2毫秒时,readFile 方法被推入堆栈 。由于 readFile 是异步操作 , 因此它会转移(off-loaded)到 libuv 。
JavaScript 从调用堆栈中弹出了 readFile 方法,因为就第 2 行的执行而言,它的工作已经完成了 。在后台 , libuv 开始在单独的线程上读取文件内容 。在第 3 毫秒时 , JavaScript 继续进行到第 5 行,将 log 函数推入堆栈,“Third”被记录到控制台中,并将该函数弹出堆栈 。
大约在第 4 毫秒左右 , 假设文件读取任务已经完成 , 则相关回调函数现在会在调用栈上执行,在回调函数内部遇到 log 函数 。
log 函数推入到到调用栈,“Second”被记录到控制台并弹出 log 函数。由于回调函数中没有更多要执行的语句,因此也被弹出。没有更多代码可运行了  , 所以全局函数也从堆栈中删除。
控制台输出“First”,“Third”,然后是“Second” 。
Libuv 和异步操作很明显 , libuv 用于处理 Node.js 中的异步操作 。对于像处理网络请求这样的异步操作 , libuv 依赖于操作系统原生机制 。对于没有本地 OS 支持的异步读取文件的操作,libuv 则依赖其线程池以确保主线程不被阻塞 。然而 , 这也引发了一些问题 。
  • 当一个异步任务在 libuv 中完成时,什么时候 Node 会在调用栈上运行相关联的回调函数?
  • Node 是否会等待调用栈为空后再运行回调函数?还是打断正常执行流来运行回调函数?
  • 像 setTimeout 和 setInterval 这类延迟执行回调函数的方法又是何时执行回调函数呢?
  • 如果 setTimeout 和 readFile 这类异步任务同时完成,Node 如何决定哪个回调函数先在调用栈上运行?其中一个会有更多的优先级吗?
所有这些问题都可以通过理解 libuv 核心部分——事件循环来得到答案 。
什么是事件循环?从技术上讲,事件循环只是一个 C 语言程序 。但是在 Node.js 中,你可以将其视为一种设计模式,用于协调同步和异步代码的执行 。
可视化事件循环事件循环是一个循环 , 只要你的 Node.js 应用程序在运行,它就一直运行 。每个循环中有六个不同的队列,每个队列都包含一个或多个需要最终在调用堆栈上执行的回调函数 。
理解 Node.js 中的事件循环

文章插图
图片