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

6666

如何从一个执行异步请求的函数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`
}

使用deasync的方法如下: https://dev59.com/DGAg5IYBdhLWcg3wq8aU#47051880 - Sunil Kumar
17
@SunilKumar,我认为这并没有什么用处。OP发布这个问题和自我回答是为了记录如何获取异步调用的响应。建议使用第三方模块会破坏这一目的,并且在我看来,该模块引入的编程范例并不是一个好的实践。 - Seblor
6
@Liam:这只是一个接受回调函数的异步函数示例。 - Felix Kling
承诺提供了一种更清晰、更有结构的方式来处理异步操作。您可以创建一个代表异步调用结果的承诺。该承诺可以通过响应来解决,也可以通过错误来拒绝。 - Trishant Saxena
42个回答

6602

→ 如需更一般的异步行为解释和示例,请参见 为什么在函数内修改变量后该变量没有改变?- 异步代码参考

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

问题

Ajax 中的A代表异步。这意味着发送请求(或者更确切地说,接收响应)是在正常执行流之外进行的。在您的示例中,$.ajax 立即返回,并且在调用作为success回调传递的函数之前,下一条语句return result;已经被执行。

以下类比希望能更清晰地说明同步和异步流之间的区别:

同步

想象一下你给朋友打电话,要求他帮你查找某些信息。虽然可能需要一段时间,但你会一直等待电话并盯着空间,直到朋友给你需要的答案。

当你调用包含“正常”代码的函数时,同样的情况也会发生:

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+,如果使用转换器或 regenerator,也可在旧版浏览器中使用)
  • 回调(在 node 中很受欢迎)
  • 带有 then() 的 Promise(ES2015+,如果使用许多 Promise 库之一,也可在旧版浏览器中使用)

所有三个在现代浏览器和Node 7+中都可用。


ES2017+: 使用async/await的Promises

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

async/await建立在Promise之上:一个async函数始终返回一个Promise。 await“展开”一个Promise,并且要么得到Promise解决时的值,要么如果Promise被拒绝则会抛出错误。

重要提示:你只能在JavaScript模块中或者async函数内部使用await。顶层的await不支持在模块外部,所以如果不使用模块,你可能需要创建一个异步IIFE(立即调用函数表达式)来启动async上下文。

你可以在MDN上了解更多关于asyncawait的内容。

这里有一个例子详细说明了上面的findItem()函数的delay功能:

// 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);
})();

当前browsernode版本支持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()的Promises

Promise API是ECMAScript 6(ES2015)的新功能,但它已经得到很好的浏览器支持。还有许多库实现了标准的Promise API,并提供了其他方法来简化异步函数的使用和组合(例如bluebird)。

Promise是存放未来值的容器。当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 提供的所有优势超出了本答案的范围,但如果您编写新代码,应认真考虑它们。它们提供了很好的抽象和代码分离。
更多关于 Promise 的信息:HTML5 rocks - JavaScript Promises
注:jQuery 的延迟对象 Deferred objects 是 jQuery 对 Promise 的自定义实现(在 Promise API 标准化之前)。它们的行为与 Promise 几乎相同,但公开了稍微不同的 API。
jQuery 的每个 Ajax 方法已经返回一个“deferred object”(实际上是一个延迟对象的 promise),您可以直接从函数中返回它:
function ajax() {
    return $.ajax(...);
}

ajax().done(function(result) {
    // Code depending on result
}).fail(function() {
    // An error occurred
});

顺带一提:Promise的陷阱

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

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延迟对象,而不是服务器的响应。这意味着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 对象,将 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的本质始终是异步的(这是不考虑此选项的另一个原因),因此请注意。

95
如果您想使用jQuery,您需要将其包含在内。请参考http://docs.jquery.com/Tutorials:Getting_Started_with_jQuery。 - Felix Kling
23
在解决方案1中,子jQuery部分,我无法理解这一行:“如果您使用任何其他的jQuery AJAX方法,比如$.get、$.getJSON等,都可以转换为$.ajax。”(是的,我意识到在这种情况下我的昵称有点讽刺了) - cssyphus
47
嗯,我不知道怎么能让它更清晰了。你看到了吗,foo被调用并传递了一个函数给它(foo(function(result) {....});)?result在这个函数内部被使用,并且是Ajax请求的响应。为了引用这个函数,foo的第一个参数被称为回调函数并赋值给success而不是一个匿名函数。所以,当请求成功时,$.ajax将会调用callback。我试着更详细地解释了一下。 - Felix Kling
57
这个问题的聊天室已经没有活动了,所以我不确定在哪里提出这些修改建议,但是我建议:1)将同步部分改为简单讨论为什么它不好,不要提供任何如何做的代码示例。2)删除/合并回调示例,只展示更灵活的 Deferred 方法,我认为这对学习JavaScript的人也更容易理解。 - Chris Moschini
23
@Jessi: 我认为你误解了答案中的那部分内容。如果你希望 Ajax 请求是同步的,就不能使用 $.getJSON。然而,你甚至不应该希望请求是同步的,所以这并不适用。你应该使用回调函数或 Promises 来处理响应,正如答案中早先解释的那样。 - Felix Kling
显示剩余10条评论

1220

如果你的代码中没有使用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或者Promise,我已经添加了另一个答案,请看下面)


问题概述

这是对其他答案中“问题解释”的简要总结,如果你阅读后仍然不确定,请参考原答案。

AJAX中的A代表异步。这意味着发送请求(或者更确切地说,接收响应)是在常规执行流程之外处理的。在你的例子中,.send会立即返回,并且在传递给success回调函数的函数被调用之前,下一条语句return result;就已经执行了。

这意味着当你返回时,你定义的监听器尚未执行,这意味着你返回的值尚未被定义。

这里有一个简单的比喻:

function getFive(){
    var a;
    setTimeout(function(){
         a=5;
    },10);
    return a;
}

(Fiddle)

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).

可能的解决方案

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

  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();
}

(fiddle)

现在我们使我们的foo函数接受一个成功完成AJAX时要运行的动作。我们可以进一步扩展它,通过检查响应状态是否不是200来采取相应的行动(创建一个失败处理程序等)。有效地解决了我们的问题。

如果您仍然很难理解这一点,请在MDN上阅读AJAX入门指南


32
同步请求会阻塞代码执行并可能导致内存泄漏和事件泄漏。同步请求如何会导致内存泄漏? - Matthew G

453

XMLHttpRequest 2(首先,请阅读Benjamin GruenbaumFelix 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()
}

如您所见:

  1. 它比列出的所有其他函数都要短。
  2. 回调被直接设置(因此没有额外的不必要的闭包)。
  3. 它使用了新的 onload (因此您不必检查 readystate && status)。
  4. 还有一些我不记得的其他情况,这些情况使 XMLHttpRequest 1 变得很恼人。

有两种方法可以获取此 Ajax 调用的响应(使用 XMLHttpRequest 变量名称有三种方法):

最简单的是:

this.response

或者如果由于某些原因,您将回调函数绑定到了一个类:

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被创建以来一直在使用这种方法。我在使用的任何浏览器中从未遇到任何问题。

如果你想在状态2时获取头信息,onreadystatechange才有用。

使用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”作为表单数据传递,它也不会抛出错误。
在第一种情况下,错误位于displayAjax()中的this.statusText下,显示为“Method not Allowed”。
在第二种情况下,它只是起作用。您必须在服务器端检查是否传递了正确的发布数据。
跨域不允许自动抛出错误。
在错误响应中,没有任何错误代码。
只有设置为errorthis.type

如果您完全无法控制错误,为什么要添加错误处理程序?大多数错误都在回调函数displayAjax()中返回。

所以:如果您能够正确复制和粘贴URL,则没有必要进行错误检查。 ;)

附注:作为第一个测试,我编写了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执行很多操作:
我制作了一个下载管理器(使用恢复时的两侧范围,filereader和filesystem),各种图像调整大小器转换器(使用画布),使用base64images填充Web SQL数据库等等...
但在这些情况下,您应该仅为此创建一个函数...有时需要blob、array buffers,您可以设置标头,覆盖MIME类型等等...
但问题在于如何返回Ajax响应...(我添加了一种简单的方法。)

22
虽然这个答案不错(我们都喜欢XHR2并且发布文件数据和多部分数据真的很棒),但是它只是展示如何使用JavaScript进行XHR的语法糖。你可能想把这个放在博客文章中(我会很喜欢),甚至放在一个库中(不确定xajaxxhr哪个更好:))。我不明白它如何返回从AJAX调用中获取到的响应。(有人可能仍然会执行var res = x("url"),并不理解为什么它不能正常工作)。另外,如果方法返回c,用户可以挂接到error等上面,这将很酷。 - Benjamin Gruenbaum
33
  1. Ajax旨在异步进行,因此不要使用var res=x('url')这样的同步调用。这就是这个问题和答案的全部意义 :)
- Benjamin Gruenbaum
21
@cocco你在SO的答案中写了误导性、难以阅读的代码,只是为了省下一些按键?请不要这样做。 - stone

374

如果你正在使用Promise,这篇答案适合你。

这意味着AngularJS、jQuery(使用deferred)、原生的XHR替代品(fetch)、Ember.jsBackbone.js的保存或任何返回Promise的Node.js库。

你的代码应该是这样的:

function foo() {
    var data;
    // Or $.get(...).then, or request(...).then, or query(...).then
    fetch("/echo/json").then(function(response){
        data = response.json();
    });
    return data;
}

var result = foo(); // 'result' is always undefined no matter what.

Felix Kling 做得很好,为使用带有回调函数的 Ajax 的人编写了一个答案。我有一个关于原生 XHR 的答案。这个答案适用于前端或后端通用的 Promise。


核心问题

在浏览器和服务器上,JavaScript并发模型是异步响应式的

每当调用返回一个promise的方法时,then处理程序总是异步执行——也就是说,在它们下面不在.then处理程序中的代码之后

这意味着当你返回data时,你定义的then处理程序尚未执行。这反过来意味着你返回的值没有及时设置为正确的值。

以下是该问题的简单类比:

    function getFive(){
        var data;
        setTimeout(function(){ // Set a timer for one second in the future
           data = 5; // After a second, do this
        }, 1000);
        return data;
    }
    document.body.innerHTML = getFive(); // `undefined` here and not 5

data的值是undefined,因为data = 5部分尚未执行。它可能会在一秒钟内执行,但到那时对于返回的值来说已经不相关了。
由于操作尚未发生(Ajax、服务器调用、I/O和计时器),您正在返回请求还没有机会告诉您的代码该值是什么的值。
解决此问题的一种可能方法是编写响应式代码,告诉程序在计算完成后要执行什么操作。Promises通过其时间敏感性积极地实现这一点。
关于promises的快速回顾
Promise是一个随时间变化的。Promises具有状态。它们开始作为无值的待定状态,并可以解决为:
  • 已完成表示计算成功完成。
  • 被拒绝表示计算失败。
一个Promise只能在状态改变一次后,永远保持相同的状态。您可以附加then处理程序来提取其值并处理错误。then处理程序允许调用的链接。Promise是通过返回它们的API创建的。例如,更现代的Ajax替代方案fetch或jQuery的$.get会返回Promise。
当我们在Promise上调用.then并从中返回某些内容时,我们将获得一个关于处理值的Promise。如果我们返回另一个Promise,我们将获得惊人的结果,但让我们先慢下来。

使用Promises

让我们看看如何使用Promise解决上述问题。首先,我们可以通过使用Promise构造函数创建延迟函数来展示我们对Promise状态的理解:

function delay(ms){ // Takes amount of milliseconds
    // Returns a new promise
    return new Promise(function(resolve, reject){
        setTimeout(function(){ // When the time is up,
            resolve(); // change the promise to the fulfilled state
        }, ms);
    });
}

现在,我们将转换setTimeout以使用promises后,可以使用then让它计数:

function delay(ms){ // Takes amount of milliseconds
  // Returns a new promise
  return new Promise(function(resolve, reject){
    setTimeout(function(){ // When the time is up,
      resolve(); // change the promise to the fulfilled state
    }, ms);
  });
}

function getFive(){
  // We're RETURNING the promise. Remember, a promise is a wrapper over our value
  return delay(100).then(function(){ // When the promise is ready,
      return 5; // return the value 5. Promises are all about return values
  })
}
// We _have_ to wrap it like this in the call site, and we can't access the plain value
getFive().then(function(five){
   document.body.innerHTML = five;
});

基本上,由于并发模型的限制,我们无法返回一个值,所以我们返回一个值的包装器,可以使用 then 解开。就像一个你可以用 then 打开的盒子。

应用

对于您的原始 API 调用,情况是相同的,您可以:

function foo() {
    // RETURN the promise
    return fetch("/echo/json").then(function(response){
        return response.json(); // Process it inside the `then`
    });
}

foo().then(function(response){
    // Access the value inside the `then`
})

所以这同样有效。我们已经学会了无法从已经异步调用的函数返回值,但是我们可以使用Promise并将它们链接起来执行处理。我们现在知道如何从异步调用中返回响应。

ES2015(ES6)

ES6引入了生成器,它们是可以在中途返回然后恢复到它们所在的点的函数。这通常对于序列非常有用,例如:

function* foo(){ // Notice the star. This is ES6, so new browsers, Nodes.js, and io.js only
    yield 1;
    yield 2;
    while(true) yield 3;
}

这是一个返回迭代器的函数,该迭代器可以遍历序列1,2,3,3,3,3,....。尽管这本身很有趣并且开启了许多可能性,但有一种特别有趣的情况。
如果我们生成的序列是一个动作序列而不是数字序列 - 我们可以在产生动作时暂停函数并等待它,然后再恢复函数。因此,我们需要的不是数字序列,而是未来值的序列 - 即承诺。
这个有点棘手,但非常强大的技巧让我们以同步的方式编写异步代码。有几个“运行器”可以为您完成此操作。编写一个只需几行代码,但超出了本答案的范围。我将在此处使用Bluebird的Promise.coroutine,但还有其他包装器,如coQ.async
var foo = coroutine(function*(){
    var data = yield fetch("/echo/json"); // Notice the yield
    // The code here only executes _after_ the request is done
    return data.json(); // 'data' is defined
});

该方法本身返回一个Promise,我们可以从其他协程中使用它。例如:
var main = coroutine(function*(){
   var bar = yield foo(); // Wait our earlier coroutine. It returns a promise
   // The server call is done here, and the code below executes when done
   var baz = yield fetch("/api/users/" + bar.userid); // Depends on foo's result
   console.log(baz); // Runs after both requests are done
});
main();

ES2016 (ES7)

在ES7中,这进一步被标准化。目前有几个提案,但在所有提案中,您都可以使用await promise。这只是通过添加asyncawait关键字来改进ES6提案的"语法糖"(更好的语法)。将上面的示例修改为:

async function foo(){
    var data = await fetch("/echo/json"); // Notice the await
    // code here only executes _after_ the request is done
    return data.json(); // 'data' is defined
}

它仍然返回一个承诺,完全一样 :)

简而言之,异步函数可以:1)将无意义的“pending”承诺或“undefined”变量分配给同步流程(常见的陷阱)2)从已实现的承诺中提取响应体以供自己或回调处理3)将承诺返回到另一个调用异步函数的“链”上。 - DWB

288

您正在错误地使用Ajax。其思想不是要返回任何内容,而是将数据交给称为回调函数的东西处理数据。

也就是说:

function handleData( responseData ) {

    // Do what you want with the data
    console.log(responseData);
}

$.ajax({
    url: "hi.php",
    ...
    success: function ( data, status, XHR ) {
        handleData(data);
    }
});

在提交处理程序中返回任何内容都不会产生任何作用。相反,您必须将数据移交给其他处理程序,或直接在成功函数内进行所需操作。


21
这个答案完全是语义上的……你的成功方法只是一个回调函数中的回调函数。你可以直接使用success: handleData,它也能正常工作。 - Jacques ジャック

282
我会用一张手绘的漫画来回答。第二幅图是你代码示例中 result 为何为 undefined 的原因。

enter image description here


47
一张图片胜过千言万语。人A向人B请教汽车维修的详细信息,人B发起了Ajax调用并等待服务器返回汽车修理的详细信息。当收到响应后,Ajax成功函数会调用人B函数,并将响应作为参数传递给它,人A最终收到答案。 - Shaiju T
24
如果您能在每张图片下添加代码行以说明概念,那就太好了。 - Hassan Baig
12
与此同时,那位开车的人被困在路边。他需要修好车才能继续前行。现在他独自一人在路边等待……他更愿意在电话上等待状态的变化,但是修车工不会这样做……修车工说他必须继续工作,不能简单地待在电话旁。修车工承诺会尽快回电话。大约4个小时后,这个人放弃了,打电话叫了Uber。- 超时的例子。 - barrypicker
但是使用回调函数,我感觉最后一帧左边的人被迫不给对方他们的电话号码。相反,他们必须告诉对方,“这是我想要用来处理从电话那边得到的信息的所有内容。请完成所有这些操作并且不要再告诉我。”我错过了什么吗? - Dan Narsavage
2
@FingLixon 这并不是一部完美的漫画 :-D。第二张图片应该说明当您尝试在回调发生之前读取值时会发生什么。第三张图片说明了设置回调方法的过程:左边的那个人基本上就是回调处理程序:一旦信息可用,他将被调用并可以随意处理它。我现在认为在这个漫画中有两个电话是一个坏主意:打给商店和打给左边的人。我应该简化它,对此感到抱歉。 - Johannes Fahrenkrug

264

最简单的解决方案是创建一个JavaScript函数,并在Ajax的success回调中调用它。

function callServerAsync(){
    $.ajax({
        url: '...',
        success: function(response) {

            successCallback(response);
        }
    });
}

function successCallback(responseObj){
    // Do something like read the response and show data
    alert(JSON.stringify(responseObj)); // Only applicable to a JSON response
}

function foo(callback) {

    $.ajax({
        url: '...',
        success: function(response) {
           return callback(null, response);
        }
    });
}

var result = foo(function(err, result){
          if (!err)
           console.log(result);
});

11
我不知道谁投了反对票。但是这是一个可行的解决方法,事实上我用这种方法创建了一个完整的应用程序。jquery.ajax无法返回数据,因此最好使用上述方法。如果有问题,请解释并建议更好的方法。 - Hemant Bavle
20
抱歉,我忘记留下评论了(我通常都会)。我给它点了踩。点踩不表示事实的正确与否,而是表示其在特定情境下是否有用。考虑到Felix已经详细解释了这个问题,我认为你的回答并不实用。顺便问一句,如果返回结果是JSON格式,为什么要将其字符串化? - Benjamin Gruenbaum
11
好的,@Benjamin,我使用 stringify 将 JSON 对象转换为字符串。感谢您澄清了您的观点。我会记住发布更详细的答案。 - Hemant Bavle

183

Angular 1

使用AngularJS的人可以使用promises来处理这种情况。

这里说到,

Promise 可用于取消异步函数并允许将多个函数链接在一起。

您还可以在此处找到一个很好的解释。

下面是文档中发现的一个例子。

  promiseB = promiseA.then(
    function onSuccess(result) {
      return result + 1;
    }
    ,function onError(err) {
      // Handle error
    }
  );

 // promiseB will be resolved immediately after promiseA is resolved
 // and its value will be the result of promiseA incremented by 1.

Angular 2及更新版本

在Angular 2中,我们可以看下面的例子,但是推荐使用观察者模式(observables)与Angular 2一起使用。

 search(term: string) {
     return this.http
       .get(`https://api.spotify.com/v1/search?q=${term}&type=artist`)
       .map((response) => response.json())
       .toPromise();
}

你可以这样使用它:

search() {
    this.searchService.search(this.searchField.value)
      .then((result) => {
    this.result = result.artists.items;
  })
  .catch((error) => console.error(error));
}

在这里查看original的帖子。但是TypeScript不支持native ES6 Promises,如果你想使用它,你可能需要插件。

此外,这是promises specification


20
这并不解释承诺如何解决这个问题。 - Benjamin Gruenbaum
9
jQuery和fetch方法都返回promise。我建议您对答案进行修改。尽管jQuery的不完全相同(有then,但没有catch)。 - Tracker1

183

这里大多数答案都给出了在你有单个异步操作时的有用建议,但是有时,当您需要针对数组或其他类似列表结构中的每个条目执行异步操作时,就会遇到这种情况。诱惑通常是这样做:

// WRONG
var results = [];
theArray.forEach(function(entry) {
    doSomethingAsync(entry, function(result) {
        results.push(result);
    });
});
console.log(results); // E.g., using them, returning them, etc.

示例:

// WRONG
var theArray = [1, 2, 3];
var results = [];
theArray.forEach(function(entry) {
    doSomethingAsync(entry, function(result) {
        results.push(result);
    });
});
console.log("Results:", results); // E.g., using them, returning them, etc.

function doSomethingAsync(value, callback) {
    console.log("Starting async operation for " + value);
    setTimeout(function() {
        console.log("Completing async operation for " + value);
        callback(value * 2);
    }, Math.floor(Math.random() * 200));
}
.as-console-wrapper { max-height: 100% !important; }

此方法无法正常工作的原因是,当您试图使用结果时,doSomethingAsync的回调尚未运行。

因此,如果您有一个数组(或某种列表),并且希望为每个条目执行异步操作,那么您有两个选择:并行执行(重叠)或串行执行(按顺序一个接一个地执行)。

并行执行

您可以启动所有操作,并跟踪您期望收到的回调数量,然后在获取到足够多的回调后使用结果:

var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
    doSomethingAsync(entry, function(result) {
        results[index] = result;
        if (--expecting === 0) {
            // Done!
            console.log("Results:", results); // E.g., using the results
        }
    });
});

示例:

var theArray = [1, 2, 3];
var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
    doSomethingAsync(entry, function(result) {
        results[index] = result;
        if (--expecting === 0) {
            // Done!
            console.log("Results:", JSON.stringify(results)); // E.g., using the results
        }
    });
});

function doSomethingAsync(value, callback) {
    console.log("Starting async operation for " + value);
    setTimeout(function() {
        console.log("Completing async operation for " + value);
        callback(value * 2);
    }, Math.floor(Math.random() * 200));
}
.as-console-wrapper { max-height: 100% !important; }

(我们可以放弃使用 expecting,只使用results.length === theArray.length,但这样会让我们面临着这样的可能性:当调用仍在进行时,theArray被更改了...)

请注意,我们如何使用forEach中的索引来将结果保存在results中,并与相关条目在同一位置,即使结果无序到达(因为异步调用不一定按照启动顺序完成)。

但是如果您需要从函数中返回这些结果呢?就像其他答案指出的那样,您不能;您必须让函数接受并调用回调(或返回Promise)。这里是一个回调版本:

function doSomethingWith(theArray, callback) {
    var results = [];
    var expecting = theArray.length;
    theArray.forEach(function(entry, index) {
        doSomethingAsync(entry, function(result) {
            results[index] = result;
            if (--expecting === 0) {
                // Done!
                callback(results);
            }
        });
    });
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

例子:

function doSomethingWith(theArray, callback) {
    var results = [];
    var expecting = theArray.length;
    theArray.forEach(function(entry, index) {
        doSomethingAsync(entry, function(result) {
            results[index] = result;
            if (--expecting === 0) {
                // Done!
                callback(results);
            }
        });
    });
}
doSomethingWith([1, 2, 3], function(results) {
    console.log("Results:", JSON.stringify(results));
});

function doSomethingAsync(value, callback) {
    console.log("Starting async operation for " + value);
    setTimeout(function() {
        console.log("Completing async operation for " + value);
        callback(value * 2);
    }, Math.floor(Math.random() * 200));
}
.as-console-wrapper { max-height: 100% !important; }

或者这里有一个返回一个Promise的版本:

function doSomethingWith(theArray) {
    return new Promise(function(resolve) {
        var results = [];
        var expecting = theArray.length;
        theArray.forEach(function(entry, index) {
            doSomethingAsync(entry, function(result) {
                results[index] = result;
                if (--expecting === 0) {
                    // Done!
                    resolve(results);
                }
            });
        });
    });
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});
< p >< em >当然,如果< code >doSomethingAsync向我们传递了错误,我们将在出现错误时使用< code >reject来拒绝该承诺。 < p >示例:

function doSomethingWith(theArray) {
    return new Promise(function(resolve) {
        var results = [];
        var expecting = theArray.length;
        theArray.forEach(function(entry, index) {
            doSomethingAsync(entry, function(result) {
                results[index] = result;
                if (--expecting === 0) {
                    // Done!
                    resolve(results);
                }
            });
        });
    });
}
doSomethingWith([1, 2, 3]).then(function(results) {
    console.log("Results:", JSON.stringify(results));
});

function doSomethingAsync(value, callback) {
    console.log("Starting async operation for " + value);
    setTimeout(function() {
        console.log("Completing async operation for " + value);
        callback(value * 2);
    }, Math.floor(Math.random() * 200));
}
.as-console-wrapper { max-height: 100% !important; }

(或者,您可以为doSomethingAsync创建一个返回承诺的包装器,然后执行以下操作...)

如果doSomethingAsync给您提供了一个Promise,则可以使用Promise.all

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(function(entry) {
        return doSomethingAsync(entry);
    }));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

如果你知道doSomethingAsync会忽略第二个和第三个参数,你可以直接将其传递给map(map调用其回调函数时会带有三个参数,但大多数人大部分时间只使用第一个参数):

如果您知道doSomethingAsync将忽略第二个和第三个参数,您可以直接将其传递给mapmap调用其回调函数有三个参数,但大多数人只使用第一个参数):

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(doSomethingAsync));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

示例:

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(doSomethingAsync));
}
doSomethingWith([1, 2, 3]).then(function(results) {
    console.log("Results:", JSON.stringify(results));
});

function doSomethingAsync(value) {
    console.log("Starting async operation for " + value);
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log("Completing async operation for " + value);
            resolve(value * 2);
        }, Math.floor(Math.random() * 200));
    });
}
.as-console-wrapper { max-height: 100% !important; }

请注意,Promise.all 会在所有传入的 Promise 都变成 fulfilled 状态时,用一个由所有传入 Promise 的结果组成的数组来解决(resolve)自身;如果给定的任意一个 Promise 被 rejected,则会将第一个被 reject 的 Promise 的错误信息作为参数,reject 掉整个 Promise.all

串行处理

假设您不想让操作并行执行,并且希望它们一个接一个地运行。 如果要在开始下一个操作之前等待每个操作完成,可以使用以下示例函数,并通过结果调用回调函数:

function doSomethingWith(theArray, callback) {
    var results = [];
    doOne(0);
    function doOne(index) {
        if (index < theArray.length) {
            doSomethingAsync(theArray[index], function(result) {
                results.push(result);
                doOne(index + 1);
            });
        } else {
            // Done!
            callback(results);
        }
    }
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});
< p>(由于我们是按顺序进行工作的,所以我们可以只使用results.push(result),因为我们知道我们不会按顺序获取结果。在上面的示例中,我们可以使用results[index] = result;,但在以下某些示例中,我们没有可以使用的索引。)

示例:

function doSomethingWith(theArray, callback) {
    var results = [];
    doOne(0);
    function doOne(index) {
        if (index < theArray.length) {
            doSomethingAsync(theArray[index], function(result) {
                results.push(result);
                doOne(index + 1);
            });
        } else {
            // Done!
            callback(results);
        }
    }
}
doSomethingWith([1, 2, 3], function(results) {
    console.log("Results:", JSON.stringify(results));
});

function doSomethingAsync(value, callback) {
    console.log("Starting async operation for " + value);
    setTimeout(function() {
        console.log("Completing async operation for " + value);
        callback(value * 2);
    }, Math.floor(Math.random() * 200));
}
.as-console-wrapper { max-height: 100% !important; }

(或者,再次说明,构建一个包装器用于doSomethingAsync,以给您一个Promise并执行以下操作...)

如果doSomethingAsync为您提供了一个Promise,如果您可以使用ES2017+语法(也许使用像Babel这样的转译器),则可以使用带有for-ofawaitasync函数

async function doSomethingWith(theArray) {
    const results = [];
    for (const entry of theArray) {
        results.push(await doSomethingAsync(entry));
    }
    return results;
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

例子:

async function doSomethingWith(theArray) {
    const results = [];
    for (const entry of theArray) {
        results.push(await doSomethingAsync(entry));
    }
    return results;
}
doSomethingWith([1, 2, 3]).then(function(results) {
    console.log("Results:", JSON.stringify(results));
});

function doSomethingAsync(value) {
    console.log("Starting async operation for " + value);
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log("Completing async operation for " + value);
            resolve(value * 2);
        }, Math.floor(Math.random() * 200));
    });
}
.as-console-wrapper { max-height: 100% !important; }

如果你还无法使用ES2017+语法,可以使用"Promise reduce"模式的变体(这比通常的Promise reduce更复杂,因为我们不是将结果从一个传递到另一个,而是在数组中收集它们的结果):

function doSomethingWith(theArray) {
    return theArray.reduce(function(p, entry) {
        return p.then(function(results) {
            return doSomethingAsync(entry).then(function(result) {
                results.push(result);
                return results;
            });
        });
    }, Promise.resolve([]));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

例子:

function doSomethingWith(theArray) {
    return theArray.reduce(function(p, entry) {
        return p.then(function(results) {
            return doSomethingAsync(entry).then(function(result) {
                results.push(result);
                return results;
            });
        });
    }, Promise.resolve([]));
}
doSomethingWith([1, 2, 3]).then(function(results) {
    console.log("Results:", JSON.stringify(results));
});

function doSomethingAsync(value) {
    console.log("Starting async operation for " + value);
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log("Completing async operation for " + value);
            resolve(value * 2);
        }, Math.floor(Math.random() * 200));
    });
}
.as-console-wrapper { max-height: 100% !important; }

使用ES2015+箭头函数可以让代码更加简洁:

function doSomethingWith(theArray) {
    return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => {
        results.push(result);
        return results;
    })), Promise.resolve([]));
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

例子:

function doSomethingWith(theArray) {
    return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => {
        results.push(result);
        return results;
    })), Promise.resolve([]));
}
doSomethingWith([1, 2, 3]).then(function(results) {
    console.log("Results:", JSON.stringify(results));
});

function doSomethingAsync(value) {
    console.log("Starting async operation for " + value);
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log("Completing async operation for " + value);
            resolve(value * 2);
        }, Math.floor(Math.random() * 200));
    });
}
.as-console-wrapper { max-height: 100% !important; }


5
请问你能解释一下代码中的if (--expecting === 0)是如何工作的吗?你提供的解决方案的回调版本对我很有效,但是我不理解在这个语句中,你是如何检查完成响应数量的。我知道这可能只是我的知识不足。是否有另一种方式可以编写这个检查呢? - Sarah
5
@Sarah说:“expecting”开始的值是“array.length”,这是我们要发出请求的数量。我们知道直到所有这些请求都开始后回调函数才会被调用。在回调函数中,“if (--expecting === 0)”做了以下几件事情:1.减少“expecting”的值(我们已经收到一个响应,所以我们期望得到更少的响应),如果在减少之后的值为0(我们不再期望接收任何响应),则我们完成了! - T.J. Crowder
2
@Henke - 我认为这确实是个人偏好问题,通常情况下我更喜欢记录原始数据并让控制台处理它,但在这种特定情况下,我认为你关于更改的想法是正确的。谢谢! :-) - T.J. Crowder
2
为了方便自己(和其他人?),在此添加一个相关答案的链接:如何进行多个异步调用并等待它们全部完成 - Henke

129

请看这个例子:

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope,$http) {

    var getJoke = function(){
        return $http.get('http://api.icndb.com/jokes/random').then(function(res){
            return res.data.value;  
        });
    }

    getJoke().then(function(res) {
        console.log(res.joke);
    });
});

正如你所看到的,getJoke 返回一个已解决的 promise(当返回 res.data.value 时它会被解决)。因此,你要等待直到 $http.get 请求完成,然后执行 console.log(res.joke)(就像普通的异步流程一样)。

这是 plnkr 的链接:

http://embed.plnkr.co/XlNR7HpCaIhJxskMJfSg/

ES6 的方式(async - await)

(function(){
  async function getJoke(){
    let response = await fetch('http://api.icndb.com/jokes/random');
    let data = await response.json();
    return data.value;
  }

  getJoke().then((joke) => {
    console.log(joke);
  });
})();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接