Node.js 中的事件循环是自己运行回调函数还是将回调函数传递给调用栈以便调用栈执行回调函数?

3
我正在学习Node.js,了解了事件循环和如何在其中处理Node.js的异步任务。就我现在所理解的(可能是错误的),例如当我们使用异步`fs.readFile()`时,`readFile`将被从主线程中移除并传递给操作系统内核(使内核处理`readfile`,而JS引擎可以继续读取main thread的其余代码)。然后,一旦`kernel`完成了`readFile`,就会发出事件,该事件将被事件循环接收。因此,问题是事件在被事件循环接受后,事件循环是否会自己执行回调函数,还是将回调函数从队列中取出并将其传递到调用堆栈中以便调用堆栈执行该回调函数?

观看这个视频https://www.youtube.com/watch?v=8aGhZQkoFbQ,他详细地解释了事件循环。 - chandan_kr_jha
@chandan_kr_jha,当然我看过了,他说事件循环作为一个中间人,从队列中获取回调并将其放入调用堆栈中。好的,但是一些关于Node.js中事件循环的文章说事件循环本身运行回调,这让我感到困惑)) - SAM
这将消除你的疑虑。https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/ - chandan_kr_jha
1个回答

3
来自事件循环的回调会启动一个全新的、空的调用栈。在此之前,它所在的任何调用栈都已经完成并将控制权返回到事件循环。
回调将以词法环境记录开始,该记录使得回调能够访问适当的词法环境(在定义回调时处于范围内的局部变量等),并在执行回调之前重新附加到回调上。
执行回调将启动一个新的调用栈,支持回调本身调用其他函数,并使得回调返回时,控制权将返回到事件循环。
值得注意的是,调用栈只是函数返回时返回地址的记录,因为调用栈可以建立并解除,比如 a() 调用 b(),然后调用 c(),再返回并回到 b(),最后返回并回到 a()。这就是“栈”这个词的来源,调用栈只是存储所有这些东西的机制。
由于从事件循环调用的回调在完成后立即返回到事件循环,所以在启动回调时调用栈上唯一的东西就是返回到事件循环的返回地址。所以,当回调返回时,控制权回到事件循环。

那么问题来了:在事件被事件循环接受后,事件循环会自行执行回调函数吗?还是事件循环会将回调从队列中取出,并按顺序将其传递到调用栈中以便调用栈执行该回调函数?

这听起来可能是术语混淆。调用栈只是一组存储的返回地址。JS 解释器使用调用栈来记住返回地址,然后在函数返回时跳回这些返回地址。真正运行的是解释器,而不是调用栈。调用栈只是数据。
所以,假设您有以下代码:
let greeting1 = "Hello";

console.log("AA");

setTimeout(() => {
    let greeting2 = "GoodBye";
    console.log("A");

    fs.readfile('./myfile.txt', ()  => {
        console.log("B", greeting1)
    });

    console.log("C");
}, 5000);

console.log("BB");

这段代码的事件顺序如下:
  1. setTimeout()函数被调用并在5秒后调用回调函数。
  2. 当JS解释器回到事件循环时,它看到计时器已经准备好触发。它会为该超时设置适当的词法记录,其中包含greeting1,因此当该回调运行时,它将能够访问greeting1变量。事件循环创建一个新的调用堆栈,将自己的返回地址放入其中,并调用定时器回调函数。
  3. 定时器回调运行,定义greeting2,输出console.log("A")并运行fs.readFile()
  4. fs.readFile()启动异步操作以读取该文件,然后返回。作为这个过程的一部分,它注册一个完成回调函数,等待调用。
  5. 然后执行console.log("C")。接着,定时器回调函数返回到事件循环中。
  6. 稍后,fs.readFile()操作(实际上由多个单独的异步操作组成,但在这里简化为一个)完成,当文件最终关闭时触发回调函数(在fs模块中内部调用)。 事件循环类似地创建一个新的调用堆栈,将自己的返回地址放入其中,附加适当的词法记录,并调用内部fs模块的回调函数。 当这个回调函数执行时,它会调用你的回调函数,然后你会看到来自console.log("B", greeting)的输出。
控制台输出应如下所示:
AA
BB
A
C
B, Hello

抱歉,再有几个问题。首先,真的有OS内核处理异步readFile()操作,一旦读取完成,就将发出的事件传递给事件循环吗?其次,当事件循环接收到关于readFile()完成的发出事件后,仅仅调用回调函数。但是,如您所见,所有的难度都是由OS内核完成的,而事件循环只执行简单的部分,即回调。所以,如果回调被阻塞,即使回调也可以被阻塞,因此线程池也会被用来处理对事件循环难以处理的回调。

让我们暂时不讨论readFile(),因为这是一个多阶段操作,包括在node.js中的JavaScript、本地C++代码和OS库调用。所以,那有点复杂。我们选择类似但更简单的单一操作,比如fs.stat()的代码:

fs.stat('./myfile.txt', (stats) => {
    console.log(stats);
});
console.log("A");

以下是步骤:
1. 调用fs.stat()。 2. 这将进入node.js JavaScript代码以进行stat()调用。该调用设置了一些上下文,允许JavaScript数据传递到C++代码,然后调用C++函数(仍为nodejs特定代码,但现在是C++代码)进行stat()操作。 3. C++函数然后准备进行操作系统调用以获取特定文件的统计信息。但是,它不会在当前线程中执行该调用,这会阻塞JavaScript引擎的运行,而是使用本机C++线程(从本机代码线程池中获得),并告诉该线程去运行OS调用。 4. 线程调用操作系统以获取特定文件的统计信息。 5. 与此同时,在收到原始stat调用的第一个C++函数中,在它启动线程以执行OS调用后,它就返回到JavaScript。JavaScript stat()操作从C++返回,并返回到您的JavaScript,继续执行其他JavaScript。 6. 然后在上面的代码中遇到console.log("A"),并将其输出到控制台。 7. 同时,操作系统正在努力获取文件的统计信息。当它得到结果时,它将该结果返回给来自线程池的线程。该线程然后向nodejs事件循环发布事件。该事件不仅包含stat的结果,还包括完成回调在您的JavaScript代码中所需的上下文。 8. 当事件循环空闲并且此事件获得其机会时,事件循环会创建正确的JavaScript上下文(最初与原始fs.stat()调用一起传递),设置它并调用与该完成事件相关联的JavaScript回调。 9. 该回调运行并执行console.log(stats)。 10. 该回调返回,控制立即返回到事件循环,其中寻找下一个要运行的事件。

嘿,老兄 :) 我向你脱帽致敬。抱歉,有些问题需要再次确认,首先,是否真的存在处理异步readFile()并在完成readFile后将发出的事件传递给事件循环的OS内核?其次,所以当事件循环收到关于readFile()完成的发出事件时,事件循环只是调用回调函数。但正如您所看到的,所有的难事都由OS内核完成,而事件循环只执行回调函数这个简单的部分。因此,即使回调函数可能会阻塞,因此如果回调函数对事件循环来说太难了,则使用线程池。 - SAM
请帮帮我,我真的需要你的帮助。 - SAM
1
@SAM - 看看我在答案末尾添加了什么。 - jfriend00

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接