【问题标题】:How to leverage event loop in JavaScript and Node.js?如何在 JavaScript 和 Node.js 中利用事件循环?
【发布时间】:2016-12-15 02:38:16
【问题描述】:

事件循环的概念是通用的还是特定于语言的? 我正在寻找带有示例的详细解释,以清楚地理解以下内容:

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() 使用事件循环来防止 stackoverflow 是否正确?我读到了这段代码的解释:

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

更新 2

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

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

前面提到的解释和这里的答案中的矛盾使得它更难理解。

【问题讨论】:

  • AFAIK ecmascript 根本没有定义事件循环。所以不存在 JavaScript 事件循环之类的东西。它依赖于实现(例如 nodejs)。
  • 您想要什么级别的答案?它的工作原理可以是从“这里和如何使用setTimeout 的示例”到“这就是C 中发生的事情”
  • @freakish:在 ES2015 规范中确实如此。它称之为“作业队列”。
  • 如果你想要一个简单的 js 例子,已经有很多重复了。没有理由不关闭它。
  • 这是我写的 3 个答案,在不同的详细程度,描述了事件循环:stackoverflow.com/questions/29799316/…stackoverflow.com/questions/19616477/…stackoverflow.com/questions/29883525/…

标签: javascript node.js ecmascript-6 event-loop


【解决方案1】:

事件循环的概念是通用的还是特定于语言的?

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

它是如何工作的?

所有血淋淋的细节都在规范中,主要在名为Jobs and Job Queues的部分。

有几个关键方面:

  1. 有一个 队列 等待由JavaScript 引擎。当引擎完成一项作业时,它会开始处理队列中的下一个作业/任务(如果有)。

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

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

所以考虑一下这段代码:

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

如果您运行该代码(通过 NodeJS 或浏览器),会发生以下情况(省略一些不相关的细节):

  1. 一开始队列为空,JavaScript 引擎空闲
  2. 环境(NodeJS、浏览器)将作业排队以运行脚本
  3. JavaScript 引擎获取作业并运行脚本:
    • 输出“一”
    • 它为我们提供给setTimeout 的匿名函数设置了一个计时器
    • 工作结束
  4. 在某些时候,环境中的计时器机制确定是时候调用回调了,所以它会排队一个作业来调用它
  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);

如您所见,它在 500 毫秒安排一个定时回调,然后在 100 毫秒安排另一个。但是 100 毫秒回调中的代码至少需要 1000 毫秒才能运行。会发生什么?

  1. 环境将作业排队以运行脚本
  2. JS 引擎接手这项工作
    • 输出时间值和“一”,比如1470727293584 one
    • 为函数 first 设置一个定时回调,在未来 500 毫秒内
    • 为函数 second 设置一个定时回调,在未来 100 毫秒内
    • 工作结束
  3. 大约 100 毫秒后,环境将作业排队运行second
  4. JavaScript 引擎获取作业并运行函数
    • 它开始工作(好吧,实际上它只是在忙着等待)
  5. 大约 400 毫秒后(从设置定时器开始的 500 毫秒),环境排队一个作业来调用first;由于 JavaScript 引擎正忙于上一个作业,因此该作业位于队列中
  6. 与此同时,JavaScript 引擎仍在处理调用first 的工作:
    • JavaScript 引擎从 #3 开始所做的工作终于完成了
    • 工作完成
  7. JavaScript 引擎从队列中选择下一个作业并运行对second 的调用
    • 它输出时间值和“二”,比如1470727294687 two
    • 工作结束

请注意,环境在 JavaScript 忙碌时做了一些事情;它排队等待完成的工作。

环境可以在 JavaScript 引擎忙碌时做事这一事实非常重要。

(值得注意的是,在排队作业时,某些环境可能不一定将作业添加到队列的end;例如在某些浏览器中,“队列”实际上不止一个优先级略有不同的队列...)

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

与其说是利用,不如说是知道它的存在。还需要注意的是,虽然 JavaScript 具有 run-to-completion 语义,但它运行的环境可能同时在做其他事情,即使在 JavaScript 代码运行时也是如此。

ECMAScript-6/2015 中引入了关于事件循环的任何更改?

并非如此,尽管它在规范中的定义比以前更完整。可能与更改最接近的事情与承诺有关:您使用thencatch 安排的回调将总是作为作业排队,它永远不会同步运行。这是 JavaScript 规范第一次定义异步发生的事情(ajax 和计时器不是 JavaScript 规范的一部分)。


下面,你问过:

考虑这段代码:

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

这里假设 setTimeout() 使用事件循环来防止 stackoverflow 是否正确?

(我假设在那之后立即有一个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();

...在这种情况下,可能会发生堆栈溢出,但编写该代码的方式会非常奇怪。

【讨论】:

  • 这是一个非常详细的答案。谢谢你。如果我有一个递归函数来访问一个非常大的列表,那么递归函数内部的 settimeout() 函数是否可以防止 stackoverflow 被称为利用事件循环?
  • @steady_daddy:我不确定如何在真正的递归函数中使用setTimeout,因为堆栈必须在作业结束之前展开,这意味着函数已经终止,在计时器回调可以运行。但是使用setTimeout 来安排在当前作业完成后发生的事情是一种常见的技术,并且肯定会利用队列。假设你做了一些你知道会立即排队事件回调的事情。您可以使用值为 0 的 setTimeout 来安排一些代码在该事件回调之后运行,因为知道队列是按顺序处理的。
  • 考虑这段代码: var list = readHugeList(); var nextListItem = function() { var item = list.pop(); if (item) { // 处理列表项... setTimeout( nextListItem, 0); } };这里假设 setTimeout() 使用事件循环来防止 stackoverflow 是否正确?
  • @steady_daddy:不(除非代码一开始写得有点奇怪),但它正在做其他重要的事情。我已添加到答案的末尾。
  • 先生,请您原谅。请看看我更新的问题。
猜你喜欢
  • 2015-10-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-23
  • 2017-04-28
相关资源
最近更新 更多