如何利用JavaScript和Node.js中的事件循环?

事件循环的概念是一般还是特定于语言? 我正在寻找具体的解释与例子来清楚地理解以下内容:

1.它是如何工作的?

  1. 如何/何时应该/可以利用JavaScript中的事件循环?
  2. ECMAScript-6/2015中引入的有关事件循环的任何更改?

更新:没有缺乏答案,但我正在寻找一个易于理解的定义与例子。

附录:

考虑下面的代码:

var list = readHugeList(); var nextListItem = function() { var item = list.pop(); if (item) { // process the list item... setTimeout( nextListItem, 0); } }; 

假设setTimeout()在这里使用事件循环来防止计算器溢出是否正确? 我读了这个代码的解释:

堆栈溢出被消除,因为事件循环处理recursion,而不是调用堆栈。

更新2

在上面的代码中给出的setTimeout()的完整解释如下:

堆栈溢出被消除,因为事件循环处理recursion,而不是调用堆栈。 当nextListItem运行时,如果item不为null,则将超时函数(nextListItem)推送到事件队列中 ,并且函数退出,从而使调用堆栈清空。 当事件队列运行超时事件时 ,下一个项目被处理,一个计时器被设置为再次调用nextListItem。 因此,该方法从开始到结束处理而没有直接recursion调用,因此无论迭代次数如何, 调用栈都保持清晰

上述解释中的矛盾和答案使这一切更加难以理解。

事件循环的概念是一般还是特定于语言?

在最高一级,一般。 但是不同的环境将实现不同的细节,以至于您需要特定于环境的信息才能知道发生了什么。

怎么运行的?

所有的细节都在规范中,主要在“ 作业和作业队列”部分 。

有几个关键方面:

  1. 有一个等待由JavaScript引擎运行的作业 队列 (JavaScript规范的术语)或“任务”(如HTML规范所称的那样)。 当引擎完成一个工作时,它将被用于队列中的下一个工作/任务(如果有的话)。

  2. 一旦工作开始 ,它会一直运行,直到完成; 没有其他工作可以打断它。 这被称为运行完成run-to-completion) ,对JavaScript的工作原理非常重要。

  3. 作业队列按顺序处理。

所以考虑这个代码:

 console.log("one"); setTimeout(function() { console.log("two"); }, 1000); 

如果你运行这个代码(通过NodeJS或浏览器),下面是发生了什么(省略了一些不相关的细节):

  1. 开始时,队列是空的,JavaScript引擎处于空闲状态
  2. 环境(NodeJS,浏览器)排队工作来运行脚本
  3. JavaScript引擎提取作业并运行脚本:
    • 它输出“一”
    • 它为我们给setTimeout的匿名函数设置了一个计时器
    • 工作结束
  4. 在某种程度上,环境中的定时器机制决定了是时候调用callback函数了,所以它将一个作业排队等候调用它
  5. JavaScript引擎从队列中提取该作业并运行该函数
    • 它输出“两个”
    • 工作结束

现在考虑这个代码:

 console.log(Date.now(), "one"); setTimeout(function first() { console.log(Date.now(), "two"); }, 500); setTimeout(function second() { var end = Date.now() + 1000; while (end > Date.now()) { // Busy-wait (normally this is a Bad Thing™, I'm using it here // to simulate actual work that takes significant time } }, 100); 

正如你所看到的,它安排了一个500ms的定时callback,然后是100ms的定时callback。 但是在100ms的callback代码将至less需要1000ms运行。 怎么了?

  1. 环境将作业排队以运行脚本
  2. JS引擎select这个工作
    • 输出时间值和“one”,比如说1470727293584 one
    • 在将来设置一个定时的callback函数为500ms
    • 将来定时callback函数为100ms
    • 工作结束
  3. 大约100毫秒后,环境排队工作second
  4. JavaScript引擎提取作业并运行该function
    • 它开始工作(好吧,真的只是在等待)
  5. 大约400毫秒后(定时器设置后500毫秒),环境排队工作先打电话; 由于JavaScript引擎忙于前一个作业,因此作业位于队列中
  6. 与此同时,JavaScript引擎仍然在努力工作:
    • 最终由#3的JavaScript引擎完成的工作已经完成
    • 工作结束
  7. JavaScript引擎从队列中提取下一个作业,然后将调用运行到second
    • 它输出时间值和“两个”,比如1470727294687 two
    • 工作结束

请注意,JavaScript在繁忙的时候, 环境做了些什么。 它排队工作要完成。

JavaScript引擎繁忙时, 环境可以做的事情是非常重要的。

(值得注意的是,当排队作业时,某些环境可能不一定会在队列末尾添加作业;例如在某些浏览器中,“队列”实际上是多于一个队列,而且具有稍微不同的优先级……)

如何/何时应该/可以利用它?

这不是因为知道它在那里。 同样重要的是要注意,虽然JavaScript具有运行到完成的语义,但是运行的环境可能同时也在做其他事情,即使JavaScript代码正在运行。

在ECMAScript-6/2015中引入的有关事件循环的更改?

不是真的,虽然它在规格上的定义比以前更完整。 可能最接近变化的是与承诺有关的事情:您计划的callback或catch 总是作为一个工作排队,它将永远不会同步运行。 这是JavaScript规范首次定义了asynchronous发生的事情(ajax和定时器不是JavaScript规范的一部分)。


下面,你问:

考虑这个代码:

 var list = readHugeList(); var nextListItem = function() { var item = list.pop(); if (item) { // process the list item... setTimeout(nextListItem, 0); } }; 

假设setTimeout()在这里使用事件循环来防止计算器溢出是否正确?

(我假设有一个nextListItem();之后立即调用。)

不,但是它正在做其他重要的事情。 非setTimeout版本会看起来像这样: 1

 var list = readHugeList(); while (list.length) { var item = list.pop(); // process the list item... } 

这是一个简单的循环,没有堆栈溢出的可能性。

它所做的就是与事件循环协同工作,避免了一个真正的长时间运行的工作,它会阻塞JavaScript引擎,阻止它处理任何其他工作(如I / O完成,点击或其他事件)。 因此,通过逐个处理项目,将工作分解为小型工作,从而确保整个工作队列得到处理。 这意味着处理列表需要更长的时间,但是在这样做的时候不会阻止其他事情。


1的确,该代码的非setTimeout版本可能如下所示:

 var list = readHugeList(); var nextListItem = function() { var item = list.pop(); if (item) { // process the list item... // then recurse nextListItem(); } }; nextListItem(); 

…在这种情况下,堆栈溢出是有可能的,但是编写这些代码将是一种非常奇怪的方式。