【问题标题】:Event loop queueing order事件循环排队顺序
【发布时间】:2021-08-24 01:51:30
【问题描述】:

我正在尝试了解以下代码的顺序(我在本地使用节点运行)。

async function bfunc() {
  console.log("2 - b func");
}

async function afunc() {
  await bfunc();                           // rest of function pushed to event queue
  console.log('4 - a func');
}

const prom = new Promise((resolve, reject) => {
  console.log("1 - inside promise");
  resolve('5 - foo');
  afunc();
}).then((value) => {                       // this is also pushed to the event queue
  console.log(value);
});

console.log("3 - last thing");

打印出来:

1 - inside promise
2 - b func
3 - last thing
4 - a func
5 - foo

我相信我的大部分困惑源于不确定事件何时被推送到事件队列。

  1. 上面评论的 2 个地方是否正确?

  2. 在等待调用的情况下,是异步函数本身(包括其被推送到事件队列的堆栈环境吗?

  3. 为什么 '5 - foo' 在 '4 - a func' 之前不打印?我读到here那个

一个未决的承诺可以用一个值来实现,也可以用一个原因(错误)拒绝。当这些选项中的任何一个发生时,将调用由 Promise 的 then 方法排队的相关处理程序

所以我认为这会使队列中的第一个事件成为 then 函数的处理程序。

【问题讨论】:

  • “上面评论的 2 个地方是否正确?” -- 我不确定我是否理解这意味着什么。 “正确”的定义是什么?
  • 你不会在这里学到很多有用的东西,因为这些操作实际上都不是异步的。而且,您将事物描述为“推送到事件队列”的事​​情过于简单化了。事件循环中有多个单独的队列,用于不同类型的事件。但是,await.then() 总是在当前的 Javascript 链完成执行后解决(根据 promise 规范),这就是输出的最后两行排在最后的原因。
  • @ggorlen,我想我的意思是根据语言规范准确无误。 IE。在代码中的这些点,事件被推送,以便它们最终在当前链之后执行。
  • @jfriend00 是这些队列在 javascript 规范中实现的方式,还是特定于引擎的?关于“await 或 .then() 总是在当前链之后解决”,是不是说在“await bfunc()”行之后发生的所有事情(在 afunc 内)都将在当前链之后发生?最后,你能解释一下为什么“2 - b func”会在“3 -last thing”之前打印吗?
  • 查看this 以及从那里链接的线程。请注意,afunc()prom 形成了完全独立的承诺链,你不应该试图争论它们之间的任何顺序。

标签: javascript node.js event-loop


【解决方案1】:

这是妙语:只有在当前的 Javascript 序列完成执行并将控制权返回给事件系统后,Promise 才会通知侦听器解析/拒绝。请继续阅读以了解有关这意味着什么以及它如何影响您的代码的更多详细信息。

让我们在你的代码上加上行号,这样我们就可以更轻松地讨论:

1     async function bfunc() {
2       console.log("2 - b func");
3     }
4    
5     async function afunc() {
6       await bfunc();         // rest of function pushed to event queue
7       console.log('4 - a func');
8     }
9    
10    const prom = new Promise((resolve, reject) => {
11      console.log("1 - inside promise");
12      resolve('5 - foo');
13      afunc();
14    }).then((value) => {     // this is also pushed to the event queue
15      console.log(value);
16    });
17    
18    console.log("3 - last thing");

事件顺序

  1. 执行第 10 行。这会导致 const promise = new Promise(...).then(...) 开始执行
  2. 作为new Promise(...) 构造函数的一部分,它调用promise 的执行函数(您传递给构造函数的回调)。立即同步执行。
  3. 由于同步调用了 Promise 执行程序,您会看到执行第 11 行的第一行输出 1 - inside promise
  4. 然后执行第 12 行,promise 的状态更改为用值 '5 - foo' 完成。
  5. 然后执行第 13 行并调用 afunc()
  6. 即使afunc() 被声明为async,它仍会开始同步执行其函数体,因此它会立即在第6 行调用await bfunc()。这会执行bfunc() 并等待它返回的promise 解决。
  7. 作为执行bfunc() 的一部分,您会在控制台的第2 行看到2 - b func
  8. bfunc() 返回一个 Promise(因为它被声明为 async)并且由于其中没有 await 并且没有从它返回任何 Promise,因此该 Promise 的状态被推进到履行。这在 Promise 作业队列中插入一个作业,以在将来调用任何已注册的 .then() 处理程序或在此承诺上使用 await 的代码。
  9. 由于在await bfunc() 中的afunc() 内部有一个await,所以afunc() 的执行被暂停,直到它收到通知,通过调用bfunc() 返回的promise 已解决。该通知尚未到来,因为它正在 Promise 作业队列中等待。同时,afunc() 立即返回一个处于待处理状态的 Promise。
  10. promise 执行器返回,新创建的 promise 可用。
  11. 由于这个新创建的 Promise 现在已经实现,它在 Promise 作业队列中插入一个作业,以便在将来调用任何已注册的 .then() 处理程序或使用 await 在此 Promise 上的代码。
  12. 然后,第 14 行在新创建的 Promise 上执行 .then(...)。这会为之前创建的 Promise 注册一个 .then() 处理程序,然后返回一个新的 Promise。
  13. 新的承诺分配给变量prom
  14. 然后执行第 18 行,您会看到 3 - last thing 作为您的第三行输出。
  15. 那么当前的 Javascript 链就完成了,不再需要同步执行,所以它将控制权返回给事件系统。
  16. 作为事件系统获得控制权后首先要做的事情之一,就是检查 Promise 作业队列中是否存在任何待处理的任务。 Promise 作业队列中的第一项是调用 bfunc() 的已解决承诺,它在第 6 行等待。由于承诺已解决,afunc() 的执行被恢复,您在输出中看到 4 - a funcafunc() 返回解决了它的承诺,但没有人听它的承诺,所以在这条 Javascript 链中没有什么可做的,控制权回到了事件系统。
  17. 再次,它会在检查其他内容之前检查 Promise 作业队列,并且再次有一个待处理的 Promise 任务。这会从prom 承诺调用.then() 处理程序,您会在输出中看到5 - foo

要点

  1. awaitasync 函数中暂停该函数的执行,并立即从该函数返回一个承诺返回给调用者。
  2. 当 Promise 被解决或拒绝时,它们总是通过 Promise 作业队列通知来自 await.then()try/catch.catch() 的完成或错误,该队列仅在当前的 Javascript 链返回控制时为作业提供服务回到事件系统。因此,连续的代码行会一直运行,直到它们将控制权返回给事件系统。这就是为什么当你在上面的描述中看到一个 promise 被解析时,它会在 Promise 作业队列中插入一个作业,并且该作业不会立即运行。人们常说,promise 总是“在事件循环的下一个滴答声中异步”解决或拒绝。虽然有些人不同意“事件循环的下一个滴答声”的确切含义,但这个概念是正确的,即 只有在当前 Javascript 线程完成执行并将控制权返回给事件系统后才会通知解决/拒绝强>。然后事件系统可以查看它的各种队列以确定下一步要做什么,它查看的第一个队列之一是 Promise 作业队列,其中承诺解决和拒绝事件正在等待并等待通知正在监听它们的人(使用 @ 987654364@或.then().catch()
  3. afunc() 承诺链完全独立于此代码中的其他承诺链。这就是我所说的“即发即弃”的承诺链,因为没有代码监视它是否成功完成或有错误。这几乎总是一个编程错误,因为如果它被拒绝,则没有处理该拒绝的处理程序,您将从系统收到未处理的拒绝错误(类似于同步代码的未捕获异常)。
  4. 另外,当您必须有多个独立的 Promise 链(其中包含具有不确定时间的真正异步操作)时,一个 Promise 链将如何完成相对于另一个 Promise 链是不可预测的。此特定代码将始终以相同的方式运行,因为您的 Promise 或 async 函数背后没有真正的异步操作,因此所有这些代码都有确定的时间,但在实际代码中并非如此。因此,如果您需要代码以特定顺序运行,您可以连接两条链并让一条链依赖于另一条链,这样您就可以控制顺序。

您的问题

上面评论的 2 个地方是否正确?

有点。首先,请记住已完成的 Promise 通过 Promise 作业队列通知 await.then()。并且,请记住 async 函数中的 await 会暂停该函数的执行并立即返回一个新的承诺。因此,第 6 行的第一条评论更准确地说是“afunc() 执行暂停,等待来自bfunc() 返回的承诺的通知”。第 14 行的第二条注释是“.then() 处理程序已注册并等待来自第 10 行创建的承诺的通知”。

在等待调用的情况下,是异步函数本身(包括其被推送到事件队列的堆栈环境吗?

不是真的。使用与生成器相同的过程,async 函数可以在某个执行点暂停,保留其所有当前状态。在这种情况下,该函数已经注册了对具有bfunc() 返回的承诺的已完成或拒绝状态链感兴趣,并且当它收到该通知时,它将继续执行该函数(如果承诺已完成)或将拒绝其自己的 Promise(如果 Promise 被拒绝并且该拒绝没有本地处理程序)。

所以,函数状态(它是 JS 解释器中的一个对象)只是坐在那里等待来自 bfunc() 返回的承诺的通知。它本身并没有在事件队列中放入任何东西。它在bfunc() 承诺上注册了对状态更改的兴趣,然后当bfunc() 承诺解决或拒绝时,将在 Promise 作业队列中插入一些内容以通知注册的侦听器。

为什么 '5 - foo' 在 '4 - a func' 之前不打印?

正如我之前在要点 #3 和 #4 中所说的,这里有两个独立的承诺链,因为没有任何东西在监控 afunc() 的完成情况。最后两行之间的顺序与将承诺状态更改添加到每个承诺链的 Promise 作业队列的确切时间有关。如果您关心这个顺序并且这些是真正的异步操作,那么您将不得不对其进行不同的编程,这样您就不会有两个独立且不协调的 Promise 链,每个链都有自己不可预测的时间。

您可以在我上面对步骤的描述中找到确切的操作顺序,但这实际上并不是那么相关,因为如果没有真正的异步操作和真正的异步操作,您不会使用这样的代码,这些操作的顺序两个独立的承诺链是不可预测的,因为它们相对于彼此解决或拒绝承诺的时间是不可预测的。因此,尝试理解这种详细程度并不值得,因为无论如何您都不应该在异步编程中依赖它。如果您关心排序,请链接两条链,以便您的代码确定排序,而不是时间的微小细节。

【讨论】:

  • 应该注意的是,4 在 5 之前返回是最近的:stackoverflow.com/questions/62032674/… 在旧版本的 JS 引擎中,5 会首先返回。
  • @Kaiido - 注意到与await 的性能改进相关的细节。但是,这里更大的一点(正如我在回答中多次说过的那样)是根本不应该依赖于 4 和 5 的相对顺序,因为如果这些是真正的异步操作,那么该顺序将是不确定的,所以只会是用不同的编码控制连接两条链。而且,如果这些不是真正的异步操作,那么我们不应该为它们使用 Promise。幸运的是,您可以安全地使用 Promise,而无需了解或关心该级别的详细时间安排。
  • 是的,这种行为最近确实发生了变化,这一事实进一步巩固了人们不应该依赖它的观点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-18
  • 2021-11-10
  • 2018-10-16
  • 1970-01-01
  • 1970-01-01
  • 2023-03-18
相关资源
最近更新 更多