来自事件循环的回调会启动一个全新的、空的调用栈。在此之前,它所在的任何调用栈都已经完成并将控制权返回到事件循环。
回调将以词法环境记录开始,该记录使得回调能够访问适当的词法环境(在定义回调时处于范围内的局部变量等),并在执行回调之前重新附加到回调上。
执行回调将启动一个新的调用栈,支持回调本身调用其他函数,并使得回调返回时,控制权将返回到事件循环。
值得注意的是,调用栈只是函数返回时返回地址的记录,因为调用栈可以建立并解除,比如 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");
这段代码的事件顺序如下:
setTimeout()
函数被调用并在5秒后调用回调函数。
- 当JS解释器回到事件循环时,它看到计时器已经准备好触发。它会为该超时设置适当的词法记录,其中包含
greeting1
,因此当该回调运行时,它将能够访问greeting1
变量。事件循环创建一个新的调用堆栈,将自己的返回地址放入其中,并调用定时器回调函数。
- 定时器回调运行,定义
greeting2
,输出console.log("A")
并运行fs.readFile()
。
fs.readFile()
启动异步操作以读取该文件,然后返回。作为这个过程的一部分,它注册一个完成回调函数,等待调用。
- 然后执行
console.log("C")
。接着,定时器回调函数返回到事件循环中。
- 稍后,
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. 该回调返回,控制立即返回到事件循环,其中寻找下一个要运行的事件。