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


我有一个功能 foo 这是一个Ajax请求。我如何从中返回响应 foo

我试过从中返回值 success 回调以及将响应分配给函数内部的局部变量并返回该变量,但这些方法都没有实际返回响应。

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            result = response;
            // return response; // <- I tried that one as well
        }
    });

    return result;
}

var result = foo(); // It always ends up being `undefined`.

4321
2018-01-08 17:06


起源


@MaximShoustin我添加了一个只有JavaScript的答案(没有jQuery),但像Felix所说 - “如何从AJAX调用返回响应”真的是一个很好的方式来说“并发在JavaScript中是如何工作的”。更不用说Angular了 $http与jQuery非常相似 $.ajax 无论如何都有一些细微的差别(比如与消化周期交织)。 - Benjamin Gruenbaum
不要 使用 async: false。它会冻结浏览器并驱使用户离开。读 blog.slaks.net/2015-01-04/async-method-patterns 学习如何使用异步操作。 - SLaks
你可以通过ading async:false作为第一个参数的属性来做到这一点。请参阅下面的答案。 @tvanfosson在Stackoverflow上提供了这个解决方案。 - arrowman
这个mey帮助你 codingbin.com/get-return-data-ajax-call - Manoj Kumar
@SLaks有人请转发给AirAsia.com网站的“摇滚明星开发者”。这是一个伟大的现场案例研究(由ajax调用增强几秒钟),为什么永远不要使用async:false。 - Mâtt Frëëman


答案:


-> 有关不同示例的异步行为的更一般说明,请参阅  为什么我的变量在函数内部修改后没有变化? - 异步代码引用 

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

问题

一个 在 阿贾克斯 代表 异步 。这意味着发送请求(或者更确切地说是接收响应)将从正常执行流程中取出。在你的例子中, $.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 item
});
doSomethingElse();

而不是等待响应,执行立即继续执行Ajax调用之后的语句。为了最终获得响应,您提供了一个在收到响应后调用的函数,a 回电话 (注意什么? 回电话 ?)。调用之后的任何语句都会在调用回调之前执行。


解决方案(S)

拥抱JavaScript的异步特性! 虽然某些异步操作提供了同步对应(“Ajax”也是如此),但通常不鼓励使用它们,尤其是在浏览器上下文中。

你问为什么这么糟糕?

JavaScript在浏览器的UI线程中运行,任何长时间运行的进程都会锁定UI,使其无响应。此外,JavaScript的执行时间有一个上限,浏览器会询问用户是否继续执行。

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

在下文中,我们将看看三种不同的解决方案,它们都是相互建立的:

  • 承诺与 async/await (ES2017 +,如果使用转换器或再生器,可在旧版浏览器中使用)
  • 回调 (在节点中流行)
  • 承诺与 then() (ES2015 +,如果您使用众多承诺库中的一个,则可在旧版浏览器中使用)

这三种都可以在当前浏览器和节点7+中使用。 


ES2017 +:承诺与 async/await

2017年发布的新ECMAScript版本推出 语法级支持 用于异步功能。在...的帮助下 async 和 await,您可以在“同步样式”中编写异步。不过没错:代码仍然是异步的,但它更容易阅读/理解。

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

重要: 你只能使用 await 在里面 async 功能。这意味着,在最高级别,您仍然必须直接与承诺一起工作。

你可以阅读更多关于 async 和 await 在MDN上。

这是一个建立在上面延迟之上的例子:

// 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 a second (just for the sake of this example)
    await delay(1000);
    // GET information about each book
    return await 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;
  }
}

// Async functions always return a promise
getAllBooks()
  .then(function(books) {
    console.log(books);
  });

较新 浏览器 和 节点 版本支持 async/await。您还可以通过在以下帮助下将代码转换为ES5来支持旧环境 再生 (或使用再生器的工具,例如 巴别塔)。


让函数接受 回调

回调只是传递给另一个函数的函数。其他函数可以在函数准备就绪时调用函数。在异步过程的上下文中,只要异步过程完成,就会调用回调。通常,结果将传递给回调。

在问题的例子中,你可以做 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 +:承诺 然后()

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

承诺是容器 未来 值。当承诺收到价值时(它是 解决)或取消时(拒绝),它通知所有想要访问此值的“听众”。

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

以下是使用承诺的简单示例:

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

应用于我们的Ajax调用,我们可以使用这样的promises:

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("/echo/json")
  .then(function(result) {
    // Code depending on result
  })
  .catch(function() {
    // An error occurred
  });

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

有关承诺的更多信息: HTML5摇滚 - JavaScript承诺

旁注:jQuery的延迟对象

延期对象 是jQuery的promises自定义实现(在Promise API标准化之前)。它们的行为几乎与承诺相似,但暴露出略微不同的API。

jQuery的每个Ajax方法都已经返回一个“延迟对象”(实际上是一个延迟对象的承诺),你可以从你的函数返回:

function ajax() {
    return $.ajax(...);
}

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

旁注:承诺陷阱

请记住,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 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开始。 然后你可以使用a success 回调或访问 responseText 的财产 jqXHR对象

function foo() {
    var jqXHR = $.ajax({
        //...
        async: false
    });
    return jqXHR.responseText;
}

如果您使用任何其他jQuery Ajax方法,例如 $.get$.getJSON等等,你必须把它改成 $.ajax(因为您只能将配置参数传递给 $.ajax)。

抬头! 无法进行同步 JSONP 请求。 JSONP本质上总是异步的(甚至不考虑这个选项的另一个原因)。


4666
2018-01-08 17:06



@Pommy:如果你想使用jQuery,你必须包含它。请参阅 docs.jquery.com/Tutorials:Getting_Started_with_jQuery。 - Felix Kling
在解决方案1,子jQuery中,我无法理解这一行: If you use any other jQuery AJAX method, such as $.get, $.getJSON, etc., you have them to $.ajax. (是的,我知道我的昵称在这种情况下有点讽刺) - gibberish
@gibberish:嗯,我不知道如何让它更清晰。你明白了吗? foo 被调用并传递一个函数(foo(function(result) {....});)? result 在此函数中使用,是Ajax请求的响应。要引用此函数,将调用foo的第一个参数 callback 并分配给 success 而不是匿名函数。所以, $.ajax 将会通知 callback 当请求成功时。我试着解释一下。 - Felix Kling
这个问题的聊天已经死了,所以我不确定在哪里提出概述的更改,但我建议:1)将同步部分更改为简单讨论为什么它不好而没有代码示例如何执行此操作。 2)删除/合并回调示例以仅显示更灵活的延迟方法,我认为对于那些学习Javascript的人来说也可能更容易理解。 - Chris Moschini
@Jessi:我觉得你误解了那部分答案。你不能用 $.getJSON 如果您希望Ajax请求是同步的。但是,您不应该希望请求是同步的,因此不适用。您应该使用回调或承诺来处理响应,如前面的答案中所述。 - Felix Kling


如果你是  在你的代码中使用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中代表 异步。这意味着发送请求(或者更确切地说是接收响应)将从正常执行流程中取出。在你的例子中, .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, 不要这样做! 菲利克斯的回答提出了一些令人信服的论据,说明为什么这是一个坏主意。总结一下,它会冻结用户的浏览器,直到服务器返回响应并创建非常糟糕的用户体验。以下是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并进行相应的操作来进一步扩展它(创建一个失败处理程序等)。有效解决我们的问题。

如果你仍然很难理解这一点 阅读AJAX入门指南 在MDN。


888
2018-05-29 23:30



“同步请求阻止代码执行并可能泄漏内存和事件”同步请求如何泄漏内存? - Matthew G
@MatthewG我已经在它上面加了一笔赏金 这个问题,我会看到我能钓到什么。我正在删除答案中的引文。 - Benjamin Gruenbaum
仅供参考,XHR 2允许我们使用 onload 处理程序,只在什么时候触发 readyState 是 4。当然,它在IE8中不受支持。 (iirc,可能需要确认。) - Florian Margaine
您对如何将匿名函数作为回调传递的说明是有效的,但具有误导性。示例var bar = foo();要求定义变量,而建议的foo(functim(){});没有定义吧 - Robbie Averill
@BenjaminGruenbaum如何返回内容 result在一个变量,我可以在我的代码中的其他地方使用而不是用html打印它? - MorganFR


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

如你看到的:

  1. 它比列出的所有其他功能短。
  2. 回调是直接设置的(因此没有额外的不必要的闭包)。
  3. 它使用新的onload(因此您不必检查readystate && status)
  4. 还有一些我不记得的情况使得XMLHttpRequest 1令人讨厌。

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

最简单的:

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

再次......这是一个非常短的功能,但它确实得到和发布。

用法示例:

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

如你所见,我没有实现同步...这是一件坏事。

话虽如此......为什么不这么简单呢?


正如评论中所提到的,使用error && synchronous确实完全打破了答案的要点。哪个是以正确的方式使用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'作为formdata传递它也不会抛出错误。

在第一种情况下,错误在内部 displayAjax() 下 this.statusText 如 Method not Allowed

在第二种情况下,它只是起作用。如果您传递了正确的帖子数据,则必须在服务器端进行检查。

跨域不允许自动抛出错误。

在错误响应中,没有错误代码。

只有 this.type 这被设置为错误。

如果您完全无法控制错误,为什么要添加错误处理程序? 大多数错误都在回调函数中返回 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做很多事情:

我制作了一个下载管理器(使用简历,文件读取器,文件系统两侧的范围),使用画布的各种图像缩放器转换器,使用base64images填充websql数据库等等......但在这些情况下,您应该仅为此目的创建一个函数...有时你需要一个blob,数组缓冲区,你可以设置标题,覆盖mimetype,还有更多......

但这里的问题是如何返回Ajax响应...(我添加了一个简单的方法。)


304
2017-08-19 08:06



虽然这个答案很好(我们都是 爱 XHR2和发布文件数据和多部分数据非常棒) - 这显示了使用JavaScript发布XHR的语法糖 - 您可能希望将其放在博客文章中(我喜欢它)或甚至放在库中(不确定名称 x, ajax 要么 xhr 可能更好:))。我没有看到它如何解决从AJAX调用返回响应的问题。 (有人还可以 var res = x("url") 而不明白为什么它不起作用;))。在旁注 - 如果你回来会很酷 c 从方法,所以用户可以挂钩 error 等等 - Benjamin Gruenbaum
2.ajax is meant to be async.. so NO var res=x('url').. 这就是这个问题和答案的全部要点:) - Benjamin Gruenbaum
为什么在函数中有一个'c'参数,如果在第一行你覆盖它有什么价值?我错过了什么吗? - Brian H.
您可以使用参数作为占位符以避免多次写入“var” - cocco
@cocco所以你在SO中编写了误导性的,不可读的代码 回答 为了省点几次击键?请不要那样做。 - stone


如果您使用承诺,这个答案适合您。

这意味着AngularJS,jQuery(带有延迟),本机XHR的替换(fetch),EmberJS,BackboneJS的保存或任何返回promises的节点库。

你的代码应该是这样的:

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为使用jQuery和AJAX回调的人写了一个很好的答案。我有一个原生XHR的答案。这个答案是对前端或后端的承诺的一般用法。


核心问题

浏览器和NodeJS / io.js服务器上的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,服务器调用,IO,计时器),因此在请求有机会告诉您的代码该值是什么之前,您将返回该值。

这个问题的一个可能的解决方案是编码 重新活跃 ,告诉你的程序计算完成后该怎么做。承诺通过本质上是暂时的(时间敏感的)来积极地实现这一点。

快速回顾承诺

承诺是一个 随着时间的推移。承诺有状态,它们开始等待没有价值,可以满足:

  • 完成 意味着计算成功完成。
  • 拒绝 这意味着计算失败了。

承诺只能改变状态 一旦 之后它将永远保持在同一个状态。你可以附上 then 处理程序承诺提取其价值并处理错误。 then 处理程序允许 链接 电话。承诺是由创造的 使用返回它们的API。例如,更现代的AJAX替代品 fetch 或jQuery的 $.get 回报承诺。

当我们打电话 .then 承诺和 返回 来自它的东西 - 我们得到了承诺 处理后的价值。如果我们回到另一个承诺,我们会得到惊人的东西,但让我们抓住我们的马。

有了承诺

让我们看看我们如何用promises解决上述问题。首先,让我们通过使用来证明我们对上述承诺状态的理解 承诺构造函数 用于创建延迟功能:

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, 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`
})

所以这也适用。我们已经知道我们不能从已经异步的调用中返回值,但我们可以使用promises并将它们链接起来执行处理。我们现在知道如何从异步调用返回响应。

ES2015(ES6)

ES6介绍 发电机 这些函数可以在中间返回,然后恢复它们所处的位置。这通常对序列有用,例如:

function* foo(){ // notice the star, this is ES6 so new browsers/node/io only
    yield 1;
    yield 2;
    while(true) yield 3;
}

是一个返回的函数 迭代器 在序列上 1,2,3,3,3,3,.... 可以迭代。虽然这本身很有趣并且为很多可能性开辟了空间,但有一个特别有趣的案例。

如果我们生成的序列是一系列动作而不是数字 - 我们可以在每次动作时暂停该函数并在我们恢复该函数之前等待它。因此,我们需要一系列数字而不是一系列数字 未来 价值观 - 即:承诺。

这个有点棘手但非常强大的技巧让我们以同步的方式编写异步代码。有几个“跑步者”为你做这个,写一个是几行代码,但超出了这个答案的范围。我将使用Bluebird的 Promise.coroutine 在这里,但还有其他包装 co 要么 Q.async

var foo = coroutine(function*(){
    var data = yield fetch("/echo/json"); // notice the yield
    // 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
   // server call done here, 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 done
});
main();

ES2016(ES7)

在ES7中,这是进一步标准化的,现在有几个提议,但在所有提案中你都可以 await 诺言。这只是上面提到的ES6提案的“糖”(更好的语法) async 和 await 关键字。制作上面的例子:

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
}

它仍然返回一个相同的承诺:)


245
2018-05-12 02:22



ES 2016看起来是一劳永逸的最终解决方案。 - ShrekOverflow
绝对最终的解决方案,一劳永逸?完全绝对的最终解决方案,一次又一次加一个? - Shmiddty
你到底在哪里得到“五”?它永远不会在任何地方宣布。 - recurf
@recurf首先“地狱哪里”不适合这个网站(孩子们也在使用它,一个人)。第二个 - 它是一个函数参数,我可以在其他地方命名它,它可以正常工作。 - Benjamin Gruenbaum
你回答了这个问题吗?如何从异步调用返回响应? @BenjaminGruenbaum - OnPrinciples


您正在使用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);
    }
});

在提交处理程序中返回任何内容都不会执行任何操作。您必须切换数据,或者直接在成功函数内执行您想要的操作。


193
2018-05-23 02:05



这个答案完全是语义的......你的成功方法只是回调中的回调。你可以拥有 success: handleData 它会起作用。 - Jacques
如果你想在“handleData”之外返回“responseData”怎么办... :) ...你会怎么做......? ...导致一个简单的返回将它返回到ajax的“成功”回调...而不是在“handleData”之外... - pesho hristov
@Jacques&@pesho hristov你错过了这一点。提交处理程序不是 success 方法,它是周围的范围 $.ajax。 - travnik
@travnik我没有错过。如果您获取了handleData的内容并将其放入success方法中,它将完全相同... - Jacques


最简单的解决方案是创建一个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 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);    
}); 

185
2018-02-18 18:58



我不知道谁投票否定。但这是一个工作,事实上我使用这种方法来创建一个完整的应用程序。 jquery.ajax不返回数据,因此最好使用上述方法。如果它是错的,请解释并建议更好的方法来做到这一点。 - Hemant Bavle
对不起,我忘记发表评论(我通常会这样做!)。我贬低了它。 Downvotes并不表示事实的正确性或缺乏,它们表明在上下文中缺乏有用性。我没有找到你的答案,因为菲利克斯已经解释了这一点,只是更详细。另外,如果它是JSON,为什么要对响应进行字符串化? - Benjamin Gruenbaum
好.. @Benjamin我使用stringify,将JSON对象转换为字符串。并且谢谢你澄清你的观点。请记住发布更精细的答案。 - Hemant Bavle
如果你想在“successCallback”之外返回“responseObj”怎么办... :) ...你会怎么做......? ...导致一个简单的返回将它返回到ajax的“成功”回调...而不是在“successCallback”之外... - pesho hristov


我会回答一个看起来很可怕的手绘漫画。第二张图是原因 result 是 undefined 在你的代码示例中。

enter image description here


155
2017-08-11 14:17



一张图片胜过千言万语, 人A  - 请问B的人详细说明修理他的车 人B.  - 进行Ajax调用并等待服务器响应汽车修复细节,当收到响应时,Ajax Success函数调用Person B函数并将响应作为参数传递给它,Person A收到答案。 - stom
如果您为每个图像添加代码行以说明概念,那将会很棒。 - Hassan Baig
我遇到的最好的解释 - anonymous


Angular1

对于正在使用的人 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.

Angular2和Later

Angular2 看看下面的例子,但是它 推荐的 使用 Observables 同 Angular2

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

原版的 发布在这里。但是Typescript不支持 本机es6承诺,如果你想使用它,你可能需要插件。

此外,这是承诺 规范 在这里定义。


114
2017-08-26 08:11



这并不能解释承诺如何解决这个问题。 - Benjamin Gruenbaum
它不起作用。 promiseB将获得'undefined' - An Overflowed Stack
jQuery和 取 方法也都返回承诺。我建议修改你的答案。虽然jQuery不完全相同(然后就是那里,但抓不到)。 - Tracker1


这里的大多数答案都提供了有关何时进行单个异步操作的有用建议,但有时,当您需要执行异步操作时会出现这种情况。  在数组或其他类似列表的结构中的条目。诱惑是这样做:

// 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:", 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 在电话未决时更改......)

注意我们如何使用 index 从 forEach 保存结果 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(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:", 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);
});

当然,如果 doSomethingAsync 传递给我们错误,我们使用 reject 当我们收到错误时拒绝承诺。)

例:

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:", 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.all

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

例:

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(function(entry) {
        return doSomethingAsync(entry, function(result) {
            results.push(result);
        });
    }));
}
doSomethingWith([1, 2, 3]).then(function(results) {
    console.log("Results:", 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 通过一系列所有承诺的结果解决它的承诺,当它们全部被解决时,或者当它们被解决时拒绝承诺 第一 你给它的承诺拒绝。

系列

假设您不希望操作并行?如果要一个接一个地运行它们,则需要等到每个操作完成后再开始下一个操作。下面是一个函数的示例,它使用结果调用回调函数:

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

(由于我们正在进行系列工作,我们可以使用 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:", 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 如果你可以使用ES2017 +语法(也许有一个类似的转换器)给你一个Promise 巴别塔),你可以使用 async 功能 同 for-of 和 await

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:", 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更复杂,因为我们没有将结果从一个传递到下一个,而是在数组中收集它们的结果):

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:", 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:", 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;
}


91
2018-05-03 16:59



你能解释一下吗? if (--expecting === 0) 部分代码可以吗?您的解决方案的回调版本对我来说非常有用,我只是不明白如何使用该语句检查已完成的响应数量。欣赏它只是缺乏我的知识。是否有另一种方法可以编写支票? - Sarah
@莎拉: expecting 从值开始 array.length,这是我们要做多少次请求。我们知道在所有这些请求都启动之前不会调用回调。在回调中, if (--expecting === 0) 这样做:1。减量 expecting (我们收到了回复,所以我们期待的回复少一个),如果有价值的话 后 减量为0(我们不再期待任何回复),我们已经完成了! - T.J. Crowder