异步Javascript执行是如何发生的?什么时候不使用return语句?

时间:2011-08-18 08:27:46

标签: javascript function asynchronous return execution

// synchronous Javascript
var result = db.get('select * from table1');
console.log('I am syncronous');

// asynchronous Javascript 
db.get('select * from table1', function(result){
    // do something with the result
});
console.log('I am asynchronous')

我知道在同步代码中,console.log()在从db获取结果后执行,而在异步代码中,console.log()在db.get()获取结果之前执行。

现在我的问题是,异步代码的幕后执行是如何发生的?为什么它是非阻塞的?

我已经搜索了Ecmascript 5标准,以了解异步代码的工作原理,但在整个标准中找不到异步这个词。

从nodebeginner.org我也发现我们不应该使用return语句,因为它会阻止事件循环。但是nodejs api和第三方模块在任何地方都包含return语句。那么什么时候应该使用return语句呢?什么时候不应该使用它?

有人可以对此有所了解吗?

2 个答案:

答案 0 :(得分:29)

首先,将一个函数作为参数传递告诉你正在调用的函数,你希望它将来某个时候调用这个函数。如果将来它将被调用,取决于函数正在做什么的性质。

如果该功能正在进行某种联网并且该功能被配置为非阻塞或非同步,则该功能将执行,网络操作将启动,您调用的功能将立即返回,其余的内联该函数执行后的javascript代码。如果从该函数返回一个值,它将立即返回,早在您作为参数传递的函数被调用之前(网络操作尚未完成)。

与此同时,网络运营正在进行中。它正在发送请求,侦听响应,然后收集响应。当网络请求完成并收集了响应时,那么只有这样,您调用的原始函数才会调用您作为参数传递的函数。这可能只是几毫秒之后,也可能只是几分钟之后 - 取决于网络操作完成的时间。

重要的是要理解的是,在您的示例中,db.get()函数调用早已完成,代码也在其执行后依次执行。尚未完成的是您作为参数传递给该函数的内部匿名函数。这是在javascript函数关闭中保存,直到稍后网络函数完成。

我的观点是,让很多人感到困惑的一件事是匿名函数是在你对db.get的调用中声明的,并且似乎是其中的一部分,看来当db.get()完成时,这个也会这样做,但事实并非如此。如果它以这种方式表现出来,那或许看起来就像那样:

function getCompletionfunction(result) {
    // do something with the result of db.get
}

// asynchronous Javascript 
db.get('select * from table1', getCompletionFunction);

然后,也许更明显的是db.get将立即返回,并且getCompletionFunction将在未来的某个时间被调用。我并不是建议你这样写,而只是将这种形式展示为一种说明真实情况的方法。

这是一个值得理解的序列:

console.log("a");
db.get('select * from table1', function(result){
    console.log("b");
});
console.log("c");

您将在调试器控制台中看到的是:

a
c
b

“a”首先发生。然后,db.get()启动它的操作,然后立即返回。因此,接下来会发生“c”。然后,当db.get()操作实际上在将来某个时间完成时,“b”发生。


有关异步处理在浏览器中的工作原理的一些阅读,请参阅How does JavaScript handle AJAX responses in the background?

答案 1 :(得分:17)

jfriend00's answer解释了异步,因为它非常适用于大多数用户,但在您的评论中,您似乎想要了解有关实施的更多详细信息:

  

[...]任何机构都可以写一些伪代码,解释Ecmascript规范的实现部分来实现这种功能吗?为了更好地理解JS内部。

您可能知道,函数可以将其参数存入全局变量。假设我们有一个数字列表和一个添加数字的函数:

var numbers = [];
function addNumber(number) {
    numbers.push(number);
}

如果我添加一些数字,只要我指的是与以前相同的numbers变量,我就可以访问我之前添加的数字。

JavaScript实现可能会做类似的事情,除了存放数字,它们将函数(特别是回调函数)存放起来。

事件循环

许多应用程序的核心是所谓的事件循环。它基本上是这样的:

  • 永远循环:
    • 获取事件,如果不存在则阻止
    • 流程事件

假设您要执行类似问题的数据库查询:

db.get("select * from table", /* ... */);

为了执行该数据库查询,可能需要执行网络操作。由于网络操作可能需要花费大量时间,在此期间处理器正在等待,因此认为可能我们应该而不是等待而不是做其他工作是有道理的,只需让它告诉我们什么时候完成所以我们可以做同时还有其他事情。

为简单起见,我假装发送永远不会同步阻止/停止。

get的功能可能如下所示:

  • 为请求生成唯一标识符
  • 发送请求(再次,为简单起见,假设这不会阻止)
  • 在全局字典/哈希表变量中收起(标识符,回调)对

所有get都会这样做;它不会执行任何接收位,它本身也不负责调用您的回调。这发生在进程事件位中。进程事件位可能(部分)看起来像这样:

  • 是数据库响应的事件吗?如果是这样:
    • 解析数据库响应
    • 在哈希表中的响应中查找标识符以检索回调
    • 使用收到的回复调用回调

真实生活

在现实生活中,它有点复杂,但总体概念并没有太大差异。例如,如果要发送数据,则可能必须等到操作系统的传出网络缓冲区中有足够的空间才能添加数据。在读取数据时,您可能会以多个块的形式获取数据。进程事件位可能不是一个大函数,但它本身只是调用一堆回调(然后调度到更多的回调,依此类推......)

虽然现实生活和我们的例子之间的实现细节略有不同,但概念是相同的:你开始做“做某事”,并且当工作完成时,将通过某种机制调用回调。