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

问题描述 投票:37回答:2
// 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语句,何时不应该使用return语句?

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

javascript function asynchronous return execution
2个回答
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?


17
投票

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

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

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

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

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

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

事件循环

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

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

假设你想要像你的问题那样执行数据库查询:

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

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

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

get的功能可能如下所示:

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

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

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

现实生活

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

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

© www.soinside.com 2019 - 2024. All rights reserved.