如何从发出异步请求的函数 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`
}
使用 promise 的 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`
}
原文由 Felix Kling 发布,翻译遵循 CC BY-SA 4.0 许可协议
问题
Ajax 中的 A 代表 异步。这意味着发送请求(或者更确切地说是接收响应)从正常的执行流程中取出。在您的示例中,
$.ajax
立即返回,下一条语句return result;
, 在您作为success
回调传递的函数甚至被调用之前执行。这是一个类比,它有望使同步流和异步流之间的区别更加清晰:
同步
想象一下,您给朋友打了个电话,请他为您查找一些东西。虽然这可能需要一段时间,但您在电话上等待并凝视空间,直到您的朋友给您所需的答案。
当您进行包含“正常”代码的函数调用时,也会发生同样的情况:
尽管
findItem
可能需要很长时间才能执行,但var item = findItem();
之后的任何代码必须 等到 函数返回结果。异步
你出于同样的原因再次打电话给你的朋友。但是这次你告诉他你有事,他应该用你的手机 给你回 电话。你挂断电话,离开家,做你打算做的任何事情。一旦你的朋友给你回电话,你就在处理他给你的信息。
这正是您执行 Ajax 请求时发生的情况。
不是等待响应,而是立即继续执行,并执行 Ajax 调用之后的语句。为了最终获得响应,您提供了一个在收到响应后调用的函数,即 _回调_(注意什么? _回调_?)。在调用回调之前执行该调用之后的任何语句。
解决方案
拥抱 JavaScript 的异步特性! 虽然某些异步操作提供同步对应物(“Ajax”也是如此),但通常不鼓励使用它们,尤其是在浏览器上下文中。
你问为什么不好?
JavaScript 在浏览器的 UI 线程中运行,任何长时间运行的进程都会锁定 UI,使其无响应。此外,JavaScript 的执行时间有一个上限,浏览器会询问用户是否继续执行。
所有这些都会导致非常糟糕的用户体验。用户将无法判断一切是否正常。此外,对于连接速度较慢的用户,效果会更差。
在下文中,我们将研究三种不同的解决方案,它们都建立在彼此之上:
带有
async/await
的 Promise(ES2017+,如果您使用转译器或再生器,则在旧版浏览器中可用)回调(在节点中流行)
then()
的承诺(ES2015+,如果您使用众多承诺库之一,则可在较旧的浏览器中使用)所有这三个都在当前浏览器和节点 7+ 中可用。
ES2017+:
async/await
的承诺2017 年发布的 ECMAScript 版本引入了对异步函数的 _语法级支持_。在
async
和await
的帮助下,您可以以“同步风格”编写异步。代码仍然是异步的,但更容易阅读/理解。async/await
建立在 Promise 之上:async
函数总是返回一个 Promise。await
“解包” 一个promise 并导致promise 被解决的值或者如果promise 被拒绝则抛出一个错误。重要提示: 您只能在
async
函数或 JavaScript 模块 中使用await
。模块外部不支持顶级await
,因此如果不使用模块,您可能必须创建异步 IIFE( 立即调用函数表达式)来启动async
上下文。您可以在 MDN 上阅读有关
async
和await
的更多信息。这是一个详细说明上面的 延迟 函数
findItem()
的示例:当前的 浏览器 和 节点 版本支持
async/await
。您还可以通过在 regenerator (或使用 regenerator 的工具,例如 Babel )的帮助下将代码转换为 ES5 来支持旧环境。让函数接受 回调
回调是将函数 1 传递给函数 2。函数 2 可以在函数 1 准备就绪时调用它。在异步进程的上下文中,只要异步进程完成,就会调用回调。通常,结果被传递给回调。
在问题示例中,您可以让
foo
接受回调并将其用作success
回调。所以这变成
在这里,我们定义了函数“内联”,但您可以传递任何函数引用:
foo
本身定义如下:callback
将引用我们在调用时传递给foo
的函数,并将其传递给success
。即一旦 Ajax 请求成功,$.ajax
将调用callback
并将响应传递给回调(可以用result
引用,因为这是我们定义回调的方式)。您还可以在将响应传递给回调之前对其进行处理:
使用回调编写代码比看起来更容易。毕竟,浏览器中的 JavaScript 是高度事件驱动的(DOM 事件)。接收 Ajax 响应只不过是一个事件。
当您必须使用第三方代码时可能会出现困难,但大多数问题都可以通过考虑应用程序流程来解决。
ES2015+:使用 then() 进行 承诺
Promise API 是 ECMAScript 6 (ES2015) 的一个新特性,但它已经有很好的 浏览器支持。还有许多库实现了标准 Promises API 并提供了额外的方法来简化异步函数的使用和组合(例如 bluebird )。
Promise 是 未来 值的容器。当 Promise 接收到该值(它被 _解决_)或被取消( _被拒绝_)时,它会通知所有想要访问该值的“侦听器”。
与普通回调相比,它们的优势在于它们允许您解耦代码并且它们更易于编写。
下面是一个使用 Promise 的例子:
应用到我们的 Ajax 调用中,我们可以使用这样的 Promise:
描述承诺提供的所有优势超出了这个答案的范围,但如果你编写新代码,你应该认真考虑它们。它们为您的代码提供了很好的抽象和分离。
有关 Promises 的更多信息: HTML5 Rocks - JavaScript Promises 。
旁注:jQuery 的延迟对象
延迟对象 是 jQuery 对 Promise 的自定义实现(在 Promise API 标准化之前)。它们的行为几乎像 Promise,但暴露了稍微不同的 API。
jQuery 的每个 Ajax 方法都已经返回了一个“延迟对象”(实际上是一个延迟对象的承诺),您可以从函数中返回它:
旁注:承诺陷阱
请记住,承诺和延迟对象只是未来值的 _容器_,它们不是值本身。例如,假设您有以下内容:
此代码误解了上述异步问题。具体来说,
$.ajax()
在检查服务器上的“/密码”页面时不会冻结代码 - 它向服务器发送请求,在等待时,它会立即返回一个 jQuery Ajax Deferred 对象,而不是响应从服务器。这意味着if
语句将始终获取此 Deferred 对象,将其视为true
,并像用户登录一样继续进行。不好。但修复很简单:
不推荐:同步“Ajax”调用
正如我所提到的,一些(!)异步操作具有同步对应物。我不提倡使用它们,但为了完整起见,以下是执行同步调用的方式:
没有 jQuery
如果您直接使用
XMLHttpRequest
对象,请将false
作为第三个参数传递给.open
。jQuery
如果您使用 jQuery ,您可以将
async
选项设置为false
。请注意,此选项自 jQuery 1.8 起已 _弃用_。然后,您仍然可以使用
success
回调或访问 jqXHR 对象 的responseText
属性:如果您使用任何其他 jQuery Ajax 方法,例如
$.get
、$.getJSON
等,则必须将其更改为$.ajax
(因为您只能将配置参数传递给$.ajax
)。小心! 无法发出同步 JSONP 请求。 JSONP 就其本质而言始终是异步的(甚至不考虑此选项的另一个原因)。