异步 JavaScript 简介
介绍
在 JavaScript 中,我们经常需要处理异步行为,这可能会让只具有同步代码经验的程序员感到困惑。本指南将解释什么是异步代码、使用异步代码的一些困难以及处理这些困难的方法。
同步代码
在同步程序中,如果您有两行代码(L1 后跟 L2),则 L2 必须在 L1 执行完毕后才能开始运行。
你可以想象一下,如果你在排队买火车票。你必须等到你前面的人都买完票后才能开始买火车票。同样,你后面的人也必须等到你买完票后才能开始买票。
异步代码
在异步程序中,您可以有两行代码(L1 后跟 L2),其中 L1 安排某项任务在将来运行,但 L2 在该任务完成之前运行。
你可以想象自己在一家餐厅吃饭。其他人点菜。你也可以点菜。你不必等他们拿到食物并吃完再点菜。同样,其他人也不必等你拿到食物并吃完再点菜。食物一煮好,每个人都可以拿到食物。
人们收到食物的顺序通常与他们点菜的顺序相关,但这些顺序并不总是必须相同。例如,如果你点了一份牛排,然后我点了一杯水,我可能会先收到我的订单,因为通常上一杯水所花的时间并不像准备和上一份牛排那么长。
请注意,异步并不意味着并发或多线程。JavaScript可以有异步代码,但它通常是单线程的。这就像一家餐馆,只有一个工人负责等待和烹饪。但如果这名工人工作速度足够快,并且可以足够高效地在任务之间切换,那么这家餐馆似乎就有多名工人。
示例
setTimeout函数可能是异步安排代码在未来运行的最简单的方法:
// Say "Hello."
console.log("Hello.");
// Say "Goodbye" two seconds from now.
setTimeout(function() {
console.log("Goodbye!");
}, 2000);
// Say "Hello again!"
console.log("Hello again!");
如果您只熟悉同步代码,您可能会期望上面的代码按以下方式运行:
- 问好”。
- 两秒钟内什么也不要做。
- 告别!”
- 说“再次问好!”
但是setTimeout不会暂停代码的执行。它只是安排一些将来要发生的事情,然后立即继续下一行。
- 问好。”
- 说“再次问好!”
- 两秒钟内什么也不要做。
- 告别!”
从 AJAX 请求获取数据
同步代码和异步代码的行为之间的混淆是初学者在处理 JavaScript 中的 AJAX 请求时经常遇到的问题。他们通常会编写如下所示的 jQuery 代码:
function getData() {
var data;
$.get("example.php", function(response) {
data = response;
});
return data;
}
var data = getData();
console.log("The data is: " + data);
从同步的角度来看,这并不像您预期的那样。与上例中的setTimeout类似, $.get不会暂停代码的执行,它只是安排一些代码在服务器响应后运行。这意味着return data;行将在data = response之前运行,因此上面的代码将始终打印“数据为:未定义”。
异步代码需要以与同步代码不同的方式来构造,而最基本的方法是使用回调函数。
回调函数
假设你打电话给你的朋友并向他询问一些信息,比如你忘记了的共同朋友的邮寄地址。你的朋友没有记住这些信息,所以他必须找到他的通讯录并查找地址。这可能需要几分钟。你可以采取不同的策略来处理:
- (同步)你继续和他通话,等待他查看。
- (异步)你告诉朋友,一旦他得到信息后就给你回电话。同时,你可以把所有的注意力集中在需要完成的其他任务上,比如叠衣服和洗碗。
在 JavaScript 中,我们可以创建一个回调函数,将其传递给异步函数,一旦任务完成,该函数就会被调用。
也就是说,
var data = getData();
console.log("The data is: " + data);
我们将传递一个回调函数给getData:
getData(function(data) {
console.log("The data is: " + data);
});
当然,getData如何知道我们传入的是一个函数?它是如何被调用的,数据参数又是如何填充的?目前,这些都没有发生;我们还需要更改getData函数,以便它知道回调函数是它的参数。
function getData(callback) {
$.get("example.php", function(response) {
callback(response);
});
}
您会注意到,我们之前已经将一个回调函数传递给了$.get,也许您没有意识到它是什么。在第一个示例中,我们还将一个回调传递给了setTimeout(callback, delay)函数。
由于$.get已经接受回调,我们不需要在getData中手动创建另一个回调,我们可以直接传入我们给出的回调:
function getData(callback) {
$.get("example.php", callback);
}
回调函数在 JavaScript 中使用非常频繁,如果您花了很多时间用 JavaScript 编写代码,那么很有可能您已经使用过它们(也许是无意中)。几乎所有 Web 应用程序都会使用回调,无论是通过事件(例如window.onclick)、setTimeout和setInterval,还是 AJAX 请求。
常见问题:尝试完全避免使用异步代码
有些人认为处理异步代码太复杂,所以他们尝试让所有事情都同步。例如,你可以创建一个同步函数来代替使用setTimeout,让它在一定时间内不执行任何操作:
function pause(duration) {
var start = new Date().getTime();
while (new Date().getTime() - start < duration);
}
类似地,在执行 AJAX 调用时,可以设置选项以使调用同步而不是异步(尽管此选项正在逐渐失去浏览器支持)。Node.js 中许多异步函数也有同步替代方案。
在 JavaScript 中,尝试避免使用异步代码并将其替换为同步代码几乎总是一个坏主意,因为 JavaScript 只有一个线程(使用 Web Workers 时除外)。这意味着在脚本运行时网页将无响应。如果您使用上面的同步暂停功能或同步 AJAX 调用,则用户在运行时将无法执行任何操作。
使用服务器端 JavaScript 时,问题会更加严重:服务器在等待同步函数完成时将无法响应任何请求,这意味着每个向服务器发出请求的用户都必须等待才能得到响应。
常见问题:循环内回调的作用域问题
在 for 循环内创建回调时,您可能会遇到一些意外行为。考虑一下您希望下面的代码执行什么操作,然后尝试在浏览器的 JavaScript 控制台中运行它。
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i + " second(s) elapsed");
}, i * 1000);
}
上述代码可能旨在输出以下消息,每条消息之间有一秒的延迟:
1 second(s) elapsed.
2 second(s) elapsed.
3 second(s) elapsed.
但代码实际上输出以下内容:
4 second(s) elapsed.
4 second(s) elapsed.
4 second(s) elapsed.
问题在于console.log(i + " second(s) elapsed");位于异步函数的回调中。当它运行时,for 循环已经终止,变量i将等于4。
这个问题有多种解决方法,但最常见的方法是将对setTimeout的调用包装在一个闭包中,这将在每次迭代中创建一个具有不同i 的新范围:
for (var i = 1; i <= 3; i++) {
(function(i) {
setTimeout(function() {
console.log(i + " second(s) elapsed");
}, i * 1000);
})(i);
}
如果您使用的是 ECMAScript6 或更高版本,那么更优雅的解决方案是使用let而不是var,因为let在每次迭代中都会为i创建一个新的作用域:
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i + " second(s) elapsed");
}, i * 1000);
}
常见问题:回调地狱
有时,你有一系列任务,其中每个步骤都依赖于前一步的结果。在同步代码中,这是非常容易处理的事情:
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~