Node.js中的回调函数是始终异步或始终同步吗?还是它们可以"有时是异步,有时是同步"?

3

我正在考虑用node.js制作某些东西,和其他学习node的人一样,我对它的异步性质有疑问。我搜索了一下,但没有找到关于这个问题的具体答案(也许是我没搜索好...),所以我问一下:

如果文档说明node.js回调函数是异步的,那么它们是否保证异步?如果您自己编写带有回调的函数,应该设计成始终异步或始终同步吗?还是它们有时可以是同步的,有时不是?

举个例子,假设你想要从互联网上加载一些数据,并创建一个加载数据的函数。但是数据很少更改,所以你决定将其缓存以供将来调用。你可以想象有人会像这样编写该函数:

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

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

如果数据已被缓存,该函数仅会直接执行回调而不会异步触发。在这种情况下,函数的异步性质实际上是完全不必要的。

这种做法是否不被鼓励?应该将函数的第二行改为setTimeout(function () {callback(cachedData)}), 0),以确保它异步触发吗?

我之所以问这个问题,是因为我曾经看到过一些代码,其中回调函数内的代码仅仅假定回调函数外的函数已经执行完毕。我对此感到有些反感,想着“但你怎么知道回调函数一定会异步触发呢?如果它不需要异步触发并同步执行了怎么办?你为什么要假设每个回调都保证是异步的呢?”

如果您能就此问题进行澄清,我将不胜感激。谢谢!


有时候是这样,有时候是那样,如果你也涉及到第三方模块的话。它们应该总是异步的,但并不是所有开发者都完美无缺。 - Kevin B
回调函数可以是同步的,也可以是异步的。另一方面,只要遵循 Promise/A+ 规范,Promises 就始终是异步的。 - idbehold
取决于您认为什么是“回调”,这可能有所不同。 - Kevin B
4个回答

3

你的假设都是正确的。

如果文档中说了,node.js回调通常是异步的,那么这是否保证了回调一定是异步的?

是的。当然,有些带异步回调的函数和带同步回调的函数,但没有同时做两者的。

如果你自己编写带回调的函数,应该设计成始终异步或始终同步吗?

是的。

它们可能有时是同步的,有时不是,例如缓存?这种做法被反对吗?

是的。非常混乱。

函数的第二行应该改为setTimeout(function () {callback(cachedData)}), 0),以确保它异步触发吗?

是的,那是个好主意,但在node中,你最好使用{{link2:setImmediateprocess.nextTick}}代替setTimeout
或者使用promise,它们确保异步执行,这样你就不必自己延迟处理了。

我曾经看到一些代码,其中回调函数内部的代码仅仅假设了回调函数外部的函数已经执行完毕。我对此有些不安。

是的,可以理解。即使您使用的API保证异步性,编写代码以便按照执行顺序阅读仍然更好。如果可能的话,应该将立即在异步回调之前执行的内容放置在回调之前(有例外情况)。


2

个人认为您的想法是正确的,即永远不要假设异步代码在函数返回后执行(按定义来说,异步只是意味着您不能假设它是同步的,而不是假设它不是同步的)。

但是,在javascript周围已经形成了一种文化,认为既可以同步也可以异步的函数是反模式。这很有道理:如果您无法预测代码运行的时间,那么很难对其进行推理。

因此,通常所有流行的库都会避免使用它。通常情况下,可以安全地假定异步代码永远不会在脚本结束之前运行。

除非您有非常特殊的原因,否则不要编写既可以同步也可以异步的函数-这被认为是反模式。


尽管我个人持有这种观点,但我很想看到一些引用和参考来支持它。 - georg
刚刚花了10分钟在谷歌上搜索我和 Promises/A 团队的讨论,当时我抱怨 Promise 应该可以处理同步代码。甚至有一些博客文章提到永远不要混合使用同步/异步代码。但是现在谷歌上突然变得非常流行,找不到任何相关内容。这就是我愿意投入寻找答案的时间。 - slebetman
@georg,slebetman:你可能正在寻找http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony。 - Bergi

0

你需要摆脱“不需要异步”的观念 - 在Node中,要么某些东西需要同步,要么就应该毫不犹豫地采用异步方式。

异步是Node.js整个哲学的核心,而回调函数本质上是异步的(如果它们被正确设计的话),这使得Node能够像它所声称的那样非阻塞。这是假设回调将异步执行的前提 - 它是Node设计哲学的一个密切部分。

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

在Node中,我们使用process.nextTick()来确保某些东西以异步方式运行,延迟其函数直到事件循环的下一个tick。


1
回调函数本质上是异步的,不,本质上需要回调函数的异步函数是异步的,但回调函数本身并非必须是异步的。即使在Node中也存在许多同步回调的例子,例如Array.prototype.forEach(),Array.prototype.map()等。 - slebetman
@slebetman 我不认为它们是回调函数。虽然这个术语在JavaScript世界中有点混乱或冲突。我会考虑它们是迭代器函数或比较器函数。 - Kevin B
@slebetman,正如Kevin B指出的那样,它们是迭代器的示例。这是必须要做出的区分。 - jonny
1
还有一个事实,这个问题正在询问一个特定的情况,在这种情况下,有时同步可能会导致真正的问题。 - Kevin B
@KevinB: 迭代器是不同的东西 - 事实上它们也存在于es6中。我们所谓的“回调函数”在技术上是高阶函数 - 也称为函数式编程。“回调函数”本身是操作异步数据的高阶函数的子集 - 也称为I/O monads。但是,js社区已经习惯于将它们称为回调函数。请注意,Array.prototype.forEach的规范本身将其称为回调函数:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach。不要将回调函数与迭代器混淆。 - slebetman
MSDN也称其为回调函数:https://msdn.microsoft.com/zh-cn/library/ff679980(v=vs.94).aspx。ES5规范也将其称为回调函数:https://people.mozilla.org/~jorendorff/es5.html。 - slebetman

0

你不应该编写一个接受回调函数的函数,有时候这个回调函数是同步的,这可能会导致尾递归问题。看下面的例子:

function doSynchronousWork (cb) {
    cb();
}
doSynchronousWork(function looper () {
   if (...somecondition...) {
       doSynchronousWork(looper);
   }
});

在上面的例子中,由于调用栈嵌套太深,您将遇到最大调用栈超出错误。通过强制同步函数异步运行,可以通过在继续递归之前清除调用栈来解决该问题。
function doSynchronousWork (cb) {
    process.nextTick(cb);
}
doSynchronousWork(function () {
   if (...somecondition...) {
       doSynchronousWork();
   }
});

请注意,这个尾递归问题最终将在js引擎中得到解决。
不要将回调函数与迭代器函数混淆。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接