节点无错误退出且不等待Promise(事件回调)

IT技术 javascript node.js promise
2021-01-25 10:47:57

我有一个非常奇怪的问题,即等待已将其传递resolve给事件发射器回调的 Promise只是退出进程而不会出错。

const {EventEmitter} = require('events');

async function main() {
  console.log("entry");

  let ev = new EventEmitter();

  let task =  new Promise(resolve=>{
    ev.once("next", function(){resolve()}); console.log("added listener");
  });

  await task;

  console.log("exit");
}

main()
.then(()=>console.log("exit"))
.catch(console.log);

process.on("uncaughtException", (e)=>console.log(e));

当我运行它时,我希望该过程停止,因为显然“下一步”目前从未发出。但我得到的输出是:

条目
添加监听器

然后 nodejs 进程优雅地终止。

我认为这是什么做的垃圾收集器,但evtask显然还是在范围main所以我真的不知道为什么这个过程完全没有错误地退出。

很显然,我最终发出的事件,但我已经简化我的代码上面的重现。我在node v8.7.0我的代码有问题还是这是一个节点错误?

3个回答

这个问题基本上是:节点如何决定是退出事件循环还是再次循环?

基本上,节点保持一个已调度异步请求的引用计数——setTimeouts网络请求等。每次调度一个,该计数增加,每次完成一个,计数减少。如果您到达事件循环周期的末尾并且引用计数为零,则节点退出。

简单地创建一个Promise或事件发射器不会增加引用计数——创建这些对象实际上不是一个异步操作。例如,这个promise 的状态将始终为pending,但进程会立即退出:

const p = new Promise( resolve => {
    if(false) resolve()
})

p.then(console.log)

同样,这也会在创建发射器并注册一个监听器后退出:

const ev = new EventEmitter()
ev.on("event", (e) => console.log("event:", e))

如果您希望 Node 等待一个从未安排过的事件,那么您可能认为 Node 不知道是否有可能发生未来的事件,但它确实如此,因为每次安排一个事件时它都会进行计数。

所以考虑这个小改动:

const ev = new EventEmitter()
ev.on("event", (e) => console.log("event:", e))

const timer = setTimeout(() => ev.emit("event", "fired!"), 1000)
// ref count is not zero, event loop will go again. 
// after timer fires ref count goes back to zero and node exits

作为旁注,您可以删除对计时器的引用:timeout.unref()这与前面的示例不同,将立即退出:

const ev = new EventEmitter()
ev.on("event", (e) => console.log("event:", e))

const timer = setTimeout(() => ev.emit("event", "fired!"), 1000)
timer.unref()

Bert Belder 在这里对事件循环进行了很好的讨论,消除了很多误解:https : //www.youtube.com/watch?v= PNa9OMajw9w

@ Mark Meyer - 这里非常有用的答案,谢谢,使整个问题变得非常清楚。关于这一点:Basically node keeps a reference count of scheduled async requests. setTimeouts, network requests, etc- 你知道这包括什么有严格的定义吗?它基本上与节点的核心/本机module异步吗?
2021-03-29 10:47:57
@SohailSi,您可以添加一个 setTimeout 来解决第一个示例中的Promise。然后它将等待定时器设置的时间量。
2021-04-06 10:47:57
你能让你的第一个例子(Promise 例子)等一下吗?
2021-04-09 10:47:57
很好的答案。您是否了解这在浏览器中的表现?例如v8?或者你在文本中引用节点的所有时间(例如节点保持引用计数、节点退出等),它也可以解释为 v8(例如 v8 保持引用计数、v8 退出等)?所以事情在浏览器中也会以完全相同的方式工作。
2021-04-12 10:47:57
@Gaurang Patel - 浏览器 javascript 引擎是否会退出?(我想不是。)
2021-04-12 10:47:57

我调试了几个小时,为什么我们的一个脚本在main函数中间的一行代码之后退出(没有任何错误)那是一条线await connectToDatabase(config)你知道吗?

我发现这两个函数之间的区别很关键:

第一的:

async function connectToDatabase(config = {}) {
    if (!config.port) return;
    return new Promise(resolve => {
       resolve();
    })
}

第二:

async function connectToDatabase(config = {}) {
    return new Promise(resolve => {
       if (!config.port) return;
       resolve();
    })
}

第二个函数有时(当 config.port 为空时)创建从未解决的Promise,它使事件循环为空,并且 node.js 退出时认为“这里无事可做”

自己检查一下:

// index.js - start it as node index.js
(async function main() {
  console.log('STARTED')
  await connectToDatabase()
  console.log('CONNECTED')
  console.log('DOING SOMETHING ELSE')
})()

如果您使用第二个功能,则不会打印“CONNECTED”和“DOING SOMETHING ELSE”,如果您使用第一个功能,则会打印

其次,如果config.port未定义,那么你的Promise永远不会解决,永远......因为你没有调用解决或拒绝。回调(reject, resolve)=>{}应该是同步的 - 如果你在那里提出一个套接字请求,它应该让节点保持活动状态。
2021-04-13 10:47:57

作为一般说明,您的代码结合了三个相似但不同的方法:async/await、promises、事件侦听器。我不确定你所说的“炸弹爆炸”是什么意思。但是看代码,结果似乎是预料之中的。

您的进程退出,因为您在添加事件侦听器时调用了 promise。它成功解析,因此退出。如果您尝试记录任务,它会给您未定义。不要在 then 语句中记录“exit”,而是记录结果。任务将是未定义的,因为程序不会等待解析其值及其“代码块已完成”。

您可以将代码简化为以下内容。正如您所看到的,因为您调用了 resolve 函数,它会立即解析。

const { EventEmitter } = require('events');

let ev = new EventEmitter()
var p = new Promise(( resolve ) => {
    ev.once("next", resolve("Added Event Listener"));
})

p
    .then(res => console.log(res))
    .catch(e => console.log(e))
恐怕它永远不会解决,因为该事件永远不会发出。这个例子是人为的,最终归结为马克的回答所说的。
2021-03-20 10:47:57
好吧,我的示例是人为设计的(没有尝试) - 我实际上是在做流 -> 异步可迭代转换(这就是我混合这三者的原因)。我猜您在说“已解决”时正在谈论返回的函数,但在Promise的上下文中,“解决”是指Promise完成的时间。这就是为什么你的答案乍一看似乎是错误的。
2021-03-27 10:47:57
那就是这个想法。他很困惑为什么它会这样工作,所以我写了另一种方式来展示它。除了理解,没有什么可以“解决”的。
2021-03-28 10:47:57
您的代码在语义上与 OP 不同;您的 Promise 正在解决,因为您尚未将resolve调用放入函数中,因此它会立即被评估。
2021-04-01 10:47:57
我明白那个。我试图证明如果没有发出或 setTimeout 解析(即使是嵌套的)也不会阻止您的程序停止。但无论如何,马克的回答更明确。
2021-04-13 10:47:57