→ 有关异步行为的更一般的解释以及不同的示例,请参阅 为什么我的变量在函数内部修改后没有改变? - 异步代码参考 https://stackoverflow.com/q/23667086/218196
→ 如果您已经了解该问题,请跳至下面的可能解决方案。
问题
The A in Ajax https://en.wikipedia.org/wiki/Ajax_(programming)代表异步 https://www.merriam-webster.com/dictionary/asynchronous。这意味着发送请求(或者更确切地说接收响应)被从正常执行流程中删除。在你的例子中,$.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();
has to wait直到函数返回结果。
异步
你出于同样的原因再次给你的朋友打电话。但这一次你告诉他你很着急,他应该打回给你在您的手机上。你挂断电话,离开家,做你计划做的事情。一旦你的朋友给你回电话,你就正在处理他给你的信息。
这正是您发出 Ajax 请求时所发生的情况。
findItem(function(item) {
// Do something with the item
});
doSomethingElse();
不等待响应,而是立即继续执行并执行 Ajax 调用之后的语句。为了最终获得响应,您提供一个在收到响应后调用的函数,callback(注意到什么了吗?打回来?)。该调用之后的任何语句都会在调用回调之前执行。
解决方案
拥抱 JavaScript 的异步特性!虽然某些异步操作提供同步对应项(“Ajax”也是如此),但通常不鼓励使用它们,尤其是在浏览器上下文中。
你问为什么不好?
JavaScript 在浏览器的 UI 线程中运行,任何长时间运行的进程都会锁定 UI,使其无响应。另外,JavaScript的执行时间是有上限的,浏览器会询问用户是否继续执行。
所有这些都会导致非常糟糕的用户体验。用户将无法判断一切是否正常。此外,对于连接速度较慢的用户来说,效果会更差。
下面我们将研究三种不同的解决方案,它们都是相互构建的:
-
承诺与
async/await
(ES2017+,如果您使用转译器或再生器,则可在旧版浏览器中使用)
-
回调(在节点中流行)
-
承诺与
then()
(ES2015+,如果您使用众多 Promise 库之一,则可在旧版浏览器中使用)
所有这三个都可以在当前浏览器和 Node 7+ 中使用。
ES2017+:承诺async/await https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
2017年发布的ECMAScript版本介绍语法级支持对于异步函数。在...的帮助下async
and await
,您可以以“同步风格”编写异步。代码仍然是异步的,但更容易阅读/理解。
async/await
建立在承诺之上:async
函数总是返回一个承诺。await
“解开”一个 Promise,要么产生 Promise 被解析的值,要么在 Promise 被拒绝时抛出错误。
重要的:你只能使用await
里面一个async
函数或在JavaScript 模块 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules。顶层await
模块外部不支持,因此您可能必须创建一个异步 IIFE (立即调用函数表达式 https://en.wikipedia.org/wiki/Immediately_invoked_function_expression)开始一个async
如果不使用模块,则使用上下文。
您可以阅读更多有关async https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function and await https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await on MDN.
这是一个详细说明的示例delay功能findItem()
above:
// 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);
})();
Current browser https://kangax.github.io/compat-table/es2016plus/#test-async_functions and node http://node.green/#ES2017-features-async-functions版本支持async/await
。您还可以通过将代码转换为 ES5 来支持旧环境再生器 https://github.com/facebook/regenerator(或使用再生器的工具,例如Babel https://babeljs.io/).
让函数接受回调
回调是指函数 1 传递给函数 2 时。函数 2 可以在函数 1 准备好时调用它。在异步进程的上下文中,只要异步进程完成,就会调用回调。通常,结果会传递给回调。
在问题的例子中,你可以foo
接受回调并将其用作success
打回来。所以这
var result = foo();
// Code that depends on 'result'
becomes
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
将引用我们传递给的函数foo
当我们调用它并将其传递给success
。 IE。一旦Ajax请求成功,$.ajax
将会通知callback
并将响应传递给回调(可以用result
,因为这就是我们定义回调的方式)。
您还可以在将响应传递给回调之前对其进行处理:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
使用回调编写代码比看起来更容易。毕竟,浏览器中的 JavaScript 很大程度上是事件驱动的(DOM 事件)。接收 Ajax 响应只不过是一个事件。
当您必须使用第三方代码时可能会出现困难,但大多数问题只需思考应用程序流程就可以解决。
ES2015+:承诺then() https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
The 承诺API https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise是 ECMAScript 6 (ES2015) 的新特性,但它有很好的浏览器支持 http://caniuse.com/#feat=promises已经。还有许多库实现了标准 Promises API 并提供了额外的方法来简化异步函数的使用和组合(例如,bluebird https://github.com/petkaantonov/bluebird).
Promise 是容器future价值观。当 Promise 收到值时(它是resolved)或取消时(rejected),它通知所有想要访问该值的“监听者”。
与普通回调相比的优点是它们允许您解耦代码并且更容易编写。
这是使用 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 调用时,我们可以使用这样的 Promise:
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 震撼人心 - JavaScript 的承诺 http://www.html5rocks.com/en/tutorials/es6/promises/.
旁注:jQuery 的延迟对象
延迟对象 https://stackoverflow.com/questions/4866721/what-are-deferred-objects是 jQuery 的 Promise 自定义实现(在 Promise API 标准化之前)。它们的行为几乎与 Promise 类似,但公开的 API 略有不同。
jQuery 的每个 Ajax 方法都已经返回一个“延迟对象”(实际上是延迟对象的承诺),您可以从函数中返回它:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
旁注:Promise 陷阱
请记住,承诺和延迟对象只是容器对于未来的价值,它们不是价值本身。例如,假设您有以下内容:
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
}
这段代码误解了上面的异步问题。具体来说,$.ajax()
在检查服务器上的“/password”页面时不会冻结代码 - 它会向服务器发送请求,在等待时,它会立即返回 jQuery Ajax Deferred 对象,而不是来自服务器的响应。这意味着if
语句总是会得到这个 Deferred 对象,将其视为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 https://xhr.spec.whatwg.org/对象、传递false
作为第三个参数.open https://xhr.spec.whatwg.org/#the-open()-method.
jQuery
如果你使用jQuery http://api.jquery.com/jQuery.ajax/,您可以设置async
选项false
。注意这个选项是已弃用从 jQuery 1.8 开始。
然后您仍然可以使用success
回调或访问responseText
的财产jqXHR 对象 http://api.jquery.com/jQuery.ajax/#jqXHR:
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
如果您使用任何其他 jQuery Ajax 方法,例如$.get
, $.getJSON
等等,你必须将其更改为$.ajax
(因为您只能将配置参数传递给$.ajax
).
小心!无法实现同步JSONP https://stackoverflow.com/questions/2067472/please-explain-jsonp要求。 JSONP 本质上始终是异步的(这是不考虑此选项的又一个原因)。