→ 有关使用不同示例对异步行为的更一般解释,请参阅为什么我的变量在函数内部修改后未更改?- 异步代码引用
→ 如果您已经了解问题,请跳到下面的可能解决方案。
问题
Ajax 中的 A 代表异步。这意味着发送请求(或者更确切地说是接收响应)将从正常的执行流中移除。在您的示例中,立即返回,下一个语句 , 在调用您作为回调传递的函数之前执行。$.ajax
return result;
success
下面是一个类比,希望它能使同步流和异步流之间的区别更清晰:
同步
想象一下,你给一个朋友打了个电话,让他帮你找点东西。虽然这可能需要一段时间,但你在电话上等待并盯着太空,直到你的朋友给你你需要的答案。
当您进行包含“正常”代码的函数调用时,也会发生同样的情况:
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
尽管执行可能需要很长时间,但之后的任何代码都必须等到函数返回结果。findItem
var item = findItem();
异步
出于同样的原因,你再次打电话给你的朋友。但这次你告诉他你很着急,他应该用手机给你回电话。你挂断电话,离开家,做任何你计划做的事情。一旦你的朋友给你回电话,你就是在处理他给你的信息。
这正是您执行Ajax请求时发生的情况。
findItem(function(item) {
// Do something with the item
});
doSomethingElse();
不是等待响应,而是立即继续执行,并在 Ajax 调用后执行语句。为了最终获得响应,您提供了一个函数,一旦收到响应就被调用,一个回调(注意什么?回电?调用之后的任何语句都将在调用回调之前执行。
解决方案
拥抱 JavaScript 的异步特性!虽然某些异步操作提供同步对应项(“Ajax”也是如此),但通常不鼓励使用它们,尤其是在浏览器上下文中。
你问为什么不好?
JavaScript在浏览器的UI线程中运行,任何长时间运行的进程都将锁定UI,使其无响应。此外,JavaScript 的执行时间有上限,浏览器会询问用户是否继续执行。
所有这些都会导致非常糟糕的用户体验。用户将无法判断一切是否正常工作。此外,对于连接速度较慢的用户,效果会更糟。
在下文中,我们将介绍三种不同的解决方案,它们都是相互叠加的:
-
带有
异步/等待
的承诺(ES2017+,如果您使用转译器或再生器,则在较旧的浏览器中可用)
-
回调(在节点中很受欢迎)
-
promise with
then()
(ES2015+,如果您使用众多 promise 库之一,可在较旧的浏览器中使用)
这三者都在当前的浏览器和节点7+中可用。
ES2017+:具有异步/等待
的承诺
2017 年发布的 ECMAScript 版本引入了对异步函数的语法级支持。在 和 的帮助下,您可以以“同步样式”编写异步。代码仍然是异步的,但它更容易阅读/理解。async
await
async/await
建立在承诺之上:函数总是返回承诺。 “解开”承诺,要么产生承诺被解析的值,要么在承诺被拒绝时抛出错误。async
await
重要:您只能在函数内部或 JavaScript 模块中使用。在模块之外不支持顶级,因此如果不使用模块,则可能必须创建异步 IIFE(立即调用的函数表达式)才能启动上下文。await
async
await
async
您可以在MDN上阅读有关异步
和等待
的更多信息。
下面是一个详细说明上述延迟函数的示例:findItem()
// Using 'superagent' which will return a promise.
var superagent = require('superagent')
// This is isn't declared as `async` because it already returns a promise
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
async function getAllBooks() {
try {
// GET a list of book IDs of the current user
var bookIDs = await superagent.get('/user/books');
// wait for 3 seconds (just for the sake of this example)
await delay();
// GET information about each book
return superagent.get('/books/ids='+JSON.stringify(bookIDs));
} catch(error) {
// If any of the awaited promises was rejected, this catch block
// would catch the rejection reason
return null;
}
}
// Start an IIFE to use `await` at the top level
(async function(){
let books = await getAllBooks();
console.log(books);
})();
当前浏览器和节点版本支持 。您还可以通过在再生器(或使用再生器的工具,如 Babel)的帮助下将代码转换为 ES5 来支持较旧的环境。async/await
让函数接受回调
回调是指将函数 1 传递给函数 2。函数 2 可以在函数 1 准备就绪时调用函数 1。在异步进程的上下文中,每当异步进程完成时,都会调用回调。通常,结果将传递给回调。
在问题的示例中,您可以使接受回调并将其用作回调。所以这个foo
success
var result = foo();
// Code that depends on 'result'
成为
foo(function(result) {
// Code that depends on 'result'
});
在这里,我们定义了函数“内联”,但您可以传递任何函数引用:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
foo
其本身定义如下:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
将引用我们在调用它时传递给它的函数,并将其传递给 。即,一旦Ajax请求成功,就会调用并将响应传递给回调(可以使用 来引用,因为这是我们定义回调的方式)。foo
success
$.ajax
callback
result
您还可以在将响应传递给回调之前对其进行处理:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
使用回调编写代码比看起来更容易。毕竟,浏览器中的JavaScript是高度事件驱动的(DOM事件)。接收Ajax响应只是一个事件。当您必须使用第三方代码时,可能会遇到困难,但大多数问题都可以通过思考应用程序流来解决。
Promise API 是 ECMAScript 6 (ES2015) 的新功能,但它已经具有良好的浏览器支持。还有许多库实现了标准的承诺API,并提供了其他方法来简化异步函数(例如,bluebird)的使用和组合。
承诺是未来价值的容器。当 promise 收到该值(已解析)或当它被取消(被拒绝)时,它会通知所有想要访问此值的“侦听器”。
与普通回调相比,它们的优点是它们允许您解耦代码,并且它们更易于编写。
下面是使用承诺的示例:
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
delay()
.then(function(v) { // `delay` returns a promise
console.log(v); // Log the value once it is resolved
})
.catch(function(v) {
// Or do something else if it is rejected
// (it would not happen in this example, since `reject` is not called).
});
.as-console-wrapper { max-height: 100% !important; top: 0; }
应用于我们的Ajax调用,我们可以使用这样的承诺:
function ajax(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve(this.responseText);
};
xhr.onerror = reject;
xhr.open('GET', url);
xhr.send();
});
}
ajax("https://jsonplaceholder.typicode.com/todos/1")
.then(function(result) {
console.log(result); // Code depending on result
})
.catch(function() {
// An error occurred
});
.as-console-wrapper { max-height: 100% !important; top: 0; }
描述承诺提供的所有优势超出了这个答案的范围,但是如果你编写新代码,你应该认真考虑它们。它们为代码提供了很好的抽象和分离。
有关 promise 的更多信息:HTML5 rocks - JavaScript Promises。
附注:jQuery 的延迟对象
延迟对象是 jQuery 的自定义 promise 实现(在 Promise API 标准化之前)。它们的行为几乎与承诺类似,但公开了略有不同的API。
jQuery的每个Ajax方法都已经返回一个“延迟对象”(实际上是延迟对象的承诺),您可以从函数返回:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
附注:承诺陷阱
请记住,承诺和延迟对象只是未来值的容器,它们不是值本身。例如,假设您有以下情况:
function checkPassword() {
return $.ajax({
url: '/password',
data: {
username: $('#username').val(),
password: $('#password').val()
},
type: 'POST',
dataType: 'json'
});
}
if (checkPassword()) {
// Tell the user they're logged in
}
此代码误解了上述异步问题。具体来说,在检查服务器上的“/password”页面时不会冻结代码 - 它向服务器发送请求,在等待时,它会立即返回jQuery Ajax Deferred对象,而不是来自服务器的响应。这意味着该语句将始终获取此延迟对象,将其视为 ,然后像用户登录一样继续。不好。$.ajax()
if
true
但修复方法很简单:
checkPassword()
.done(function(r) {
if (r) {
// Tell the user they're logged in
} else {
// Tell the user their password was bad
}
})
.fail(function(x) {
// Tell the user something bad happened
});
不建议:同步“Ajax”调用
正如我所提到的,一些(!)异步操作具有同步对应项。我不主张使用它们,但为了完整性,以下是执行同步调用的方式:
没有jQuery
如果直接使用 XMLHttpRequest
对象,请作为第三个参数传递给 .open
。false
jQuery
如果使用 jQuery,则可以将该选项设置为 。请注意,自 jQuery 1.8 起,此选项已弃用。然后,您仍然可以使用回调或访问 jqXHR 对象的属性:async
false
success
responseText
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
如果您使用任何其他jQuery Ajax方法,例如,等,则必须将其更改为(因为您只能将配置参数传递给 )。$.get
$.getJSON
$.ajax
$.ajax
小心!无法发出同步 JSONP 请求。JSONP本质上始终是异步的(甚至不考虑此选项的另一个原因)。