"return Promise.resolve()" 如何影响 JavaScript 事件循环?

4

我已经使用Javascript有一段时间了,也了解Javascript事件循环的工作原理。然而,我遇到了一个不太能理解的情况。考虑以下代码:

setTimeout(()=> console.log('1'));
Promise.resolve('whatever').then(()=>console.log('2'));
console.log('3');

我期望得到以下输出结果:

3
1
2

然而,我在Chrome js控制台上运行它,却得到了以下结果:

3
2
1

问题是: "Promise.resolve().then(fn)" 应该立即调用该函数或者将函数执行过程插入到事件循环结束的位置 - 就像 'setTimeout' 的工作方式一样 - 我是否遗漏了什么?

Promise 回调函数的一个重要行为是它们总是在“干净的堆栈”中运行。 - Pointy
1
你可以将消息附加到当前事件循环的迭代中,或者将其预置到下一个迭代中。Promise执行前者,而setTimeout执行后者。因此,通过递归调用then可以阻塞事件循环,但不能通过递归调用setTimeout来阻塞。 - user6445533
2个回答

3

浏览器实现了多个任务队列。规范要求实现维护两个队列:

  1. ScriptQueue
  2. PromiseQueue

很可能还有一个TimerQueue(HTML DOM任务队列),也符合HTML DOM Level 2 Timer规范。这些都是在运行时填充的FIFO队列,最终成为事件循环队列。根据您的代码示例:

setTimeout(()=> console.log('1'));
Promise.resolve('whatever').then(()=>console.log('2'));
console.log('3');
  • 第1行代码会被推入(很可能是)一个TimerQueue(HTML DOM任务队列)中。
  • 第2行代码会被推入PromiseQueue中。
  • 第3行代码会被推入栈中并立即执行(直到完成)。

一旦栈为空,每个队列将依次排空。在您的示例中,Promise队列首先排空,然后TimerQueue最后排空。

通过稍微扩展您的示例,可以进一步说明这一点:

setTimeout(()=> console.log('second from final')); // <-- first item in TimerQueue
Promise.resolve('whatever').then(()=>console.log('2')); //<-- first time in PromiseQueue
setTimeout(()=> console.log('just prior to final')); // <-- second item in TimerQueue
Promise.resolve('whatever').then(()=>console.log('1')); //<-- second item in PromiseQueue
setTimeout(()=> console.log('final')); // <-- third item in ScriptQueue 
console.log('3'); // <-- synchrounous function call placed onto the stack, executed immediately

请注意,执行顺序不能保证。规范没有定义队列的执行顺序:

本规范未定义多个作业队列的服务顺序。ECMAScript 实现可以将作业队列的 PendingJob 记录的 FIFO 评估与一个或多个其他作业队列的 PendingJob 记录的评估交织在一起。

编辑

与Bergi讨论后(下面),我指出浏览器实现可能会创建其他队列。最有可能与此帖子相关的队列是TimerQueue,它是一个Task Queue,保存了HTML DOM规定的计时器任务。


1
“Line 1 将其推入 ScriptQueue” - 不是的,setTimeout 在 ECMAScript 规范中并没有规定,并且它也不使用 ScriptQueue。你需要查看 HTML5 定时器规范来了解这一点。 - Bergi
@Bergi - 恕我直言,每个请求未来执行的工作都会被排队 - 不管该请求的来源是什么。如果它不是 Promise,则该工作将排队在 ScriptQueue 中。 - Randy Casburn
1
是的,它们都在队列中,但是setTimeout甚至不会创建一个“作业”,而是创建一个“任务”。术语“作业”仅在ECMAScript规范中使用。 - Bergi
HTML DOM Level 2任务/微任务与ECMAScript Jobs之间存在1:1的关系。 - Randy Casburn
是的,我确实相信也可能有一个计时器队列 - 如果实现了它。我没有找到任何关于这个事实的参考资料。像你一样,我只能_假设_它们存在。因此,我采取了规范中引用的事实信息的直线。实现可能包括其他队列,也可能不包括,但根据规范,它们只必须创建这两个队列。 - Randy Casburn
显示剩余3条评论

0

setTimeout 的行为遵循一组微妙的规则。例如,在几个浏览器上,最小超时时间是 4ms,而不是0ms

您是正确的,Promise 不会立即执行,而是等待清洁的堆栈运行。

还有一些技巧,比如添加一个 <script> 标签来强制将某些内容推到事件循环的顶部。请参见 setImmediate 浏览器的 polyfill。


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