如何返回异步调用的响应?
如何从发出异步请求的函数
foo
返回响应/结果?
我试图从回调中返回值,以及将结果分配给函数内的局部变量并返回该变量,但这些方法实际上都没有返回响应 - 它们都返回
undefined
或变量
result
的初始值。
接受回调的异步函数示例
(使用 jQuery 的
ajax
函数):
function foo() {
var result;
$.ajax({
url: '...',
success: function(response) {
result = response;
// return response; // <- I tried that one as well
}
});
return result; // It always returns `undefined`
}
使用 Node.js 的示例:
function foo() {
var result;
fs.readFile("path/to/file", function(err, data) {
result = data;
// return data; // <- I tried that one as well
});
return result; // It always returns `undefined`
}
使用承诺的
then
块的示例:
function foo() {
var result;
fetch(url).then(function(response) {
result = response;
// return response; // <- I tried that one as well
});
return result; // It always returns `undefined`
}
→ For a more general explanation of asynchronous behaviour with different examples, see Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
→ If you already understand the problem, skip to the possible solutions below.
问题
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 调用后的语句。为了最终获得响应,您提供了一个在收到响应后调用的函数,即 callback (注意到什么了吗? call back ?)。在该调用之后的任何语句都会在调用回调之前执行。
解决方案
拥抱 JavaScript 的异步特性! 虽然某些异步操作提供了同步对应项(“Ajax”也是如此),但通常不鼓励使用它们,尤其是在浏览器环境中。
你会问为什么它不好?
JavaScript 在浏览器的 UI 线程中运行,任何长时间运行的进程都会锁定 UI,使其无响应。此外,JavaScript 的执行时间有上限,浏览器会询问用户是否继续执行。
所有这些都会导致非常糟糕的用户体验。用户将无法判断一切是否正常。此外,对于连接速度较慢的用户来说,效果会更差。
接下来,我们将介绍三种不同的解决方案,它们都是相互叠加的:
-
带有
async/await
的 Promises(ES2017+,如果您使用转译器或再生器,则可在较旧的浏览器中使用) - 回调 (在节点中很流行)
-
带有
then()
的 Promises(ES2015+,如果您使用众多 Promise 库之一,则可在较旧的浏览器中使用)
这三种方法在当前浏览器和 Node 7 及以上版本中均可用。
ES2017+:带有
async/await
的 Promises>
2017 年发布的 ECMAScript 版本引入了对异步函数的
语法级支持
。借助
async
和
await
,您可以以“同步风格”编写异步代码。代码仍然是异步的,但更易于阅读/理解。
async/await
建立在 Promises 之上:
async
函数始终返回一个 Promise。
await
“解开” 一个承诺,并产生承诺被解决的值,或者在承诺被拒绝时抛出错误。
重要提示
:您只能在
async
函数内或
JavaScript 模块
中使用
await
。模块之外不支持顶级
await
,因此如果不使用模块,您可能必须创建异步 IIFE(
立即调用函数表达式
)来启动
async
上下文。
您可以在 MDN 上阅读有关
async
和
await
的更多信息。
下面是一个详细说明
delay
函数
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);
})();
当前
browser
和
node
版本支持
async/await
。您还可以借助
regenerator
(或使用 regenerator 的工具,例如
Babel
)将代码转换为 ES5,从而支持较旧的环境。
让函数接受 回调
回调是将函数 1 传递给函数 2。函数 2 可以在准备就绪时调用函数 1。在异步过程的上下文中,只要异步过程完成,就会调用回调。通常,结果会传递给回调。
在问题的示例中,您可以让
foo
接受回调并将其用作
success
回调。因此这个
var result = foo();
// Code that depends on 'result'
变成了
foo(function(result) {
// Code that depends on 'result'
});
这里我们定义了“inline”函数,但是您可以传递任何函数引用:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
foo
本身定义如下:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
将引用我们在调用
foo
时传递给它的函数,并将其传递给
success
。即,一旦 Ajax 请求成功,
$.ajax
将调用
callback
并将响应传递给回调(可以使用
result
引用,因为这是我们定义回调的方式)。
您还可以在将响应传递给回调之前对其进行处理:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
使用回调编写代码比看起来要容易。毕竟,浏览器中的 JavaScript 是高度事件驱动的(DOM 事件)。接收 Ajax 响应只不过是一个事件。 当您必须使用第三方代码时,可能会出现困难,但大多数问题都可以通过思考应用程序流程来解决。
ES2015+:使用 then() 实现的 Promise
Promise API 是 ECMAScript 6 (ES2015) 的一项新功能,但它已经具有良好的 浏览器支持 。还有许多库实现了标准 Promises API,并提供了额外的方法来简化异步函数的使用和组合(例如, bluebird )。
Promises 是 未来 值的容器。当承诺收到值( 已解决 )或被取消( 已拒绝 )时,它会通知所有想要访问此值的“侦听器”。
与普通回调相比,其优势在于它们允许您解耦代码,并且更易于编写。
以下是使用承诺的一个例子:
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; }
描述承诺提供的所有优势超出了本答案的范围,但如果您编写新代码,则应该认真考虑它们。它们为您的代码提供了出色的抽象和分离。
有关承诺的更多信息: HTML5 rocks - JavaScript Promises 。
附注:jQuery 的延迟对象
延迟对象 是 jQuery 对承诺的自定义实现(在 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
}
此代码误解了上述异步问题。具体来说,
$.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
对象,请将
false
作为第三个参数传递给
.open
。
jQuery
如果使用
jQuery
,可以将
async
选项设置为
false
。请注意,自 jQuery 1.8 以来,此选项已
弃用
。
然后,您仍然可以使用
success
回调或访问
jqXHR 对象
的
responseText
属性:
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
如果您使用任何其他 jQuery Ajax 方法,例如
$.get
、
$.getJSON
等,则必须将其更改为
$.ajax
(因为您只能将配置参数传递给
$.ajax
)。
注意! 无法发出同步 JSONP 请求。 JSONP 本质上始终是异步的(这是不考虑此选项的另一个原因)。
如果您在代码中 未 使用 jQuery,此答案适合您
您的代码应类似于以下内容:
function foo() {
var httpRequest = new XMLHttpRequest();
httpRequest.open('GET', "/echo/json");
httpRequest.send();
return httpRequest.responseText;
}
var result = foo(); // Always ends up being 'undefined'
Felix Kling 做得很好 ,为使用 jQuery 进行 AJAX 的人编写了一个答案,但我决定为不使用 jQuery 的人提供替代方案。
(
请注意,对于那些使用新的
fetch
API、Angular 或承诺的人,我添加了另一个答案(见下文
)
您面临的问题
这是另一个答案中“问题解释”的简短摘要,如果您在阅读此内容后不确定,请阅读该内容。
AJAX 中的
A
代表
异步
。这意味着发送请求(或者更确切地说接收响应)已脱离正常执行流程。在您的示例中,
.send
立即返回,并且下一个语句
return result;
在您作为
success
回调传递的函数被调用之前执行。
这意味着当您返回时,您定义的侦听器尚未执行,这意味着您返回的值尚未定义。
这是一个简单的类比:
function getFive(){
var a;
setTimeout(function(){
a=5;
},10);
return a;
}
返回的
a
的值为
undefined
,因为
a=5
部分尚未执行然而。AJAX 的行为方式是,在服务器有机会告诉浏览器该值是什么之前,您就返回了该值。
解决此问题的一个可能方法是 重新 编码,告诉程序在计算完成时该做什么。
function onComplete(a){ // When the code completes, do this
alert(a);
}
function getFive(whenDone){
var a;
setTimeout(function(){
a=5;
whenDone(a);
},10);
}
这称为
CPS
。基本上,我们向
getFive
传递一个操作,以便在它完成时执行,我们告诉代码在事件完成时如何反应(例如我们的 AJAX 调用,或者在本例中为超时)。
用法如下:
getFive(onComplete);
这应该会在屏幕上显示“5”。 (Fiddle) 。
可能的解决方案
基本上有两种方法可以解决这个问题:
- 使 AJAX 调用同步(我们称之为 SJAX)。
- 重构代码以正确使用回调。
1. 同步 AJAX - 不要这样做!!
至于同步 AJAX, 不要这样做! Felix 的回答提出了一些令人信服的论据,说明为什么这是一个坏主意。总而言之,它会冻结用户的浏览器,直到服务器返回响应并创建非常糟糕的用户体验。以下是来自 MDN 的另一个简短摘要,说明了原因:
XMLHttpRequest supports both synchronous and asynchronous communications. In general, however, asynchronous requests should be preferred to synchronous requests for performance reasons.
In short, synchronous requests block the execution of code... ...this can cause serious issues...
如果您 必须 这样做,您可以传递一个标志。 操作方法如下 :
var request = new XMLHttpRequest();
request.open('GET', 'yourURL', false); // `false` makes the request synchronous
request.send(null);
if (request.status === 200) {// That's HTTP for 'ok'
console.log(request.responseText);
}
2. 重构代码
让您的函数接受回调。在示例代码中,可以使
foo
接受回调。我们将告诉代码在
foo
完成时如何
做出反应
。
所以:
var result = foo();
// Code that depends on `result` goes here
变成:
foo(function(result) {
// Code that depends on `result`
});
在这里我们传递了一个匿名函数,但我们可以轻松地传递对现有函数的引用,使其看起来像:
function myHandler(result) {
// Code that depends on `result`
}
foo(myHandler);
有关如何完成此类回调设计的更多详细信息,请查看 Felix 的答案。
现在,让我们定义 foo 本身以采取相应的行动
function foo(callback) {
var httpRequest = new XMLHttpRequest();
httpRequest.onload = function(){ // When the request is loaded
callback(httpRequest.responseText);// We're calling our method
};
httpRequest.open('GET', "/echo/json");
httpRequest.send();
}
现在,我们已经让我们的 foo 函数接受在 AJAX 成功完成时运行的操作。我们可以通过检查响应状态是否为 200 并采取相应措施(创建失败处理程序等)来进一步扩展这一点。实际上,它正在解决我们的问题。
如果您仍然难以理解这一点,请 阅读 MDN 上的 AJAX 入门指南 。
XMLHttpRequest 2 (首先,请阅读 Benjamin Gruenbaum 和 Felix Kling 的答案)
如果您不使用 jQuery,并且想要一个在现代浏览器和移动浏览器中都能正常工作的简短 XMLHttpRequest 2,我建议您这样使用它:
function ajax(a, b, c){ // URL, callback, just a placeholder
c = new XMLHttpRequest;
c.open('GET', a);
c.onload = b;
c.send()
}
如您所见:
- 它比列出的所有其他函数都短。
- 回调是直接设置的(因此没有额外的不必要的闭包)。
- 它使用新的 onload(因此您不必检查 readystate && 状态)
- 还有一些我不记得的情况,使 XMLHttpRequest 1 令人讨厌。
有两种方法可以获取此 Ajax 调用的响应(三种使用 XMLHttpRequest 变量名称的方法):
最简单的:
this.response
或者,如果出于某种原因,您将回调
bind()
到类:
e.target.response
示例:
function callback(e){
console.log(this.response);
}
ajax('URL', callback);
或者(上面的更好匿名函数总是有问题):
ajax('URL', function(e){console.log(this.response)});
没有比这更简单的了。
现在有些人可能会说最好使用 onreadystatechange 甚至 XMLHttpRequest 变量名。这是错误的。
查看 XMLHttpRequest 高级功能 。
它支持所有*现代浏览器。我可以确认,自从 XMLHttpRequest 2 创建以来,我一直在使用这种方法。我在使用的任何浏览器中都没有遇到过任何类型的问题。
onreadystatechange 仅在您想要获取状态 2 上的标头时才有用。
使用
XMLHttpRequest
变量名是另一个大错误,因为您需要在 onload/oreadystatechange 闭包内执行回调,否则您将丢失它。
现在,如果您想要使用 POST 和 FormData 实现更复杂的东西,您可以轻松扩展此功能:
function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val},placeholder
c = new XMLHttpRequest;
c.open(e||'get', a);
c.onload = b;
c.send(d||null)
}
再次...这是一个非常短的函数,但它确实 GET 和 POST。
使用示例:
x(url, callback); // By default it's GET so no need to set
x(url, callback, 'post', {'key': 'val'}); // No need to set POST data
或者传递完整的表单元素 (
document.getElementsByTagName('form')[0]
):
var fd = new FormData(form);
x(url, callback, 'post', fd);
或者设置一些自定义值:
var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);
正如您所见,我没有实现同步......这是一件坏事。
话虽如此......我们为什么不简单地做呢?
正如评论中提到的那样,使用错误 && 同步确实完全破坏了答案的重点。哪种方法可以正确使用 Ajax?
错误处理程序
function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val}, placeholder
c = new XMLHttpRequest;
c.open(e||'get', a);
c.onload = b;
c.onerror = error;
c.send(d||null)
}
function error(e){
console.log('--Error--', this.type);
console.log('this: ', this);
console.log('Event: ', e)
}
function displayAjax(e){
console.log(e, this);
}
x('WRONGURL', displayAjax);
在上面的脚本中,您有一个静态定义的错误处理程序,因此它不会损害功能。错误处理程序也可以用于其他功能。
但是要抛出错误, 唯一 的方法是输入错误的 URL,这种情况下每个浏览器都会抛出错误。
如果您设置自定义标头、将 responseType 设置为 blob 数组缓冲区或其他任何内容,错误处理程序可能会有用...
即使您传递“POSTAPAPAP”作为方法,它也不会抛出错误。
即使您传递“fdggdgilfdghfldj”作为表单数据,它也不会抛出错误。
在第一种情况下,错误位于
this.statusText
下的
displayAjax()
中,为
Method not Allowed
。
在第二种情况下,它只是起作用。您必须在服务器端检查是否传递了正确的帖子数据。
不允许跨域会自动引发错误。
在错误响应中,没有任何错误代码。
只有
this.type
设置为
error
。
如果您无法控制错误,为什么要添加错误处理程序?
大多数错误都在回调函数
displayAjax()
中返回。
因此:如果您能够正确复制和粘贴 URL,则无需进行错误检查。;)
PS:作为第一个测试,我写了 x('x', displayAjax)...,它完全得到了响应......??? 所以我检查了 HTML 所在的文件夹,有一个名为“x.xml”的文件。因此,即使您忘记了文件的扩展名,XMLHttpRequest 2 也会找到它 。我大笑了
同步读取文件
不要那样做。
如果您想阻止浏览器一段时间,请同步加载一个很大的
.txt
文件。
function omg(a, c){ // URL
c = new XMLHttpRequest;
c.open('GET', a, true);
c.send();
return c; // Or c.response
}
现在您可以这样做
var res = omg('thisIsGonnaBlockThePage.txt');
没有其他方法可以非异步执行此操作。(是的,使用 setTimeout 循环......但真的吗?)
另一点是......如果您使用 API 或仅使用您自己的列表文件或任何您总是对每个请求使用不同函数的东西......
仅当您有一个始终加载相同 XML/JSON 或其他任何东西的页面时,您才只需要一个函数。在这种情况下,请稍微修改一下 Ajax 函数,并将 b 替换为您的特殊函数。
上述函数仅用于基本用途。
如果您想 扩展 该函数...
是的,您可以。
我使用了很多 API,我集成到每个 HTML 页面中的第一个函数是此答案中的第一个 Ajax 函数,仅使用 GET...
但您可以使用 XMLHttpRequest 2 做很多事情:
我制作了一个下载管理器(在两侧使用范围,包括 resume、filereader 和 filesystem)、使用画布制作了各种图像调整器转换器、使用 base64images 填充 Web SQL 数据库等等...
但在这些情况下,您应该只为此目的创建一个函数...有时您需要一个 blob 或数组缓冲区,您可以设置标题、覆盖 mimetype 等等...
但这里的问题是如何返回Ajax 响应...(我添加了一种简单的方法。)