node.js 中的回调是始终异步还是始终同步?或者它们可以是“有时是一个,有时是另一个”?

Posted

技术标签:

【中文标题】node.js 中的回调是始终异步还是始终同步?或者它们可以是“有时是一个,有时是另一个”?【英文标题】:Are callbacks in node.js either always asynchronous or always synchronous? Or can they be "sometimes one, sometimes the other"? 【发布时间】:2015-11-03 20:35:18 【问题描述】:

我正在尝试在 node.js 中制作一些东西,而且我(和其他所有开始学习 node 的人一样)对它的异步性质有疑问。我搜索了一下,但找不到关于它的这个特定问题的答案(也许我只是没有很好地搜索......),所以这里是:

node.js 回调,一般来说,保证如果文档这么说是异步的吗?如果您制作自己的回调函数,您是否应该以这样的方式进行设计,以使它们始终异步或始终同步?或者它们有时是同步的,有时不是同步的?

例如,假设您想通过 Internet 加载一些数据,并且您创建了一个函数来加载它。但是数据不会经常更改,因此您决定将其缓存以备将来调用。你可以想象有人会这样写这个函数:

function getData(callback) 
    if(dataIsCached) 
        callback(cachedData)
     else 
        fetchDataFromNetwork(function (fetchedData) 
            dataIsCached = true;
            cachedData = fetchedData;
            callback(fetchedData);
        );
    

其中 fetchDataFromNetwork 是一个异步执行其回调的函数(这有点伪代码,但我希望你明白我的意思)。

这个函数只会在数据没有被缓存的情况下异步触发,如果数据被缓存了它就直接执行回调。在这种情况下,函数的异步特性毕竟是完全没有必要的。

这种事情不鼓励吗?函数的第二行是否应该改为setTimeout(function () callback(cachedData)), 0),以保证它异步触发?

我问的原因是我不久前看到了一些代码,其中回调内部的代码只是假设回调外部的函数的其余部分在回调内部的代码之前执行。我对此有点退缩,想“但是你怎么知道回调将异步触发?如果它不需要并且同步执行怎么办?为什么你会假设每个回调都是有保证的?是异步的?”

任何关于这一点的澄清将不胜感激。谢谢!

【问题讨论】:

有时是一个,有时是另一个,如果您也指的是第 3 方模块。它们应该始终是异步的,但并非所有开发人员都是完美的。 回调可以是任何一种。另一方面,Promise 将总是异步(只要它们遵循 Promise/A+ 规范)。 猜猜这取决于您认为callback 【参考方案1】:

你的假设都是正确的。

一般来说,node.js 回调是否保证是异步的,如果文档这么说的话?

是的。当然,也有functions with async callbacks and functions with sync callbacks,但没有两者兼得。

如果您自己制作接受回调的函数,您是否应该以这样一种方式设计,使它们要么始终是异步的,要么始终是同步的?

是的。

它们有时可能是同步的,有时不是同步的,例如缓存?这种事情不鼓励吗?

是的。 Very much d̲̭i̫̰̤̠͎͝ͅͅs͙̙̠c̙͖̗̜o͇̮̗̘͈̫ų̗͔̯ŕa҉̗͉͚͈͈̜g͕̳̱̗e҉̟̟̪͖̠̞ͅd͙͈͉̤̞̞̩.

函数的第二行是否应该改为setTimeout(function () callback(cachedData)), 0),以保证它异步触发?

是的,这是个好主意,尽管在节点中您宁愿使用setImmediate or process.nextTick 而不是setTimeout。 或者使用保证异步的 promise,这样你就不必关心自己延迟事情了。

不久前我看到一些代码,其中回调内的代码只是假设回调外的其余函数在回调内的代码之前执行。我当时有点退缩了

是的,可以理解。即使您使用的 API 保证异步,编写代码以便可以按照执行顺序读取它仍然会更好。如果可能的话,你应该把执行的东西放在异步回调之前(异常证明规则)。

【讨论】:

【参考方案2】:

我个人同意你的观点,即永远不要假设函数返回后执行异步代码(根据定义,异步只是意味着你不能假设它是同步的,而不是假设它不是同步的)。

但是围绕 javascript 发展起来的一种文化认为可以同步或异步的函数是一种反模式。这是有道理的:如果您无法预测何时代码运行,则很难推理。

所以一般来说所有流行的库都会避免它。一般来说,假设异步代码永远不会在脚本结束之前运行是非常安全的。

除非你有非常特殊的原因,否则不要编写一个既可以同步又可以异步的函数——这被认为是一种反模式。

【讨论】:

虽然我个人同意这个观点,但我希望看到一些引用和参考来支持它。 当我抱怨承诺也应该处理同步代码时,我花了最后 10 分钟谷歌搜索我与承诺/A 人的讨论。甚至还有几篇关于从不混合同步/异步的博客文章。但找不到任何(同步/异步突然成为谷歌上非常流行的话题)。这大约是我想在答案上投入的时间。 @georg, slebetman:你可能正在寻找blog.izs.me/post/59142742143/designing-apis-for-asynchrony。【参考方案3】:

您需要摆脱不需要异步的观念——在 Node 中,要么需要同步,要么它应该是异步的另一个想法。

异步是 Node.js 背后的全部理念,而回调本质上是异步的(如果设计得当的话),这允许 Node 像它声称的那样是非阻塞的。这就是回调将异步执行的假设的前提——它是 Node 设计理念的重要组成部分。

函数的第二行是否应该改为 setTimeout(function () callback(cachedData)), 0) 以保证它异步触发?

在 Node 中,我们使用process.nextTick() 来确保异步运行,将其功能延迟到事件循环的下一个滴答声。

【讨论】:

callbacks are by their very nature asynchronous.. 不,异步函数本质上需要回调,但回调本质上不是异步的。甚至在节点中也存在很多同步回调的示例:Array.prototype.forEach()、Array.prototype.map() 等。 @slebetman 我不会考虑那些回调。尽管该术语在javascript世界atm中有点混乱或冲突。我认为它们是迭代器函数或比较器函数。 @slebetman 正如 Kevin B 指出的那样,它们是迭代器的示例。这是一个必要的区分。 那么事实上,这个问题是在询问一个特定情况,在这种情况下,有时同步会导致真正的问题 @KevinB:迭代器有些不同——实际上它们也存在于 es6 中。我们所说的“回调”在技术上是高阶函数——也称为函数式编程。 “回调”本身是对异步数据进行操作的高阶函数的子集 - 也称为 I/O monad。但是 js 社区已经习惯称它们为回调。请注意,Array.prototype.forEach 的规范本身将其称为回调:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…。不要将回调与迭代器混淆。【参考方案4】:

你不应该有一个函数接受有时是同步的回调,这可能会导致你遇到尾递归问题。举个例子:

function doSynchronousWork (cb) 
    cb();

doSynchronousWork(function looper () 
   if (...somecondition...) 
       doSynchronousWork(looper);
   
);

在上面的示例中,由于调用堆栈嵌套太深,您将遇到最大调用堆栈超出错误。强制同步函数异步运行通过在继续递归之前清除调用堆栈来解决该问题。

function doSynchronousWork (cb) 
    process.nextTick(cb);

doSynchronousWork(function () 
   if (...somecondition...) 
       doSynchronousWork();
   
);

注意这个尾递归问题最终会在js引擎中修复

不要将回调函数与迭代器函数混淆

【讨论】:

以上是关于node.js 中的回调是始终异步还是始终同步?或者它们可以是“有时是一个,有时是另一个”?的主要内容,如果未能解决你的问题,请参考以下文章

node.js File System(文件系统模块)

如何识别回调是同步执行还是异步执行? [复制]

如何识别回调是同步执行还是异步执行? [复制]

node.js 可以编写一个方法,以便可以两种方式调用它 - 即使用回调或异步/等待?

node.js中的forEach是同步还是异步

node.js中的forEach是同步还是异步