我正在考虑它,这就是我想出的:
让我们看看下面的代码:
console.clear();
console.log("a");
setTimeout(function(){console.log("b");},1000);
console.log("c");
setTimeout(function(){console.log("d");},0);
一个请求进来,JS引擎开始一步步执行上面的代码。前两个调用是同步调用。但是当涉及到 setTimeout
方法时,它变成了异步执行。但是 JS 立即从它返回并继续执行,这称为 Non-Blocking
或 Async
。它继续在其他方面工作。
本次执行的结果如下:
数据库
所以基本上第二个 setTimeout
先完成,它的回调函数比第一个更早执行,这是有道理的。
我们在这里讨论的是单线程应用程序。 JS 引擎继续执行此操作,除非它完成第一个请求,否则它不会转到第二个请求。但好处是它不会等待像 setTimeout
这样的阻塞操作解决,因此它会更快,因为它接受新的传入请求。
但我的问题围绕以下项目出现:
#1:如果我们谈论的是单线程应用程序,那么在 JS 引擎接受更多请求并执行它们的同时,什么机制会处理 setTimeouts
?单线程如何继续处理其他请求?什么在 setTimeout
上有效,而其他请求不断进入并被执行。
#2:如果这些 setTimeout
函数在后台执行,同时有更多请求进入并被执行,那么在后台执行异步执行的是什么?我们谈论的这个叫做 EventLoop
的东西是什么?
#3: 但是不应该将整个方法放在 EventLoop
中,以便执行整个事情并调用回调方法吗?这是我在谈论回调函数时所理解的:
function downloadFile(filePath, callback)
{
blah.downloadFile(filePath);
callback();
}
但是在这种情况下,JS 引擎如何知道它是否是一个异步函数,以便它可以将回调放在 EventLoop
中?也许像 C# 中的 async
关键字或某种属性,它指示 JS 引擎将采用的方法是异步方法,应该相应地处理。
#4: 但是 article 的说法与我对事情可能如何运作的猜测完全相反:
事件循环是一个回调函数队列。当异步函数执行时,回调函数被推入队列。 JavaScript 引擎在异步函数执行后的代码之前不会开始处理事件循环。
#5:这里有这张图片可能会有所帮助,但图片中的第一个解释与问题 4 中提到的完全相同:
https://i.stack.imgur.com/zYgcr.png
所以我的问题是对上面列出的项目进行一些澄清?
1:如果我们说的是单线程应用程序,那么当JS引擎接受更多请求并执行它们时,什么处理setTimeouts?那个单线程不会继续处理其他请求吗?那么当其他请求不断出现并被执行时,谁将继续处理 setTimeout。
节点进程中只有 1 个线程将实际执行程序的 JavaScript。然而,在节点本身内部,实际上有几个线程处理事件循环机制的操作,这包括一个 IO 线程池和一些其他线程。关键是这些线程的数量与正在处理的并发连接的数量不对应,就像在每个连接的线程并发模型中那样。
现在关于“执行 setTimeouts”,当您调用 setTimeout
时,所有节点所做的基本上是更新将来要执行的函数的数据结构。它基本上有一堆需要做的事情的队列,并且它选择一个事件循环的每个“滴答”,将其从队列中删除并运行它。
要理解的关键是节点依赖于操作系统来完成大部分繁重的工作。因此,传入的网络请求实际上是由操作系统本身跟踪的,当节点准备好处理一个请求时,它只是使用系统调用向操作系统询问网络请求,其中数据准备好被处理。 IO“工作”节点所做的大部分工作都是“嘿,操作系统,有准备好读取数据的网络连接了吗?”或“嘿操作系统,我的任何未完成的文件系统调用都准备好数据了吗?”。基于其内部算法和事件循环引擎设计,node 将选择 JavaScript 的一个“tick”来执行,运行它,然后重新重复该过程。这就是事件循环的含义。 Node 基本上一直在确定“我应该运行的下一点 JavaScript 是什么?”,然后运行它。这会影响操作系统完成的 IO,以及通过调用 setTimeout
或 process.nextTick
在 JavaScript 中排队的事情。
2:如果这些 setTimeout 将在后台执行,而更多的请求进来和执行,那么在后台执行异步执行的事情就是我们所说的 EventLoop?
没有 JavaScript 在幕后执行。程序中的所有 JavaScript 都在前端和中心运行,一次一个。幕后发生的事情是操作系统处理 IO,节点等待它准备好,节点管理其等待执行的 javascript 队列。
3:JS Engine如何知道它是不是一个异步函数,以便它可以把它放在EventLoop中?
节点核心中有一组固定的异步函数,因为它们进行系统调用,节点知道这些函数是什么,因为它们必须调用 OS 或 C++。基本上所有网络和文件系统 IO 以及子进程交互都是异步的,JavaScript 可以让节点异步运行某些东西的唯一方法是调用节点核心库提供的异步函数之一。即使您使用的是定义了自己的 API 的 npm 包,为了产生事件循环,最终该 npm 包的代码将调用节点核心的异步函数之一,此时节点知道滴答已完成并且它可以启动事件再次循环算法。
4 事件循环是一个回调函数队列。当异步函数执行时,回调函数被推入队列。 JavaScript 引擎在异步函数执行后的代码之前不会开始处理事件循环。
是的,这是真的,但它具有误导性。关键是正常模式是:
//Let's say this code is running in tick 1
fs.readFile("/home/barney/colors.txt", function (error, data) {
//The code inside this callback function will absolutely NOT run in tick 1
//It will run in some tick >= 2
});
//This code will absolutely also run in tick 1
//HOWEVER, typically there's not much else to do here,
//so at some point soon after queueing up some async IO, this tick
//will have nothing useful to do so it will just end because the IO result
//is necessary before anything useful can be done
所以,是的,你可以通过在内存中同步计算斐波那契数来完全阻止事件循环,而且是的,这会完全冻结你的程序。这是合作并发。 JavaScript 的每一次滴答都必须在合理的时间内产生事件循环,否则整个架构就会失败。
不要认为主机进程是单线程的,它们不是。什么是单线程是执行您的 javascript 代码的主机进程的一部分。
除了 background workers,但这些使情况复杂化...
因此,您的所有 js 代码都在同一个线程中运行,并且您不可能让 js 代码的两个不同部分同时运行(因此,您无需管理并发问题)。
正在执行的 js 代码是宿主进程从事件循环中获取的最后一个代码。在您的代码中,您基本上可以做两件事:运行同步指令,并安排将来在某些事件发生时执行的函数。
这是我的示例代码的心理表示(注意:只是,我不知道浏览器的实现细节!):
console.clear(); //exec sync
console.log("a"); //exec sync
setTimeout( //schedule inAWhile to be executed at now +1 s
function inAWhile(){
console.log("b");
},1000);
console.log("c"); //exec sync
setTimeout(
function justNow(){ //schedule justNow to be executed just now
console.log("d");
},0);
当您的代码正在运行时,主机进程中的另一个线程会跟踪正在发生的所有系统事件(点击 UI、读取文件、接收到网络数据包等)
当您的代码完成时,它会从事件循环中移除,并且宿主进程会返回检查它,以查看是否还有更多代码要运行。事件循环包含更多的两个事件处理程序:一个要立即执行(justNow 函数),另一个在一秒钟内执行(inAWhile 函数)。
主机进程现在尝试匹配所有发生的事件,以查看是否有为它们注册的处理程序。它发现 justNow 等待的事件已经发生,所以它开始运行它的代码。当 justNow 函数退出时,它会再次检查事件循环,搜索事件处理程序。假设 1 秒过去了,它运行 inAWhile 函数,以此类推....
事件循环有一项简单的工作——监控调用堆栈、回调队列和微任务队列。如果调用堆栈为空,事件循环将从微任务队列中获取第一个事件,然后从回调队列中获取并将其推送到调用堆栈,调用堆栈有效地运行它。这样的迭代在事件循环中称为一个滴答。
正如大多数开发人员所知,Javascript 是单线程的,这意味着 javascript 中的两条语句不能并行执行,这是正确的。执行是逐行发生的,这意味着每个 javascript 语句都是同步和阻塞的。但是有一种方法可以异步运行您的代码,如果您使用 setTimeout() 函数,一个由浏览器提供的 Web API,它可以确保您的代码在指定时间(以毫秒为单位)后执行。
例子:
console.log("Start");
setTimeout(function cbT(){
console.log("Set time out");
},5000);
fetch("http://developerstips.com/").then(function cbF(){
console.log("Call back from developerstips");
});
// Millions of line code
// for example it will take 10000 millisecond to execute
console.log("End");
setTimeout 将回调函数作为第一个参数,以毫秒为单位的时间作为第二个参数。在浏览器控制台中执行上述语句后,它将打印
Start
End
Call back from developerstips
Set time out
注意:您的异步代码在所有同步代码执行完毕后运行。
逐行理解代码如何执行
JS 引擎执行第一行并在控制台中打印“开始”
在第 2 行中,它看到名为 cbT 的 setTimeout 函数,JS 引擎将 cbT 函数推送到回调队列。
之后指针会直接跳转到第 7 行,在那里它会看到 promise 和 JS 引擎将 cbF 函数推送到微任务队列。
然后它将执行数百万行代码并结束它会打印“结束”
主线程执行结束后,事件循环将首先检查微任务队列,然后回调队列。在我们的例子中,它从微任务队列中获取 cbF 函数并将其推送到调用堆栈中,然后它将从回调队列中选择 cbT 函数并推送到调用堆栈中。
https://i.stack.imgur.com/s2VnP.jpg
There are two key differences........
JavaScript 是高级、单线程语言、解释性语言。这意味着它需要一个将 JS 代码转换为机器代码的解释器。解释器意味着引擎。用于 chrome 的 V8 引擎和用于 safari 的 webkit。每个引擎都包含内存、调用堆栈、事件循环、计时器、Web API、事件等。
事件循环:微任务和宏任务
事件循环的概念非常简单。有一个无限循环,JavaScript 引擎等待任务,执行它们然后休眠,等待更多任务
任务被设置——引擎处理它们——然后等待更多任务(在睡眠和消耗接近于零的 CPU 时)。可能会在引擎忙碌时出现任务,然后将其排入队列。任务形成一个队列,即所谓的“宏任务队列”
微任务完全来自我们的代码。它们通常由 Promise 创建:执行 .then/catch/finally 处理程序成为一个微任务。微任务也可以在 await 的“掩护下”使用,因为它是 promise 处理的另一种形式。在每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再运行任何其他宏任务或渲染或其他任何事情。
https://i.stack.imgur.com/Oc7O1.png
process.nextTick
与setTimeout
与setImmediate
的语义略有不同,尽管您不必真正关心。我有一个更详细的 blog post called setTimeout and friends。