【问题标题】:Unexpected behavior mixing process.nextTick with async/await. How does the event loop work here?意外行为将 process.nextTick 与 async/await 混合。事件循环在这里如何工作?
【发布时间】:2018-11-05 15:22:44
【问题描述】:

我的代码:

async function run(){

    process.nextTick(()=>{
        console.log(1);
    });

    await (new Promise(resolve=>resolve()).then(()=>{console.log(2)}));

    console.log(3);

    process.nextTick(()=>{
        console.log(4);
    });

    new Promise(resolve=>resolve()).then(()=>{console.log(5)});

}

run();

我的预期输出是按顺序打印 1、2、3、4、5 的数字,但我得到的是:

1
2
3
5
4

正如预期的那样,第一个nextTick 在第一个.then 回调之前被评估,因为process.nextTick.then 都被推迟到未来的报价,并且process.nextTick.then 之前声明。所以12按预期顺序输出。

在解决.then 之前,代码不应达到await 之后的内容,并且这可以按预期工作,因为3 会在预期的位置输出。

然后基本上我们重复了代码的第一部分,但这次.thenprocess.nextTick 之前被调用。

这似乎是不一致的行为。为什么process.nextTick 在第一次回调之前调用.then 而不是第二次?

【问题讨论】:

  • 我不确定,但我怀疑这与作为所谓的微任务执行的承诺回调有关。 This youtube video 很好地展示了这个概念。
  • 我没有得到完整的答案,也没有兴趣详细解释事件循环中发生的事情,但那是因为第一个 nextTick 是同步安排的,而第二个不是。此行为特定于本机承诺。任何非本地承诺实现都没有机会在nextTick 之前安排thenasync 同步执行,直到第一次出现await,如果在第一次出现nextTick 之前有await,情况会有所不同。
  • 正如我评论你这个问题的早期版本,node.js 事件循环非常复杂。它有多种类型的事件,它以特定的顺序提供服务。但是,它下一步处理的内容更加复杂,这也取决于它在事件循环优先级的循环中的位置。不做大量工作就很难理解或预测——我个人研究了很长时间,也没有完全弄清楚。
  • 我早就得出结论,您通常应该将所有异步操作视为相对于彼此随机排序,因此如果您需要特定顺序,那么您应该编写代码来强制执行命令。使一件事依赖于它之前必须发生的事情,这样无论事件循环做什么,你的代码仍然按照你需要的顺序处理事情。
  • 所以,我的解释是因为在事件循环中每个 process.nextTick() 被调用的位置,这导致了相对于其他类型事件的不同处理顺序。第一个process.nextTick() 在事件循环位于位置A 时被调用。第二个process.nextTick() 在处理了.then() 回调之后被调用,这是事件循环循环的不同部分,这意味着随着事件循环的继续处理不同类型的事件,它将在第二次以不同的顺序点击process.nextTick()

标签: javascript node.js promise async-await es6-promise


【解决方案1】:

node.js 事件队列不是单个队列。它实际上是一堆不同的队列,像 process.nextTick() 和 promise .then() 处理程序不在同一个队列中处理。所以,不同类型的事件不一定是先进先出的。

因此,如果您有多个事件大约在同一时间进入事件队列,并且您希望它们按特定顺序提供服务,那么保证该顺序的最简单方法是编写代码以强制执行您想要的顺序,不要试图准确地猜测两个同时进入队列的事物将如何排序。

确实,两个完全相同类型的操作,如两个process.nextTick() 操作或两个已解决的promise 操作将按照它们放入事件队列的顺序进行处理。但是,不同类型的操作可能不会按照它们被放入事件队列的顺序进行处理,因为在事件循环通过所有不同类型的事件进行的循环中,不同类型的事件是在不同的时间处理的。

也许可以完全理解 node.js 中的事件循环是如何针对每种类型的事件工作的,并准确预测同时进入事件队列的两个事件将如何相对于彼此进行处理,但是这不简单。当新事件被添加到事件队列时,它还取决于事件循环在其当前处理中的位置,这一事实使情况变得更加复杂。

在我之前的 cmets 中的交付示例中,新交付相对于其他交付的确切处理时间取决于新订单到达队列时交付驱动程序的位置。 node.js 事件系统也是如此。如果在 node.js 处理计时器事件时插入新事件,则它与其他类型事件的相对顺序可能与 node.js 在插入时处理文件 I/O 完成事件时不同。因此,由于这种显着的复杂性,我不建议尝试预测几乎同时插入事件队列的不同类型的异步事件的执行顺序。

而且,我应该补充一点,原生 Promise 直接插入到事件循环实现中(作为它们自己的微任务类型),因此原生 Promise 实现在您的原始代码中的行为可能与非原生 Promise 实现不同。这也是不尝试准确预测事件循环将如何安排不同类型的事件相对于彼此的原因。


如果处理顺序对您的代码很重要,则使用代码强制执行特定的完成处理顺序。

作为一个例子,当事件被插入到事件队列中时,事件队列正在做什么很重要,你的代码简化为:

async function run(){

    process.nextTick(()=>{
        console.log(1);
    });

    await Promise.resolve().then(()=>{console.log(2)});

    console.log(3);

    process.nextTick(()=>{
        console.log(4);
    });

    Promise.resolve().then(()=>{console.log(5)});

}

run();

生成此输出:

1
2
3
5
4

但是,当run() 被称为Promise.resolve().then(run) 并且顺序突然不同时,只需更改:

async function run(){

    process.nextTick(()=>{
        console.log(1);
    });

    await Promise.resolve().then(()=>{console.log(2)});

    console.log(3);

    process.nextTick(()=>{
        console.log(4);
    });

    Promise.resolve().then(()=>{console.log(5)});

}

Promise.resolve().then(run);

生成这个完全不同的输出:

2
3
5
1
4

您可以看到,当代码从已解决的承诺开始时,该代码中发生的其他已解决承诺会在 .nextTick() 事件之前得到处理,而当代码从事件队列处理。这是使事件队列系统非常难以预测的部分。


因此,如果您试图保证特定的执行顺序,您必须使用所有相同类型的事件,然后它们将按照彼此相对的 FIFO 顺序执行,或者您必须让您的代码强制执行你想要的顺序。所以,如果你真的想看到这个顺序:

1
2
3
4
5

你可以使用所有本质上映射到这个的承诺:

async function run(){

    Promise.resolve().then(() => {
        console.log(1);
    })
    Promise.resolve().then(() => {
        console.log(2)
    });

    await Promise.resolve().then(()=>{});

    console.log(3);

    Promise.resolve().then(() => {
        console.log(4)
    });

    Promise.resolve().then(()=>{console.log(5)});

}

run();

或者,您更改​​代码的结构,使其始终按所需顺序处理事物:

async function run(){

    process.nextTick(async ()=>{
        console.log(1);
        await Promise.resolve().then(()=>{console.log(2)});
        console.log(3);
        process.nextTick(()=>{
            console.log(4);
            Promise.resolve().then(()=>{console.log(5)});
        });
    });
}

run();

最后两种情况中的任何一种都会生成输出:

1
2
3
4
5

【讨论】:

  • 写得好,非常有益!谢谢!
【解决方案2】:

感谢有帮助的 cmets,让我意识到并非所有推迟到稍后刻度的 javascript 方式都是平等的。我曾认为(new Promise()).thenprocess.nextTick,甚至setTimeout(callback,0) 都将完全相同,但事实证明我不能这样假设。

现在我将离开这里,我的问题的解决方案就是如果 process.nextTick 不能按预期顺序工作,则不要使用它。

所以我可以将我的代码更改为(免责声明这实际上不是一般的好的异步代码):

async function run(){

  process.nextTick(()=>{
    console.log(1);
  });

  await (new Promise(resolve=>resolve()).then(()=>{console.log(2)}));

  console.log(3);

  (new Promise(resolve=>resolve())).then(()=>{console.log(4)});

  (new Promise(resolve=>resolve())).then(()=>{console.log(5)});

 }

run();

现在我确保45 之前被记录,方法是将它们都设为相同类型的异步调用。让它们都使用process.nextTick 也将确保45 之前记录。

async function run(){

  process.nextTick(()=>{
   console.log(1);
  });

  await (new Promise(resolve=>resolve()).then(()=>{console.log(2)}));

  console.log(3);

  process.nextTick(()=>{
    console.log(4)
  });

  process.nextTick(()=>{
    console.log(5)
  });

}

run();

这是我的问题的解决方案,但如果有人想对我的问题提供更直接的答案,我很乐意接受。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-12-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-08
    • 2019-09-29
    相关资源
    最近更新 更多