在非回调函数上调用 promisify():“有趣”的结果是节点。为啥?

Posted

技术标签:

【中文标题】在非回调函数上调用 promisify():“有趣”的结果是节点。为啥?【英文标题】:Call promisify() on a non-callback function: "interesting" results in node. Why?在非回调函数上调用 promisify():“有趣”的结果是节点。为什么? 【发布时间】:2018-08-10 07:19:51 【问题描述】:

我在 node 的 promisify() 函数中发现了一个奇怪的行为,我无法弄清楚它为什么会这样做。

考虑以下脚本:

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var http = require('http')

var promisify = require('util').promisify

;(async () => 
  try 

    // UNCOMMENT THIS, AND NODE WILL QUIT
    // var f = function ()  return 'Straight value' 
    // var fP = promisify(f)
    // await fP()

    /**
     * Create HTTP server.
     */
    var server = http.createServer()

    /**
     * Listen on provided port, on all network interfaces.
     */

    server.listen(3000)
    server.on('error', (e) =>  console.log('Error:', e); process.exit() )
    server.on('listening', () =>  console.log('Listening') )
   catch (e) 
    console.log('ERROR:', e)
  
)()

console.log('OUT OF THE ASYNC FUNCTION')

这是一个简单的自调用函数,用于启动节点服务器。 这很好。

现在...如果您取消注释“UNCOMMENT THIS”下的行,节点将退出而不运行服务器。

我知道我在一个调用回调但返回一个值的函数上使用了promisify()。所以,我知道这本身就是一个问题。

但是...为什么节点只是退出...?

这真的很难调试——尤其是当你有比一个小脚本更复杂的东西时。

如果您将函数定义更改为实际调用回调的内容:

var f = function (cb)  setTimeout( () =>  return cb( null, 'Straight value') , 2000) 

一切都按预期进行......

更新

极大的简化:

function f () 
  return new Promise(resolve => 
    console.log('AH')
  )


f().then(() => 
  console.log('Will this happen...?')
)

只会打印“AH”!

【问题讨论】:

嗯,很有趣。我假设经过一段时间的空事件循环(等待永远不会解决的承诺)节点会继续前进。而且由于您没有其他任何东西在监听输入,它就像一个脚本一样,只是关闭了吗? 这是我的理论,但它没有解释为什么使用 适当的 回调调用函数,它可以工作! the callback style is required IMHO. @lloyd,请阅读以“我知道”开头的部分。 - 他知道 这个问题的重点是确定节点可能存在的问题,并且——在未来——使开发人员更容易识别这样的问题。实际上,这不应该发生。在我在节点中打开一个适当的错误之前,我想知道更广泛的 SO 社区对此有何看法(并且可能会让我自己感到尴尬!) 【参考方案1】:

在非回调函数上调用 promisify():“有趣”的结果是节点。为什么?

因为您允许 node.js 无事可做地进入事件循环。由于没有实时的异步操作在运行,也没有更多的代码要运行,node.js 意识到没有其他事情可做,也没有办法让其他任何事情运行,所以它退出了。

当您点击await 并且 node.js 回到事件循环时,没有任何东西可以让 node.js 继续运行,所以它退出了。没有计时器或打开的套接字或任何类型的东西让 node.js 保持运行,因此 node.js 自动退出检测逻辑表示没有其他事情可做,所以它退出了。

因为 node.js 是一个事件驱动的系统,如果您的代码返回到事件循环并且没有任何类型的异步操作(打开套接字、侦听服务器、计时器、文件 I/O 操作、其他硬件侦听器等...),则没有任何运行可以在事件队列中插入任何事件并且队列当前为空。因此,node.js 意识到永远无法在此应用程序中运行更多代码,因此它退出了。这是 node.js 中内置的自动行为。

fp() 内部的真正异步操作将有某种套接字或计时器或打开的东西以保持进程运行。但是因为你的是假的,所以没有任何东西可以让 node.js 运行。

如果你在f() 中放置一个setTimeout() 1 秒,你会看到进程退出发生在1 秒之后。所以,进程退出与promise无关。这与您已经回到事件循环的事实有关,但您还没有开始任何可以让 node.js 运行的东西。

或者,如果你把setInterval()放在你的异步函数的顶部,你同样会发现进程没有退出。


所以,如果你这样做,同样会发生这种情况:

var f = function ()  return 'Straight value' 
var fP = promisify(f);
fP().then(() => 
    // start your server here
);

或者这个:

function f() 
    return new Promise(resolve => 
       // do nothing here
    );


f().then(() => 
    // start your server here
);

问题不在于promisify() 操作。这是因为您正在等待一个不存在的异步操作,因此 node.js 无事可做,它注意到无事可做,因此它自动退出。拥有一个带有.then() 处理程序的开放承诺并不是让 node.js 保持运行的东西。而是需要一些主动的异步操作(定时器、网络套接字、监听服务器、正在进行的文件 I/O 操作等)来保持 node.js 运行。

在这种特殊情况下,node.js 基本上是正确的。你的承诺永远不会解决,没有其他东西会排队运行,因此你的服务器永远不会启动,你的应用程序中的其他代码也不会运行,因此继续运行实际上没有用。您的代码无事可做,也无法实际执行任何其他操作。

如果您将函数定义更改为实际调用回调的内容:

那是因为你使用了一个计时器,所以 node.js 在等待 promise 解决的时候有一些实际的事情要做。尚未调用 .unref() 的正在运行的计时器将阻止自动退出。

值得一读:How does a node.js process know when to stop?


仅供参考,您可以“关闭”或“绕过”node.js 自动退出逻辑,只需将其添加到启动代码中的任何位置:

// timer that fires once per day
let foreverInterval = setInterval(() => 
    // do nothing
, 1000 * 60 * 60 * 24);

这总是让 node.js 有事可做,因此它永远不会自动退出。然后,当您确实希望退出进程时,您可以调用 clearInterval(foreverInterval) 或使用 process.exit(0) 强制执行操作。

【讨论】:

@Merc - 我不确定你在用那个代码问什么?你永远不会解决你的承诺,所以.then() 处理程序永远不会被调用。同样,该代码实际上没有安排异步操作,因此 node.js 将退出。仅仅创建一个 Promise 并不能阻止 node.js 退出。你需要一个 LIVE 异步操作来阻止 node.js 退出。 代码粘贴错误——抱歉。请参阅我就该问题提供的更新。你是绝对正确的——我从没想过会这样!!!那么......更新代码实际上发生了什么......?!? f() 显然会运行,并返回一个承诺。 Promise 的回调中的代码被执行(所以你会看到“AH”)。然后......当然,它肯定应该执行“then”中的内容?!? 我的意思是,that f().then 是尚未执行的代码!毫无疑问,节点应该至少执行脚本其余部分的代码!我意识到节点需要确定何时“停止”;但是一旦 ALL 的代码已经运行,这肯定应该适用!你能描述一下实际发生了什么吗? @Merc - 这不是它的工作原理。 node.js 的内部跟踪待处理的实际异步操作。它保持引用计数。 .then() 处理程序被视为 EventEmitter 上的事件侦听器。他们根本不计入这一点。所有这些都是仍在进行中的实际异步操作。而且,这是有充分理由的。如果 node.js 进入事件循环并且没有实际的异步操作在运行,那么 node.js 将永远无法运行更多代码。绝不。所以,它还不如退出。 @Merc - 这是因为一旦 node.js 进入事件循环,它可以做任何其他事情的唯一方法就是将事件放入事件循环中。但是,可能发生的唯一方法是,如果存在某种类型的异步操作,可能会导致事件被放入事件循环中。因此,如果没有这些在飞行中,那么就不会将任何内容放入事件队列中,因此事件循环将无事可做。也可以退出。

以上是关于在非回调函数上调用 promisify():“有趣”的结果是节点。为啥?的主要内容,如果未能解决你的问题,请参考以下文章

node fs 解决回调地域问题

Zend\Mail 标头:在非对象上调用成员函数 setType()

在非对象上调用成员函数 getClientOriginalName()

在非构造的“对象”上调用非虚拟成员函数是不是定义明确? [复制]

致命错误:在非对象上调用成员函数 fetch()?

致命错误:在非对象上调用成员函数 getLoginUrl()