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

IT技术 javascript function asynchronous return execution
2021-01-24 12:51:59
// 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 和第三方module到处都包含 return 语句。那么什么时候应该使用 return 语句,什么时候不应该呢?

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

2个回答

首先,将函数作为参数传递是告诉您正在调用的函数您希望它在将来某个时间调用此函数。确切地说,它何时会被调用取决于函数正在执行的操作的性质。

如果该函数正在执行一些网络操作并且该函数被配置为非阻塞或异步,则该函数将执行,网络操作将开始,您调用的函数将立即返回,其余的内联 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”发生。


有关异步处理如何在浏览器中工作的一些阅读材料,请参阅JavaScript 如何在后台处理 AJAX 响应?

“重要的是要了解,在您的示例中,db.get() 函数调用早已完成,并且代码也在执行之后按顺序执行。尚未完成的是您作为参数传递给该函数的内部匿名函数函数。它被保存在一个 javascript 函数闭包中,直到稍后网络函数完成。” 这是如何实现的。任何机构都可以编写一些伪代码,解释 Ecmascript 规范的实现部分以实现这种功能吗?为了更好地理解 JS 内部。
2021-03-20 12:51:59

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

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

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

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

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

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

事件循环

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

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

假设您想像您的问题一样执行数据库查询:

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

为了执行该数据库查询,它可能需要执行网络操作。由于网络操作可能需要大量时间,在此期间处理器正在等待,因此认为也许我们应该,与其等待而不是做一些其他工作,不如让它告诉我们何时完成,以便我们可以做在此期间的其他事情。

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

的功能get可能如下所示:

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

这就是全部get它不执行任何接收位,它本身也不负责调用您的回调。这发生在过程事件位。流程事件位可能(部分)如下所示:

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

现实生活

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

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