如果我理解正确的话,EventLoop是node用来解析异步操作然后将它们传递给调用堆栈的机制,对吗?我的问题是,当我使用同步方法(例如 pbkdf2Sync)时,它会卡在调用堆栈中直到完成,但不会被移动到 EventLoop,因为不是异步操作,那么为什么这被称为如果实际上它阻止了一切,那么阻止事件循环?不仅仅是事件循环(据我所知,可以继续工作,并在完成时将回调传递给调用堆栈)
我对 NodeJs 内部工作原理的理解完全错误吗?这个主题具体来说有点难以理解,因为我阅读的每个资源都以某种方式有所不同,所以即使我认为我了解了更大的情况,这些都是让我困惑的“细节”。
什么是阻塞
为什么在nodejs中使用同步函数被称为“阻塞事件循环”?
简而言之,这是因为事件循环只能在您从当前 Javascript 正在执行的任何操作返回时处理下一个事件,并允许事件循环查找下一个事件。同步函数会阻塞解释器直到完成。因此,在同步函数工作的整个过程中,当您等待它返回时,解释器会被阻塞,并且控制不会返回到事件循环。这会阻止事件循环并阻止 JavaScript 运行。
单线程
Nodejs 使用单个线程运行您的 Javascript。其他线程在内部使用,但您的 Javascript 本身仅在单个线程中运行(我们假设您的代码中没有使用 WorkerThreads)。因此,当您进行同步函数调用时,运行 Javascript 的单个线程会繁忙并被阻塞,直到同步函数调用返回,然后才能继续执行更多 Javascript。
这会阻止一切。它会阻止在同步函数调用后运行更多 Javascript,并阻止返回事件循环以运行任何其他待处理的事件处理程序,例如传入网络事件、计时器、来自其他事物(例如磁盘 I/O)的完成事件、等等...因此,虽然这会阻止事件循环,但它也会阻止在函数调用后运行更多您自己的代码。
非阻塞、异步操作
另一方面,诸如 之fs.readFile()类的异步函数不会阻塞。他们开始行动并立即返回。这允许解释器在调用后继续运行您自己的 Javascript fs.readFile(),并且还允许您从首先触发您的工作的任何事件返回,这会将控制返回到事件循环,以便它可以为其他等待事件提供服务或将来会触发的其他事件。 fs.readFile()然后在运行 Javascript 的主线程之外以本机代码(在幕后)完成大部分工作。因此,这些类型的异步函数不会阻塞事件循环 - 相反,它们与事件循环配合,以便在等待先前启动的异步操作完成时可以运行其他事物。当它们完成时,它们将一个事件插入到事件循环中,导致事件循环在最早方便时(当它未被阻止时)调用完成回调。
阻止的差异
还值得注意的是,表示同步和异步操作的函数都会阻止 Javascript 的执行并阻止事件循环,直到它们返回。区别在于,异步操作几乎立即从函数返回,远早于异步操作本身完成,并通过 Promise、回调或事件(这些都是处于最低级别的回调)传达其完成和/或最终结果。事件循环)。同步操作在操作本身完成之前不会返回。因此,异步操作仅在启动操作时阻塞很短的持续时间,而同步操作则在操作的整个持续时间内阻塞(直到操作完成)。
有关事件循环的更多信息
那么,我的 javascript 代码在事件循环的哪个时刻执行?
当控制返回到事件循环时,它会经历几个不同的阶段来寻找要做的事情。当它找到要做的事情时,该“某事”会调用 Javascript 回调,开始运行一些 Javascript。例如,如果“要做的事情”是一个setTimeout()准备触发的计时器,那么它将调用传递给setTimeout(). 该回调运行至完成,只有当您的 Javascript 从该回调返回时,事件循环才会重新获得控制权并开始查找下一个要运行的事件并调用其回调。
它不会被移动到 EventLoop,因为不是异步操作
这并不是思考事物的正确方式。事情并没有真正“转移到事件循环”。
同步操作只是一个阻塞函数调用,它在返回时返回,并且任何其他 Javascript 的执行都会被阻塞,直到该阻塞函数调用返回。事情被阻塞是因为运行 Javascript 的单线程解释器被卡住等待该函数完成。它不能做任何其他事情,并且事件循环也被阻止,因为在解释器将控制权返回到事件循环之前它不能做任何事情。
另一方面,异步操作会启动某些操作(假设它向其他某个主机发出 http 请求),然后立即返回,远远早于它获得该 http 请求的结果。由于此异步操作在获得结果之前返回,因此它被认为是非阻塞的,并且因为它返回得很快,因此您可以从导致代码运行的任何事件返回,然后将控制权返回到事件循环。这允许事件循环查找其他事件来处理并运行其相应的回调。同时,先前启动的异步操作有一些与其关联的本机代码(可能会也可能不会在本机代码操作系统线程中运行 - 取决于它是什么类型的操作)。但无论如何,本机代码的配置使得当异步操作完成时,它将向适当的事件队列插入一个事件。因此,在未来的某个时刻,当 NodeJS 重新控制事件循环时,它将发现该事件并运行与该事件关联的 JavaScript 回调,从而通知原始 JavaScript 代码异步操作现已完成并提供某种结果或错误代码。
例子
举一个简单的例子,假设您运行以下代码:
// timer that wants to fire in 1 second
setTimeout(function() {
console.log("timer fired")
}, 1000);
// loop that blocks for 5 seconds
const start = Date.now();
while (Date.now() - start < 5000) { }
console.log("blocking loop finished");Run Code Online (Sandbox Code Playgroud)
这将输出:
blocking loop finished
timer fired
Run Code Online (Sandbox Code Playgroud)
尽管计时器设置为从现在开始运行 1 秒,但循环while()会阻塞所有内容 5 秒,因此直到循环while()完成并返回到系统后,事件循环才能查看下一步必须执行的操作并调用与计时器关联的回调。
该while循环类似于阻塞函数,例如pbkdf2Sync(). 两者都会阻塞解释器,并且在完成之前不会返回,因此事件循环中的任何内容都没有机会运行,直到它们完成为止。
一个简单的类比
这是一个简单的类比。想象一下,您需要联系有线电视公司来解决问题。您可以通过两种方式获取它们。第一种方法是您致电他们并等待一个小时,等待客户服务代表接听您的电话。当您处于等待状态时,您实际上无法做太多其他事情,因为当有人最终拨打电话帮助您时,您必须在那里等待并准备好做出回应。因此,您被“阻止”做很多其他事情。 这是一个“阻塞、同步操作”。 在等待期间,您实际上无法做太多其他事情。
您可以致电有线电视公司的第二种方法是在将来的某个时候请求回电,当有代表时,他们会给您回电。一旦您完成回电请求,您就可以继续处理其他事情。您必须能够在来电到达时接听来电,但除此之外,您不会被阻止做许多其他事情。 这是一个“非阻塞、异步操作”。
但是,如果您在上述第二种情况下应该能够接听电话回电,但您又拨打了一个电话,并且通话时间长达 30 分钟,那么有线电视公司在这段时间内将无法通过电话与您联系。你的另一个电话。在我们的类比中,您在其他呼叫期间“阻止事件循环”,因为在您通话时无法处理来自有线电视公司的传入事件。