如何从异步调用返回响应?

新手上路,请多包涵

如何从发出异步请求的函数 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 许可协议

阅读 477
2 个回答

→ 有关不同示例的异步行为的更一般解释,请参阅 为什么我的变量在我在函数内部修改后没有改变? - 异步代码参考

→ 如果您已经了解问题,请跳至以下可能的解决方案。

问题

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 的执行时间有一个上限,浏览器会询问用户是否继续执行。

所有这些都会导致非常糟糕的用户体验。用户将无法判断一切是否正常。此外,对于连接速度较慢的用户,效果会更差。

在下文中,我们将研究三种不同的解决方案,它们都建立在彼此之上:

  • 带有 async/await 的 Promise(ES2017+,如果您使用转译器或再生器,则在旧版浏览器中可用)

  • 回调(在节点中流行)

  • then() 的承诺(ES2015+,如果您使用众多承诺库之一,则可在较旧的浏览器中使用)

所有这三个都在当前浏览器和节点 7+ 中可用。


ES2017+: async/await 的承诺

2017 年发布的 ECMAScript 版本引入了对异步函数的 _语法级支持_。在 asyncawait 的帮助下,您可以以“同步风格”编写异步。代码仍然是异步的,但更容易阅读/理解。

async/await 建立在 Promise 之上: async 函数总是返回一个 Promise。 await “解包” 一个promise 并导致promise 被解决的值或者如果promise 被拒绝则抛出一个错误。

重要提示: 您只能在 async 函数或 JavaScript 模块 中使用 await 。模块外部不支持顶级 await ,因此如果不使用模块,您可能必须创建异步 IIFE( 立即调用函数表达式)来启动 async 上下文。

您可以在 MDN 上阅读有关 asyncawait 的更多信息。

这是一个详细说明上面的 延迟 函数 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);
 })();

当前的 浏览器节点 版本支持 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'
 });

在这里,我们定义了函数“内联”,但您可以传递任何函数引用:

 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 API 是 ECMAScript 6 (ES2015) 的一个新特性,但它已经有很好的 浏览器支持。还有许多库实现了标准 Promises API 并提供了额外的方法来简化异步函数的使用和组合(例如 bluebird )。

Promise 是 未来 值的容器。当 Promise 接收到该值(它被 _解决_)或被取消( _被拒绝_)时,它会通知所有想要访问该值的“侦听器”。

与普通回调相比,它们的优势在于它们允许您解耦代码并且它们更易于编写。

下面是一个使用 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; }

描述承诺提供的所有优势超出了这个答案的范围,但如果你编写新代码,你应该认真考虑它们。它们为您的代码提供了很好的抽象和分离。

有关 Promises 的更多信息: HTML5 Rocks - JavaScript Promises

旁注:jQuery 的延迟对象

延迟对象 是 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
 });

旁注:承诺陷阱

请记住,承诺和延迟对象只是未来值的 _容器_,它们不是值本身。例如,假设您有以下内容:

 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() 在检查服务器上的“/密码”页面时不会冻结代码 - 它向服务器发送请求,在等待时,它会立即返回一个 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 就其本质而言始终是异步的(甚至不考虑此选项的另一个原因)。

原文由 Felix Kling 发布,翻译遵循 CC BY-SA 4.0 许可协议

如果您 没有 在代码中使用 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 for AJAX 的人写了一个很好 的答案,但我决定为那些不使用 jQuery 的人提供一个替代方案。

请注意,对于那些使用新的 fetch API、Angular 或 promises 的人,我在下面添加了另一个答案


你面对的是什么

这是来自另一个答案的“问题解释”的简短摘要,如果您在阅读本文后不确定,请阅读。

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”。 (小提琴)

可能的解决方案

基本上有两种方法可以解决这个问题:

  1. 使 AJAX 调用同步(我们称之为 SJAX)。
  2. 重组您的代码以正确使用回调。

1. 同步 AJAX - 不要这样做!!

至于同步AJAX, 千万别做! Felix 的回答引发了一些令人信服的论点,说明为什么这是个坏主意。总而言之,它会冻结用户的浏览器,直到服务器返回响应并造成非常糟糕的用户体验。这是另一个来自 MDN 的简短摘要,说明了原因:

XMLHttpRequest 支持同步和异步通信。然而,一般来说,出于性能原因,异步请求应该优于同步请求。

简而言之,同步请求会阻塞代码的执行…………这会导致严重的问题……

如果你 必须 这样做,你可以传递一个标志。 方法如下

 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 入门指南

原文由 Benjamin Gruenbaum 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题