“事件循环队列”和“任务队列”之间有什么区别?

36
我不明白以下代码是如何运行的。为什么"1"在"b"之后,但"h"在"3"之后呢?
按理应该是a、b、1、2、h、3的顺序才对吧?有些文章说"事件循环队列"和"任务队列"之间的区别导致了这样的输出结果。但是为什么呢?我已经阅读了ECMAScript 2015 - 8.4 Jobs and Job Queues的规范,想要了解Promise的工作原理,但是越看越糊涂。我该如何解决这个问题呢?
var promise = new Promise(function(resolve, reject) {resolve(1)});
promise.then(function(resolve) {console.log(1)});
console.log('a');
promise.then(function(resolve) {console.log(2);});
setTimeout(function() {console.log('h')}, 0);
promise.then(function(resolve) {console.log(3)});
console.log('b');

// a
// b
// 1
// 2
// 3
// h

我知道Promise是异步的,但是setTimeout(..)异步操作的回调总是在Promise的异步操作之后。为什么呢?

Promise 是异步的 - 即使是像同步代码一样内联书写,.then 也会被异步调用 - 这就是 Promise 的作用。 - Jaromanda X
请美化这段代码 :/ - AliN11
6个回答

24
“1”为什么在“b”的后面?
承诺规范说明所有的承诺 .then() 处理程序必须在调用堆栈被清空后异步调用。因此,由于 ab 在调用堆栈上同步执行,它们都将在任何 .then() 处理程序之前执行,所以 1 总是在 ab 之后。
一些有趣的阅读材料:
  1. 任务、微任务、队列和时间表
  2. JavaScript 承诺的执行顺序是什么?
  3. 编写 JavaScript 框架 - 执行定时,超出 setTimeout

这个线程 "Promises wiggle their way between nextTick and setImmediate" 中有一些很好的建议:

我不建议依赖非链式事件的确切执行顺序。如果您想控制执行顺序,请重新排列回调函数,以便您希望稍后执行的回调函数依赖于您希望较早执行的回调函数,或者实现一个队列(在幕后执行相同的操作)。

换句话说,如果您依赖于特定的异步事件排序,则应该将它们链接起来,而不是依赖于实现中未指定的调度。


1
为什么 h3 后面呢 :p - Jaromanda X
2
@JaromandaX,很可能JS实现的作业队列比浏览器实现的事件队列具有更高的优先级。但不是很确定。 - MinusFour
@JaromandaX - 这是一个略微不同的问题,与OP所问的不同。我需要进行一些研究,才能确定这是根据规范还是基于如何排队.then()处理程序与setTimeout()事件而实现特定的。 - jfriend00
7
@JaromandaX Promise会将一个任务添加到任务队列中,但setTimeout会将函数放置在事件循环的末尾,直到时间过期。当一个任务被添加到任务队列中时,JavaScript引擎会取出任务队列上的下一个任务,并且只有在任务队列为空时,它才会移动到事件循环的下一个条目。事件循环中的每个槽位都有自己的任务队列。 - balajisoundar
2
@JaromandaX - Tasks vs. micro-tasks。看起来排序是按照惯例(而不是普遍遵循的规范)进行的。 - jfriend00
显示剩余2条评论

15
在HTML术语中,来自同一域的页面或页面集合的事件循环可以有多个任务队列。来自相同任务来源的任务总是进入同一个队列,浏览器选择下一个要使用的任务队列。
来自计时器任务来源的定时器回调任务也会进入同一队列。让我们称这个队列为任务队列"A"
ECMAscript 2015 (ES6)规范要求运行Promise反应回调函数以形成自己的作业队列,称为"PromiseJobs"。 ECMAscript和HTML规范不使用相同的语言,因此我们可以将ECMA的“Promise Job queue”与浏览器中的HTML task queue "B"概念上等同-至少是与定时器使用的队列不同。
理论上,浏览器可以选择从A或B队列中选择任务来运行,但在实践中,Promise任务队列具有更高的优先级,并且会在计时器回调被运行之前清空。 这就是为什么"h"最后被记录的原因。对于已完成的Promise的then调用会将作业放置在Promise队列中,这些作业具有比计时器回调更高的优先级进行执行。只有在执行了console.log(3)之后,Promise队列才会变为空,这允许计时器回调执行。
高级内容 ECMAScript守护者选择不在其规范中使用HTML5术语或任务队列的描述,因为ECMAScript可以在不止HTML浏览器中运行。
原生的 Promise 队列实现可能使用 "微任务" 队列,而不是一个单独的专用 Promise 任务队列。微队列中的作业仅在当前脚本线程和先前添加到微队列中的任何任务完成后运行。
详细了解微任务队列并不是理解 Promise 所必需的。
对于缺乏原生 Promise 支持(所有版本的 IE 等)的浏览器,Promise polyfill 可能会使用计时器,并且当涉及到 Promise 反应和计时器回调的顺序时,其行为可能与原生实现不完全相同。

8

从Es6开始,任务队列运行时添加了以容纳Promises. 通过new Promise()我们可以本地处理异步代码。而setTimeout并不是JavaScript的一部分,而是由浏览器提供的Web API之一。

现在我们有了两个队列:回调队列(callback queue)任务队列(job queue)。任务队列也被称为微任务队列(micro task queue)。

关键是,任务队列比回调队列更具优先级。因此,在您的示例中,首先执行同步代码。

 console.log('a');  // a
 console.log('b');  // b

那么,Promise被发送到任务队列(Job Queue),setTimeout()被发送到回调队列(Callback Queue)。现在事件循环(Event Loop)会首先检查任务队列,无论setTimeout()设置了多长时间。由于队列实现了“先进先出”,它们按照顺序执行,因为它们只是将日志记录到控制台。

promise.then(function(resolve) {console.log(1)}); // 1
promise.then(function(resolve) {console.log(2)}); // 2
promise.then(function(resolve) {console.log(3)}); // 3 

在作业队列清空后,事件循环会检查回调队列。

setTimeout(function() {console.log('h')}, 0); // h

4

我发现这篇文章对于新手来说很容易理解JavaScript。

这是从@getify的书中复制粘贴的。

使用一个比喻:事件循环队列就像游乐园里的一次游玩,当你完成一次游玩后,你必须重新排队等待下一次。但是作业队列就像完成了一次游玩,然后插队直接再次上车。

事件循环队列 - 适用于除了Promise之外的所有异步回调函数,例如h。

作业队列 - 适用于所有与Promise相关的异步回调函数。1、2、3

同步 - a,b


最后三行也被复制了吗? - undefined
"getify's book"是什么? - undefined

3

ES6有两个队列

  1. 回调队列
  2. 任务队列(微任务队列)

无论是setTimeout还是promise都是异步代码。

在setTimeout中,我们明确指定要自动运行的函数在后台浏览器api工作完成后(在setTimeout的情况下是web浏览器api的计时器功能)执行,一旦计时器完成其工作,它将函数推入回调队列,并且必须等待所有js同步代码完成,这就是为什么。

console.log("a")
console.log("b")

首先,让我们谈谈完成的顺序问题。

现在来到 Promise 部分,JS 中的任何 Promise 都会做两件事:

  1. 设置后台 API 功能
  2. 返回一个 Promise 对象

.then() 指定了一旦 Promise 已被解决(但不是立即),将运行哪个函数。

.then() 内指定的函数在任务完成时会被推入作业队列中。

一旦 JS 中所有同步代码都完成,事件循环首先检查作业队列,然后检查回调队列。这就是为什么 'h' 最后被记录的原因。


1
在JavaScript运行时,任务队列(Task Queue)中创建了作业队列(Job Queue),它与任务队列非常相似,但具有优先级。这意味着JavaScript事件循环首先查看作业队列。如果作业队列中有任何任务,事件循环将检查堆栈。如果堆栈为空,它将把作业队列中的任务推入堆栈。之后,事件循环再次检查作业队列。如果作业队列为空,事件循环将检查任务队列。如果有任何任务,任务将被推送到堆栈以执行。作业队列是在ES6中添加到JavaScript运行时以执行Promise的。
例如:
var promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('promise win')
    }, 4000)
})
promise.then((result) => {
    console.log(result)
})
setTimeout(() => {
    console.log('setTimeout win')
}, 4000)

输出:

承诺胜出

定时胜出

解释:

无论是setTimeout还是promise都是异步执行的,并且它们花费的时间相同。然而,承诺先执行,然后是回调函数。这是因为承诺被移动到作业队列,而setTimeout被移动到回调队列。这些队列在JavaScript运行时创建,用于异步执行任务。Web API负责处理此过程。由于作业队列优先于任务队列,承诺会在回调函数之前执行。


1
阅读没有标点符号的长句子是很困难的。 - vadzim dvorak
请添加缺少的标点符号。提前谢谢。 - Peter Mortensen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接