如何知道JavaScript回调函数是同步还是异步?

10

嗨!我正在学习回调函数,我了解回调函数可以是同步的或异步的。

我在https://www.w3schools.com/jquery/jquery_callback.asp上阅读有关回调函数的内容时感到很困惑。

这里是示例代码:

$("button").click(function(){
    $("p").hide("slow", function(){
        alert("The paragraph is now hidden");
    });
}); 

我能否检查是否有一种方法来知道上面的回调是同步还是异步的?

我猜测它是同步的,因为它必须等到“缓慢”的动画结束后警报才会出现。在JavaScript或Node.js中,默认情况下所有回调都是同步的,除非您执行诸如setTimeOut或process.nextTick之类的操作。


3
那个链接里有许多很棒的信息,可能有助于回答这个问题,但它本身不是一个重复的问题。 - Taegost
2
我根本不认为这是重复的问题。@Sheen An提出的问题是基于试图理解Javascript代码块何时是同步或异步的。您提供的链接中的问题是询问如何从Ajax检索响应,而Ajax本身就是异步的。 - cmpgamer
5个回答

10

您有几个问题,所以我会逐一回答:

Javascript或Node.js中默认情况下所有回调函数都是同步的,除非您执行像setTimeout或process.nextTick这样的操作吗?

在浏览器中,您可以将此作为经验法则:仅setTimeoutsetInterval,请求和事件是异步的。其他内置回调(例如Array.prototype.map)是同步的。

在Node.js上更加复杂:例如,您可以同时进行文件读取同步地异步地。那么您只需要知道回调本质上是异步的。

我能否检查是否有办法知道上面的回调是同步还是异步的?

不幸的是,如果不检查您调用的方法/函数的源代码,您无法知道它是同步还是异步的。

我猜测上面是同步的,因为它必须等到“慢”动画结束才能出现警报。

确切地说,您可以从jQuery的.hide方法的文档中找到这个:

complete
类型:Function()
动画完成后要调用的函数,每个匹配元素调用一次。


1
这是一个恰当的回答。仅仅是文档(或源代码本身)! - Igor Soloydenko
那我可以在这里问另一个问题吗:到底是什么决定了一个函数是同步还是异步的?如果我想要编写一个同步函数和一个异步函数,我应该怎么做才能让它们不同呢?谢谢。 - Hang
1
@Hang 有关此主题的好文章是 MDN 关于 JavaScript 事件循环的文章(https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop)。基本上,您的 JS 中的所有内容都是同步运行的,但您可以执行异步事件侦听器(例如 setTimeout)。然后,脚本仅在下一个事件循环时返回,当特定事件被触发时 - 使您的代码异步化。 - jehna1
@jehna1非常感谢您提供的有用信息!我会好好查看的。 - Hang

3
回调函数是一种在异步进程完成后执行的函数。假设我们有一个带有以下数据的对象person.
var person = {
 id: 1034
 name: 'Julio',
 age: 23,
 moneyInBank: 0
}

我们想要获取这个人的银行存款金额 moneyInBank ,但我们在本地环境中无法访问该信息。我们需要从另一台服务器的数据库中获取它,这可能需要一些时间,具体取决于网络连接、优化等因素。 getMoneyInBank(person.id, callback) 会执行该过程并获取我们需要的信息。如果我们在没有回调函数的情况下运行下一个脚本。
getMoneyInBank(person.id);
console.log(person.money);

输出结果为0,原因是getMoneyInBank函数在日志记录时还没有执行完毕。若要确保输出有意义的信息,我们需要将代码更改成以下形式。

getMoneyInBank(persion.id, function(){
  console.log(person.money);
});

通过回调函数,只有在getMoneyInBank完成后才会调用日志。


谢谢您的回答!我可以确认一下,getMoneyInBank(person.id) 是同步还是异步的?因为如果它是同步的,那么它是否意味着它在当前堆栈中运行并阻塞操作?而如果它是同步的 - 我们将等待操作完成,实际上,我们将不会在输出中得到0。 - the_only_sa
另外一个问题,以帮助学习:对于 getMoneyInBank(person.id, function(){ console.log(person.money); }); 我理解匿名函数是回调函数。它将在 getMoneyInBank 完成后执行。getMoneyInBank 在当前堆栈中。一旦当前堆栈被清除,匿名函数将被回调。哪个函数是异步函数,哪个是同步函数(或两者都是异步的)?是 getMoneyInBank 还是匿名函数? - the_only_sa

2

您可以轻松检查以下内容:

function isSync(func, ...args){
 var isSync = false;
 const res = func(...args, r => isSync = true);
 if(res instanceof Promise) res.then(r => isSync = true);
 return isSync;
}

console.log( isSync([].map.bind([])) );

4
虽然它看起来很花哨,但我认为大多数人并不觉得这很简单。如果能解释一下会更好。 - Nandu Kalidindi

1
我不确定在这种情况下使用“同步”和“异步”是否有意义。当进行ajax请求时,可以谈论同步或异步,这意味着在同步请求中,在接收到响应之前,所有其他操作都会停止,而在异步请求中,直到服务器发送响应之前,其他操作可以继续进行。
在您的特定情况下(https://www.w3schools.com/jquery/jquery_callback.asp),发生的是:
- 在“slow”情况下,设置了超时,并在超时时调用回调函数。 - 在第二种情况下,为隐藏段落设置了超时,但该函数立即在下一行上调用,而不是等待时间结束后再调用。
这不是同步/异步,而是关于事件编程。

1

简短回答:您需要检查调用者并查找异步操作。

详细回答:

回调是一种简单的模式,其中将函数作为参数传递给高阶函数,并在其内部或更深层次的堆栈中被调用。

let mySynchronousFunction = () => 'Hello';
let myHigherOrderFunction = aFunc => {
    return (aFunc() || 'Goodbye') + ' world!';
};

一旦有会阻塞主线程的I/O操作,它将“分支出”并继续执行。
如果我们继续之前的示例,我们将观察到以下行为:
let myAsyncFunction = () => {
    return http.get('http://some.document')
       .then(response => console.log(response)
    );
};
myHigherOrderFunction(mySynchronousFunction); // 'Hello world!'
myHigherOrderFunction(myAsyncFunction); // 'Goodbye world!'

这里发生的是主线程一直执行到必须等待I/O时,它不会在此处阻塞,而是继续执行下一个指令,并记录当I/O操作不再被阻塞时需要执行的位置。
因此,在我们的代码中要评估的下一个表达式是:
return expression

但是我们的表达式分叉了,因此它返回未定义。因此,我们只剩下:
return undefined

这意味着我们传递了一个异步函数的高阶函数在调用aFunc()时得到了undefined。
一旦I/O完成,主线程返回到它离开的地方,即传递给Promise处理程序then的函数。此时,执行线程已分支并与主“线程”分离。
现在回答你的问题:
当调用它的高阶函数同步调用时,回调将是同步的。相反,如果它在异步操作的执行分支上下文中被调用,它将是异步的。
这意味着您需要检查传递回调的高阶函数正在做什么并查找异步操作。
在您的问题的背景下,让我们检查以下代码(HOF = 高阶函数):
let mySynchronousHOC = aFunc => aFunc('Hello world!');
let myAsyncHOC = aFunc => {
    http.get('http://hello.com')
        .then(response => aFunc('Goodbye world!')
    );
};

myAsyncHOC(msg => {
    console.log(msg);
});

mySynchronousHOC(msg => {
   console.log(msg);
});

其结果将为:
'Hello world'
'Goodbye world'

在这里,我们知道myAsyncHOC是异步的,因为它执行了一个I/O操作。


我能检查一下:我的异步函数 - 我猜测 "http.get('http://some.document')" 部分是在当前堆栈中调用的(如果它在当前堆栈中被调用,那么它是同步还是异步?),然后 ".then(response => console.log(response)" 是回调函数吗?它将在当前堆栈清除后被安排进队列中? - the_only_sa
另外一个问题(也许不相关,但只是为了理解):为什么有些 lambda 函数中有 "return",而有些则不需要。例如 mySynchronousFunction 和 myHigherOrderFunction。在使用 promises 时有返回和无返回的区别是什么? :) - the_only_sa
哇!!我真的很感激你提供了如此详细的答案(这是我在Stackoverflow上的第一个问题,社区给出如此详细的答案令人印象深刻)。 - the_only_sa
在当前堆栈中调用http.get(),但由于它使用语言的异步特性(xhr请求),因此会导致分支。 "then(...)"部分是分支部分,因此“then()”中的函数就像回调一样运行。它确实被安排了,但不是在清除当前堆栈时,而是在I/O操作解除阻塞后立即返回。在JavaScript中,无论它们是lambda还是其他类型的函数,都不需要使用return语句。当省略return语句时,将返回“undefined”。 - oLeduc

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