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


5506

我有一个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`.

Answers:


5700

→有关使用不同示例的异步行为的更一般说明,请参见 在函数内部修改变量后为什么变量未更改?-异步代码参考

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

问题

的Ajax代表异步。这意味着发送请求(或接收响应)已从正常执行流程中删除。在您的示例中,$.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调用后的语句。为了最终获得响应,您提供了一个在收到响应后立即调用的函数,即回调函数(注意什么?回叫?)。在调用之后执行的所有语句都将在调用回调之前执行。


解决方案

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

你问为什么不好?

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

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

在下面的内容中,我们将研究三种互为基础的不同解决方案:

  • 承诺async/await(ES2017 +,如果使用转译器或再生器,则在较旧的浏览器中可用)
  • 回调(在节点中受欢迎)
  • 承诺then()(ES2015 +,如果您使用许多承诺库之一,则在较旧的浏览器中可用)

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


ES2017 +:承诺 async/await

2017年发布的ECMAScript版本引入了对异步功能的语法级支持。借助asyncawait,您可以以“同步样式”编写异步。该代码仍然是异步的,但更易于阅读/理解。

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

重要提示:您只能awaitasync函数内部使用。目前,await尚不支持顶层,因此您可能必须进行异步IIFE(立即调用函数表达式)才能启动async上下文。

你可以阅读更多关于asyncawait的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 3 seconds (just for the sake of this example)
    await delay();
    // 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;
  }
}

// Start an IIFE to use `await` at the top level
(async function(){
  let books = await getAllBooks();
  console.log(books);
})();

当前的浏览器节点版本支持async/await。您还可以通过使用再生(或使用再生器的工具,例如Babel)将代码转换为ES5来支持较旧的环境。


让函数接受回调

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

在问题的示例中,您可以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
    });
}

callbackfoo在调用它时引用我们传递给的函数,而只是将其传递给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()的承诺

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

承诺是未来价值的容器。当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).
  });

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

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

有关诺言的更多信息:HTML5摇滚-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()它不会在检查服务器上的“ / 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本质上始终是异步的(还有一个甚至不考虑此选项的原因)。


74
@Pommy:如果要使用jQuery,则必须包含它。请参考docs.jquery.com/Tutorials:Getting_Started_with_jQuery
Felix Kling 2013年

11
在解决方案1中,在jQuery子菜单中,我听不懂这一行:(If you use any other jQuery AJAX method, such as $.get, $.getJSON, etc., you have them to $.ajax.是的,我意识到我的昵称在这种情况下有点讽刺意味)
cssyphus

31
@胡言乱语:嗯,我不知道如何使它更清晰。您是否知道如何foo调用并将函数传递给它(foo(function(result) {....});)?result在此函数内部使用,是Ajax请求的响应。要引用此函数,将调用foo的第一个参数callback并将其分配给它,success而不是使用匿名函数。因此,$.ajaxcallback在请求成功时调用。我试图解释更多。
Felix Kling

43
这个问题的Chat已经死了,所以我不确定在哪里可以提出概述的更改,但是我建议:1)将同步部分更改为简单的讨论,以说明为什么不好,没有代码示例。2)删除/合并回调示例仅显示更灵活的Deferred方法,对于那些学习Javascript的人,我认为它也可能更容易理解。
克里斯·莫斯基尼

14
@Jessi:我认为您误解了答案的这一部分。$.getJSON如果要使Ajax请求同步,则不能使用。但是,您不应该事件使请求同步,因此这并不适用。您应该使用回调或Promise处理响应,如答案中前面所述。
菲利克斯·克林

1071

如果您在代码中使用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的人提供替代方案。

请注意,对于那些使用新fetchAPI的用户,Angular或Promise我在下面添加了另一个答案


您所面对的

这是其他答案中“问题的解释”的简短摘要,如果不确定阅读此内容后,请阅读该内容。

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


20
“同步请求阻塞了代码的执行,可能会泄漏内存和事件”同步请求如何泄漏内存?
马修·G

10
@MatthewG我在这个问题上添加了赏金,我将看到可以钓鱼的东西。同时,我将从答案中删除引号。
本杰明·格林巴姆

17
仅供参考,XHR 2允许我们使用onload处理程序,该处理程序仅在readyStateis 时触发4。当然,IE8不支持它。(iirc,可能需要确认。)
Florian Margaine 2013年

9
您关于如何将匿名函数作为回调传递的说明是有效的,但会产生误导。示例var bar = foo(); 正在要求定义一个变量,而建议的foo(functim(){}); 没有定义酒吧
Robbie Averill

396

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和&状态)
  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存在,所以我正在使用这种方法。我使用的所有浏览器都从未遇到过任何类型的问题。

仅当您要获取状态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)
}

再次...这是一个非常短的函数,但是它确实可以获取和发布。

用法示例:

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'作为formdata传递,它也不会引发错误。

在第一种情况下,误差是内displayAjax()this.statusTextMethod 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填充Web SQL数据库等等。但是在这些情况下,您应该为此创建一个函数目的...有时您需要一个blob,数组缓冲区,可以设置标头,覆盖mimetype等等,还有更多...

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


15
虽然这个答案很好(而且我们所有人都喜欢 XHR2,并且发布文件数据和多部分数据都很棒)-这显示了使用JavaScript发布XHR的语法糖-您可能希望将其放在博客文章中(我想要它)甚至在图书馆中(不确定名称xajax或者xhr可能更好一些:)。我看不到它如何解决从AJAX调用返回响应的问题。(有人仍然可以做var res = x("url"),但是不明白为什么它不起作用;)。附带说明-如果您c从方法中返回,这很酷,以便用户可以进行钩挂error等等
。– Benjamin Gruenbaum

25
2.ajax is meant to be async.. so NO var res=x('url')..这就是这个问题和答案的全部要点:)
本杰明·格伦鲍姆

3
如果在第一行中覆盖了它的任何值,为什么函数中会有一个“ c”参数?我想念什么吗?
Brian H.

2
您可以将参数用作占位符,以避免多次写入“ var”
cocco

11
@cocco 为了节省一些击键,您在SO 答案中写了具有误导性的,不可读的代码?请不要那样做。

316

如果您使用的是Promise,则此答案适合您。

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

您的代码应类似于以下内容:

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有一个答案。这个答案是针对promise在前端或后端的一般用法。


核心问题

浏览器和具有NodeJS / io.js的服务器上的JavaScript并发模型是异步的响应式的

每当调用返回承诺的方法时,then处理程序总是异步执行的-也就是说,它们下面的代码之后(不在.then处理程序中)。

这意味着当您返回定义datathen处理程序时,该处理程序尚未执行。这又意味着您返回的值没有及时设置为正确的值。

这是一个简单的比喻:

    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

的价值dataundefined因为data = 5部分尚未执行。它可能会在一秒钟内执行,但到那时它与返回的值无关。

由于操作尚未发生(AJAX,服务器调用,IO,计时器),因此您将在请求有机会告诉您的代码该值之前返回该值。

一个可能的解决这个问题是代码重新活跃,告诉你的程序在计算完成后做什么。承诺本质上是时间(时间敏感)的,因此可以积极地实现这一点。

快速回顾承诺

承诺是一段时间价值。承诺具有状态,它们以没有价值的待处理状态开始,可以解决:

  • 完成意味着计算成功完成。
  • 拒绝表示计算失败。

一个承诺只能更改一次状态此后它将永远永远保持在同一状态。您可以将then处理程序附加到promise,以提取其值并处理错误。then处理程序允许链接调用。通过使用返回API的API来创建Promise 。例如,更现代的AJAX替代品fetch或jQuery的$.get回报承诺。

当我们调用.then一个承诺并从中返回某些东西时,我们就得到了处理后价值的承诺。如果我们再次兑现诺言,我们将获得惊人的收获,但让我们坚持不懈。

承诺

让我们看看如何用诺言解决上述问题。首先,让我们通过使用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转换为使用Promise之后,可以使用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`
})

因此,它也同样有效。我们已经了解到无法从已经异步的调用中返回值,但是我们可以使用promise并将它们链接起来以执行处理。现在,我们知道如何从异步调用返回响应。

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,....。尽管这本身很有趣,并且为很多可能性打开了空间,但是有一个特别有趣的案例。

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

这个有点棘手但非常强大的技巧使我们可以以同步方式编写异步代码。有几个“运行器”可以为您完成此任务,编写一小段代码即可,但超出了此答案的范围。我将在Promise.coroutine这里使用Bluebird ,但还有其他包装器,例如coQ.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保证所有建议。通过添加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(尽管我们不应该这样return await data.json();做)
Lewis Donovan

247

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

在提交处理程序中返回任何内容都不会做任何事情。相反,您必须交出数据,或者直接在成功函数中执行所需的操作。


13
这个答案完全是语义上的...您的成功方法只是回调中的回调。您可以拥有success: handleData它,并且可以使用。
雅克(Jacques)

5
还有,如果您想在“ handleData”之外返回“ responseData” ... :) ...您将如何做...?...导致简单的返回将把它返回给ajax的“ success”回调...而不是在“ handleData”之外... ...
pesho hristov 16/02/19

@Jacques和@pesho hristov您错过了这一点。提交处理程序不是success方法,而是的周围范围$.ajax
travnik

@travnik我没有错过。如果将handleData的内容放入成功方法中,它的行为将完全相同...
Jacquesジャック

234

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

3
我不知道是谁投票否定的。但这实际上是一项工作,实际上我使用这种方法来创建整个应用程序。jquery.ajax不返回数据,因此最好使用上述方法。如果不正确,请解释并提出更好的方法。
Hemant Bavle 2014年

11
抱歉,我忘了发表评论(我通常会这样做!)。我投票了。否决票并不表示事实正确或不正确,它们表示在上下文中有用或不正确。考虑到Felix的回答,我认为您的答案没有什么用处,因为Felix的解释已经非常详细了。附带说明一下,为什么要对响应进行字符串化(如果是JSON)?
Benjamin Gruenbaum 2014年

5
好的.. @Benjamin我使用了stringify,将JSON对象转换为字符串。感谢您阐明您的观点。请牢记发布更多详尽的答案。
Hemant Bavle 2014年

而且,如果您想在“ successCallback”之外返回“ responseObj”,该怎么办... :) ...您将如何做...?...导致简单的返回就将其返回给ajax的“ success”回调...而不是在“ successCallback”之外...
pesho hristov 16/02/19

221

我将以恐怖的手绘漫画来回答。第二图像是为什么的原因resultundefined在你的代码示例。

在此处输入图片说明


32
一幅价值一千个字的图片人A-询问人B详细信息来修理他的汽车,然后人B-进行Ajax调用并等待服务器的响应以提供汽车修复细节,当收到响应时,Ajax Success函数调用人B函数并将响应作为参数传递给它,人A收到答案。
shaijut

10
如果在每个图像上添加代码行来说明概念,那将是很好的。
哈桑·拜格,

1
同时,开车的家伙被困在路边。他要求将汽车修好后再继续。现在,他独自一人在路边等待着……他宁愿在电话中等待状态变化,但机械师不会这样做……机械师说他必须继续工作,不能只需挂断电话。技工答应他会尽快给他回电话。大约4个小时后,这家伙放弃了,给Uber打了电话。-超时示例。
barrypicker19年

@barrypicker :-D很棒!
约翰内斯·法伦克鲁格

159

角度1

对于正在使用的人 AngularJS的来处理这种情况Promises

这里

承诺可用于嵌套异步功能,并允许将多个功能链接在一起。

你会找到一个很好的解释 在这里

在下面提到的文档中找到示例。

  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及更高版本

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 Promises,如果要使用它,则可能需要插件。

另外,这是在这里定义的Promise 规范


15
但这并不能解释诺言如何解决这个问题。
本杰明·格伦鲍姆

4
jQuery和fetch方法也都返回诺言。我建议修改您的答案。尽管jQuery并不完全相同(然后在那里,但catch却不一样)。
Tracker1 2015年

153

此处的大多数答案都为您执行单个异步操作提供了有用的建议,但是有时,当您需要对数组或其他类似列表的结构中的每个条目执行异步操作时,就会出现此建议。诱惑在于:

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

例:

不起作用的原因是,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
        }
    });
});

例:

(我们可以删除expecting并只使用results.length === theArray.length,但是这让我们对theArray在通话未完成时发生更改的可能性持开放态度。)

请注意,即使结果未按顺序到达(由于异步调用不一定按它们启动的顺序完成),我们如何使用indexfrom 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);
});

例:

或以下是返回的版本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在遇到错误时拒绝诺言。)

例:

(或者,您可以为此包装doSomethingAsync一个返回promise 的包装,然后执行以下操作...)

如果doSomethingAsync给您一个诺言,您可以使用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将忽略第二个和第三个参数,则可以将其直接传递给mapmap使用三个参数调用其回调,但是大多数人在大多数情况下只使用第一个参数):

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

例:

请注意,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;,但是在以下某些示例中,我们没有索引使用。)

例:

(或者再次构建一个包装器,doSomethingAsync以给您一个承诺并执行以下操作...)

如果doSomethingAsync给您一个承诺,如果您可以使用ES2017 +语法(也许使用像Babel这样的编译器),则可以将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);
});

例:

如果还不能使用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);
});

例:

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

例:


1
您能解释一下if (--expecting === 0)代码的一部分如何工作吗?您的解决方案的回调版本对我而言非常有效,我只是不明白如何使用该语句检查已完成的响应数。赞赏的是我缺乏知识。有没有其他方法可以写支票?
莎拉(Sarah)

@Sarah:expecting从的值开始array.length,这是我们将要发出的请求数量。我们知道在所有这些请求启动之前,不会调用回调。在回调中,if (--expecting === 0)执行以下操作:1.减量expecting(我们收到了一个响应,因此我们希望得到一个更少的响应),如果减量的值为0(我们不希望有更多的响应),则完成!
TJ Crowder

1
@PatrickRoberts-谢谢!是的,复制和粘贴错误,该示例中的第二个参数被完全忽略(这是它没有失败的唯一原因,因为正如您所指出的那样,它results不存在)。:-) 固定它。
TJ Crowder

111

看一下这个例子:

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返回的是已解决的承诺(返回时已解决res.data.value)。因此,您可以等待$ http.get请求完成,然后执行console.log(res.joke)(作为常规的异步流程)。

这是plnkr:

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

ES6方式(异步-等待)

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

107

这是数据绑定存储概念两种方式的场所之一在许多新的JavaScript框架中使用的将非常适合您的地方之一...

因此,如果您使用的是Angular,React或任何其他通过两种方式进行数据绑定存储概念的框架,则此问题已为您简单解决,因此,简单来说,您的结果就undefined在第一阶段,因此您已经result = undefined获得了数据,那么一旦获得结果,它就会被更新并分配给您的Ajax调用响应的新值...

但是如何用纯JavaScriptjQuery做到这一点,如您在此问题中所提出的呢?

您可以使用callbackpromise和最近可观察到的为您处理它,例如,在promises中,我们有一些类似success()或的函数then(),当您的数据为您准备好时将执行该函数,与observable上的callback或subscription函数相同。

例如,在使用jQuery的情况下,可以执行以下操作:

$(document).ready(function(){
    function foo() {
        $.ajax({url: "api/data", success: function(data){
            fooDone(data); //after we have data, we pass it to fooDone
        }});
    };

    function fooDone(data) {
        console.log(data); //fooDone has the data and console.log it
    };

    foo(); //call happens here
});

有关更多信息,请参阅有关promiseobservable的信息,这是执行异步操作的新方法。


这在全局范围内很好,但是在某些模块上下文中,您可能想要确保回调的正确上下文,例如$.ajax({url: "api/data", success: fooDone.bind(this)});
steve.sims

8
这实际上是不正确的,因为React是单向数据绑定
Matthew Brent,

@MatthewBrent,您没看错,但也不对,React道具是对象,如果更改,它们会在整个应用程序中发生变化,但这不是React开发人员建议使用它的方式...
Alireza

98

在挣扎着JavaScript的“奥秘”时,这是我们面临的一个非常普遍的问题。今天让我尝试揭开这个谜团的神秘面纱。

让我们从一个简单的JavaScript函数开始:

function foo(){
// do something 
 return 'wohoo';
}

let bar = foo(); // bar is 'wohoo' here

这是一个简单的同步函数调用(其中每一行代码均按顺序在下一行之前“完成其工作”),并且结果与预期的相同。

现在,通过在函数中引入很少的延迟来增加一些扭曲,以免所有代码行都没有按顺序“完成”。因此,它将模拟功能的异步行为:

function foo(){
 setTimeout( ()=>{
   return 'wohoo';
  }, 1000 )
}

let bar = foo() // bar is undefined here

这样一来,延迟就破坏了我们期望的功能!但是到底发生了什么?好吧,如果您看一下代码,这实际上是很合逻辑的。该函数foo()在执行后不返回任何内容(因此返回的值为undefined),但它确实启动了一个计时器,该计时器在1秒后执行一个函数以返回“ wohoo”。但是正如您所看到的,分配给bar的值是foo()立即返回的内容,它什么也没有,就是just undefined

那么,我们如何解决这个问题呢?

让我们问一下函数PROMISE。Promise的确是关于它的含义:它意味着该函数保证您提供将来得到的任何输出。因此,让我们针对上面的小问题在实际中进行观察:

function foo(){
   return new Promise( (resolve, reject) => { // I want foo() to PROMISE me something
    setTimeout ( function(){ 
      // promise is RESOLVED , when execution reaches this line of code
       resolve('wohoo')// After 1 second, RESOLVE the promise with value 'wohoo'
    }, 1000 )
  })
}

let bar ; 
foo().then( res => {
 bar = res;
 console.log(bar) // will print 'wohoo'
});

因此,摘要是-为处理异步功能(例如基于ajax的调用等),您可以resolve对值使用承诺(打算返回)。因此,简而言之,您可以在异步函数中解析值而不是返回

更新(承诺异步/等待)

除了then/catch用于兑现承诺之外,还存在另一种方法。这个想法是识别异步函数,然后等待承诺解决,然后再转到下一行代码。它仍然只是promises内幕,但是采用了不同的句法方法。为了使事情更清楚,您可以在下面找到一个比较:

然后/捕获版本:

function saveUsers(){
     getUsers()
      .then(users => {
         saveSomewhere(users);
      })
      .catch(err => {
          console.error(err);
       })
 }

异步/等待版本:

  async function saveUsers(){
     try{
        let users = await getUsers()
        saveSomewhere(users);
     }
     catch(err){
        console.error(err);
     }
  }

仍然被认为是从promise或async / await返回值的最佳方法吗?
edwardsmarkf 18/09/26

3
@edwardsmarkf就我个人而言,我认为没有最好的方法。我在then / catch,async / await中使用promise,并在代码的异步部分中使用生成器。它在很大程度上取决于使用情况。
阿尼什·K

96

从异步函数返回值的另一种方法是传递一个对象,该对象将存储异步函数的结果。

这是一个相同的示例:

var async = require("async");

// This wires up result back to the caller
var result = {};
var asyncTasks = [];
asyncTasks.push(function(_callback){
    // some asynchronous operation
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;
            _callback();
        }
    });
});

async.parallel(asyncTasks, function(){
    // result is available after performing asynchronous operation
    console.log(result)
    console.log('Done');
});

我正在使用 result对象在异步操作期间存储值。这样即使在异步作业之后,结果仍然可用。

我经常使用这种方法。我想知道这种方法在通过连续模块回传结果时效果如何。


9
在这里使用对象没有什么特别的。如果您将他的回复直接分配给,那么效果也会很好result。之所以起作用,是因为异步功能完成,您正在读取变量。
菲利克斯·克林

85

尽管promise和callback在许多情况下都可以正常工作,但是表达类似以下内容很麻烦:

if (!name) {
  name = async1();
}
async2(name);

你最终会经历async1; 检查是否name未定义,并相应地调用回调。

async1(name, callback) {
  if (name)
    callback(name)
  else {
    doSomething(callback)
  }
}

async1(name, async2)

尽管在一些小示例中还可以,但是当您遇到很多类似的情况和错误处理时,它会很烦人。

Fibers 帮助解决问题。

var Fiber = require('fibers')

function async1(container) {
  var current = Fiber.current
  var result
  doSomething(function(name) {
    result = name
    fiber.run()
  })
  Fiber.yield()
  return result
}

Fiber(function() {
  var name
  if (!name) {
    name = async1()
  }
  async2(name)
  // Make any number of async calls from here
}

您可以在此处签出项目。


1
@recurf-这不是我的项目。您可以尝试使用他们的问题跟踪器。
rohithpr


1
这仍然有意义吗?
Aluan Haddad '18

async-await如果您正在使用某些最新版本的节点,则可以利用。如果有人坚持使用旧版本,则可以使用此方法。
rohithpr

83

我编写的以下示例显示了如何

  • 处理异步HTTP调用;
  • 等待每个API调用的响应;
  • 使用承诺模式;
  • 使用Promise.all模式来加入多个HTTP调用;

这个工作示例是独立的。它将定义一个简单的请求对象,该XMLHttpRequest对象使用window 对象进行调用。它将定义一个简单的函数来等待一堆承诺完成。

上下文。该示例正在查询Spotify Web API端点,以便playlist为给定的查询字符串集搜索对象:

[
 "search?type=playlist&q=%22doom%20metal%22",
 "search?type=playlist&q=Adele"
]

对于每一项,一个新的Promise将触发一个block- ExecutionBlock,解析结果,基于结果数组(即Spotify user对象的列表)安排一组新的Promise ,并ExecutionProfileBlock异步执行新的HTTP调用。

然后,您可以看到一个嵌套的Promise结构,该结构允许您产生多个完全异步的嵌套HTTP调用,并通过联接每个调用子集的结果Promise.all

注意 最近的Spotify searchAPI将要求在请求标头中指定访问令牌:

-H "Authorization: Bearer {your access token}" 

因此,要运行以下示例,需要将访问令牌放入请求标头中:

var spotifyAccessToken = "YourSpotifyAccessToken";
var console = {
    log: function(s) {
        document.getElementById("console").innerHTML += s + "<br/>"
    }
}

// Simple XMLHttpRequest
// based on https://davidwalsh.name/xmlhttprequest
SimpleRequest = {
    call: function(what, response) {
        var request;
        if (window.XMLHttpRequest) { // Mozilla, Safari, ...
            request = new XMLHttpRequest();
        } else if (window.ActiveXObject) { // Internet Explorer
            try {
                request = new ActiveXObject('Msxml2.XMLHTTP');
            }
            catch (e) {
                try {
                  request = new ActiveXObject('Microsoft.XMLHTTP');
                } catch (e) {}
            }
        }

        // State changes
        request.onreadystatechange = function() {
            if (request.readyState === 4) { // Done
                if (request.status === 200) { // Complete
                    response(request.responseText)
                }
                else
                    response();
            }
        }
        request.open('GET', what, true);
        request.setRequestHeader("Authorization", "Bearer " + spotifyAccessToken);
        request.send(null);
    }
}

//PromiseAll
var promiseAll = function(items, block, done, fail) {
    var self = this;
    var promises = [],
                   index = 0;
    items.forEach(function(item) {
        promises.push(function(item, i) {
            return new Promise(function(resolve, reject) {
                if (block) {
                    block.apply(this, [item, index, resolve, reject]);
                }
            });
        }(item, ++index))
    });
    Promise.all(promises).then(function AcceptHandler(results) {
        if (done) done(results);
    }, function ErrorHandler(error) {
        if (fail) fail(error);
    });
}; //promiseAll

// LP: deferred execution block
var ExecutionBlock = function(item, index, resolve, reject) {
    var url = "https://api.spotify.com/v1/"
    url += item;
    console.log( url )
    SimpleRequest.call(url, function(result) {
        if (result) {

            var profileUrls = JSON.parse(result).playlists.items.map(function(item, index) {
                return item.owner.href;
            })
            resolve(profileUrls);
        }
        else {
            reject(new Error("call error"));
        }
    })
}

arr = [
    "search?type=playlist&q=%22doom%20metal%22",
    "search?type=playlist&q=Adele"
]

promiseAll(arr, function(item, index, resolve, reject) {
    console.log("Making request [" + index + "]")
    ExecutionBlock(item, index, resolve, reject);
}, function(results) { // Aggregated results

    console.log("All profiles received " + results.length);
    //console.log(JSON.stringify(results[0], null, 2));

    ///// promiseall again

    var ExecutionProfileBlock = function(item, index, resolve, reject) {
        SimpleRequest.call(item, function(result) {
            if (result) {
                var obj = JSON.parse(result);
                resolve({
                    name: obj.display_name,
                    followers: obj.followers.total,
                    url: obj.href
                });
            } //result
        })
    } //ExecutionProfileBlock

    promiseAll(results[0], function(item, index, resolve, reject) {
        //console.log("Making request [" + index + "] " + item)
        ExecutionProfileBlock(item, index, resolve, reject);
    }, function(results) { // aggregated results
        console.log("All response received " + results.length);
        console.log(JSON.stringify(results, null, 2));
    }

    , function(error) { // Error
        console.log(error);
    })

    /////

  },
  function(error) { // Error
      console.log(error);
  });
<div id="console" />

在这里广泛讨论了该解决方案。



78

2017年答案:现在,您可以在每个当前的浏览器和节点中完全执行所需的操作

这很简单:

  • 退还承诺
  • 使用'await',它将告诉JavaScript等待将承诺解析为一个值(例如HTTP响应)
  • 'async'关键字添加到父函数

这是您的代码的有效版本:

(async function(){

var response = await superagent.get('...')
console.log(response)

})()

当前所有浏览器和节点8中都支持await


7
不幸的是,这仅适用于返回promise的函数–例如,它不适用于使用回调的Node.js API。我不建议在没有Babel的情况下使用它,因为不是每个人都使用“当前浏览器”。
米哈尔Perłakowski

2
@MichałPerłakowski节点8包含nodejs.org/api/util.html#util_util_promisify_original,可用于使node.js API返回承诺。您是否有时间和金钱来支持非当前浏览器显然取决于您的情况。
mikemaccana

令人遗憾的是,IE 11仍是2018年最新的浏览器,并且不支持await/async
Juan Mendes

IE11不是当前的浏览器。它发布于5年前,根据caniuse的数据,其全球市场份额为2.5%,除非有人将您的预算增加一倍以忽略所有当前技术,否则这并不值得大多数人花费时间。
mikemaccana

76

Js是单线程的。

浏览器可以分为三个部分:

1)事件循环

2)Web API

3)事件队列

事件循环永远运行,即无限循环。事件队列是将所有功能推送到某个事件(例如单击)的地方,这是逐个执行的,并放入事件循环中,该循环执行该功能并自行准备对于第一个函数执行完之后的下一个函数,这意味着直到事件循环中执行该函数的队列中的函数才开始执行一个函数。

现在让我们认为我们在队列中推送了两个函数,一个是从服务器获取数据,另一个是利用该数据。我们先在队列中推送了serverRequest()函数,然后是utiliseData()函数。serverRequest函数进入事件循环并调用服务器,因为我们不知道从服务器获取数据将花费多少时间,因此预计此过程将花费一些时间,因此我们忙于事件循环,从而挂起了页面,这就是Web API发挥作用,它从事件循环中获取此功能,并与服务器释放事件循环,以便我们可以从队列中执行下一个功能。队列中的下一个功能是utiliseData(),它进入了循环,但由于没有可用数据,它进入了浪费,下一个函数的执行一直持续到队列结束(这称为异步调用,即我们可以做其他事情直到获得数据)

假设我们的serverRequest()函数在代码中有一个return语句,当我们从服务器Web API取回数据时,它将在队列末尾将其压入队列。由于它在队列末尾被推送,因此我们无法利用其数据,因为队列中没有剩余功能可以利用此数据。因此,不可能从异步调用返回某些内容。

因此,解决方案是回调Promise

来自此处答案之一的图像,正确解释了回调的使用... 我们将函数(利用从服务器返回的数据的函数)提供给函数调用服务器。

打回来

 function doAjax(callbackFunc, method, url) {
  var xmlHttpReq = new XMLHttpRequest();
  xmlHttpReq.open(method, url);
  xmlHttpReq.onreadystatechange = function() {

      if (xmlHttpReq.readyState == 4 && xmlHttpReq.status == 200) {
        callbackFunc(xmlHttpReq.responseText);
      }


  }
  xmlHttpReq.send(null);

}

在我的代码中,它称为

function loadMyJson(categoryValue){
  if(categoryValue==="veg")
  doAjax(print,"GET","http://localhost:3004/vegetables");
  else if(categoryValue==="fruits")
  doAjax(print,"GET","http://localhost:3004/fruits");
  else 
  console.log("Data not found");
}

Javscript.info回调


68

您可以使用此自定义库(使用Promise编写)来进行远程调用。

function $http(apiConfig) {
    return new Promise(function (resolve, reject) {
        var client = new XMLHttpRequest();
        client.open(apiConfig.method, apiConfig.url);
        client.send();
        client.onload = function () {
            if (this.status >= 200 && this.status < 300) {
                // Performs the function "resolve" when this.status is equal to 2xx.
                // Your logic here.
                resolve(this.response);
            }
            else {
                // Performs the function "reject" when this.status is different than 2xx.
                reject(this.statusText);
            }
        };
        client.onerror = function () {
            reject(this.statusText);
        };
    });
}

简单用法示例:

$http({
    method: 'get',
    url: 'google.com'
}).then(function(response) {
    console.log(response);
}, function(error) {
    console.log(error)
});

67

另一种解决方案是通过顺序执行程序nsynjs执行代码。

如果底层功能被承诺

nsynjs将顺序评估所有promise,并将promise结果放入data属性中:

function synchronousCode() {

    var getURL = function(url) {
        return window.fetch(url).data.text().data;
    };
    
    var url = 'https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js';
    console.log('received bytes:',getURL(url).length);
    
};

nsynjs.run(synchronousCode,{},function(){
    console.log('synchronousCode done');
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

如果基础功能不明确

步骤1.将带有回调的函数包装到可感知nsynjs的包装器中(如果它具有约定的版本,则可以跳过此步骤):

var ajaxGet = function (ctx,url) {
    var res = {};
    var ex;
    $.ajax(url)
    .done(function (data) {
        res.data = data;
    })
    .fail(function(e) {
        ex = e;
    })
    .always(function() {
        ctx.resume(ex);
    });
    return res;
};
ajaxGet.nsynjsHasCallback = true;

步骤2.将同步逻辑放入功能中:

function process() {
    console.log('got data:', ajaxGet(nsynjsCtx, "data/file1.json").data);
}

步骤3.通过nsynjs以同步方式运行函数:

nsynjs.run(process,this,function () {
    console.log("synchronous function finished");
});

Nsynjs将逐步评估所有运算符和表达式,以防某些慢速函数的结果尚未就绪时暂停执行。

此处有更多示例:https : //github.com/amaksr/nsynjs/tree/master/examples


2
这是有趣的。我喜欢它允许如何使用其他语言编写异步调用代码。但是从技术上讲,这不是真正的JavaScript吗?
J莫里斯

41

ECMAScript 6具有“生成器”,使您可以轻松地以异步样式进行编程。

function* myGenerator() {
    const callback = yield;
    let [response] = yield $.ajax("https://stackoverflow.com", {complete: callback});
    console.log("response is:", response);

    // examples of other things you can do
    yield setTimeout(callback, 1000);
    console.log("it delayed for 1000ms");
    while (response.statusText === "error") {
        [response] = yield* anotherGenerator();
    }
}

要运行以上代码,请执行以下操作:

const gen = myGenerator(); // Create generator
gen.next(); // Start it
gen.next((...args) => gen.next([...args])); // Set its callback function

如果需要使用不支持ES6的浏览器,则可以通过Babel或闭包编译器运行代码以生成ECMAScript 5。

回调...args被包装在数组中,并在您读取它们时被解构,以便该模式可以处理具有多个参数的回调。例如,使用节点fs

const [err, data] = yield fs.readFile(filePath, "utf-8", callback);

39

以下是处理异步请求的一些方法:

  1. 浏览器承诺对象
  2. Q -JavaScript的Promise库
  3. A + Promises.js
  4. jQuery推迟了
  5. XMLHttpRequest API
  6. 使用回调概念-作为第一个答案的实现

示例:jQuery推迟实现以处理多个请求

var App = App || {};

App = {
    getDataFromServer: function(){

      var self = this,
                 deferred = $.Deferred(),
                 requests = [];

      requests.push($.getJSON('request/ajax/url/1'));
      requests.push($.getJSON('request/ajax/url/2'));

      $.when.apply(jQuery, requests).done(function(xhrResponse) {
        return deferred.resolve(xhrResponse.result);
      });
      return deferred;
    },

    init: function(){

        this.getDataFromServer().done(_.bind(function(resp1, resp2) {

           // Do the operations which you wanted to do when you
           // get a response from Ajax, for example, log response.
        }, this));
    }
};
App.init();


38

我们发现自己处于一个似乎沿着我们称为“时间”的维度发展的宇宙中。我们并不真正了解现在的时间,但是我们已经开发出抽象的概念和词汇,让我们进行推理和讨论:“过去”,“现在”,“未来”,“之前”,“之后”。

我们构建的计算机系统越来越多地将时间作为重要方面。某些事情将在将来发生。然后,在这些最初的事情最终发生之后,还需要发生其他事情。这是称为“异步性”的基本概念。在我们这个日益网络化的世界中,异步的最常见情况是等待某个远程系统响应某些请求。

考虑一个例子。您打电话给送牛奶的人,点些牛奶。到时,您想将其放入咖啡中。您现在不能将牛奶放入咖啡中,因为现在还没有。您必须等待它来之后再将其放入咖啡中。换句话说,以下操作无效:

var milk = order_milk();
put_in_coffee(milk);

因为JS有没有办法知道它需要等待order_milk完成它执行之前put_in_coffee。换句话说,它不知道order_milk异步的-直到将来某个时候才导致牛奶。JS和其他声明性语言无需等待即可执行另一个语句。

利用此事实的经典JS方法,即JS支持将函数作为可传递的一流对象的事实,是将函数作为参数传递给异步请求,并在异步请求完成后调用它的任务在将来的某个时候。这就是“回调”方法。看起来像这样:

order_milk(put_in_coffee);

order_milk开始,订购牛奶,然后,只有当牛奶到达时,它才会调用put_in_coffee

这种回调方法的问题在于,它污染了报告结果的函数的常规语义return;相反,函数不得通过调用作为参数给出的回调来报告其结果。同样,在处理较长的事件序列时,此方法可能很快变得笨拙。例如,假设我要等待牛奶放入咖啡中,然后再执行第三步,即喝咖啡。我最终需要写这样的东西:

order_milk(function(milk) { put_in_coffee(milk, drink_coffee); }

我要传递给put_in_coffee牛奶的位置,还有传递牛奶后要drink_coffee执行的动作()。这样的代码很难编写,读取和调试。

在这种情况下,我们可以将问题中的代码重写为:

var answer;
$.ajax('/foo.json') . done(function(response) {
  callback(response.data);
});

function callback(data) {
  console.log(data);
}

输入承诺

这是“诺言”概念的动机,“诺言”是一种特殊的价值类型,代表某种未来异步结果。它可以表示已经发生的事情,或者将来会发生的事情,或者可能永远不会发生的事情。Promise有一个名为的方法,then当实现了Promise表示的结果时,您将向该方法传递要执行的动作。

对于我们的牛奶和咖啡,我们设计order_milk为返回牛奶到达的承诺,然后将其指定put_in_coffeethen操作,如下所示:

order_milk() . then(put_in_coffee)

这样的优点之一是我们可以将它们串在一起以创建将来出现的序列(“链接”):

order_milk() . then(put_in_coffee) . then(drink_coffee)

让我们对您的特定问题应用承诺。我们将请求逻辑包装在一个函数中,该函数返回一个Promise:

function get_data() {
  return $.ajax('/foo.json');
}

实际上,我们所做的只是在return对的调用中添加了$.ajax。之所以可行,是因为jQuery $.ajax已经返回了一种类似于Promise的东西。(在实践中,我们无需包装细节,而是希望包装此调用,以便返回真实的诺言,或者使用其他方法来代替$.ajax。)现在,如果我们要加载文件并等待其完成并然后做点什么,我们可以简单地说

get_data() . then(do_something)

例如,

get_data() . 
  then(function(data) { console.log(data); });

使用promise时,我们最终将大量函数传递到then,因此使用更紧凑的ES6样式箭头功能通常会有所帮助:

get_data() . 
  then(data => console.log(data));

async关键字

但是,如果必须以一种同步方式编写代码,而以异步方式编写一种完全不同的方法,仍然存在一些含糊的不满。对于同步,我们写

a();
b();

但是如果a是异步的,那么我们必须保证

a() . then(b);

上面我们说过:“ JS无法知道它需要等待第一个调用完成才能执行第二个调用”。那岂不是很好,如果有一些方法来告诉JS呢?事实证明,await在特殊类型的函数(称为“异步”函数)中使用了该关键字。该功能是ES即将发布的版本的一部分,但在正确的预设下,已经可以在Babel等转译器中使用。这使我们可以简单地编写

async function morning_routine() {
  var milk   = await order_milk();
  var coffee = await put_in_coffee(milk);
  await drink(coffee);
}

就您而言,您将可以编写如下内容

async function foo() {
  data = await get_data();
  console.log(data);
}

37

简短答案:您的foo()方法立即返回,而函数返回后,调用将$ajax()异步执行。问题是一旦异步调用返回,如何或在何处存储结果。

在该线程中已经给出了几种解决方案。也许最简单的方法是将一个对象传递给该foo()方法,并在异步调用完成后将结果存储在该对象的成员中。

function foo(result) {
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;   // Store the async result
        }
    });
}

var result = { response: null };   // Object to hold the async result
foo(result);                       // Returns before the async completes

请注意,对的调用foo()仍然不会返回任何有用的信息。但是,异步调用的结果现在将存储在中result.response


14
尽管这可行,但实际上并不比分配给全局变量好。
Felix Kling 2015年

36

成功使用callback()内部函数foo()。尝试这种方式。它简单易懂。  

var lat = "";
var lon = "";
function callback(data) {
    lat = data.lat;
    lon = data.lon;
}
function getLoc() {
    var url = "http://ip-api.com/json"
    $.getJSON(url, function(data) {
        callback(data);
    });
}

getLoc();

29

问题是:

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

可以解释为:

如何使异步代码看起来同步

解决方案是避免回调,并结合使用Promisesasync / await

我想举一个Ajax请求的例子。

(尽管它可以用Javascript编写,但我更喜欢用Python编写,然后使用Transcrypt将其编译为Javascript 。这已经足够清楚了。)

让我们先启用JQuery的使用,有$可作为S

__pragma__ ('alias', 'S', '$')

定义一个返回Promise的函数,在本例中为Ajax调用:

def read(url: str):
    deferred = S.Deferred()
    S.ajax({'type': "POST", 'url': url, 'data': { },
        'success': lambda d: deferred.resolve(d),
        'error': lambda e: deferred.reject(e)
    })
    return deferred.promise()

使用异步代码,就好像它是同步的

async def readALot():
    try:
        result1 = await read("url_1")
        result2 = await read("url_2")
    except Exception:
        console.warn("Reading a lot failed")

29

使用承诺

这个问题最完美的答案是使用Promise

function ajax(method, url, params) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(this.responseText);
    };
    xhr.onerror = reject;
    xhr.open(method, url);
    xhr.send(params);
  });
}

用法

ajax("GET", "/test", "acrive=1").then(function(result) {
    // Code depending on result
})
.catch(function() {
    // An error occurred
});

可是等等...!

使用诺言有问题!

我们为什么要使用自己的自定义Promise?

我一直使用此解决方案一段时间,直到发现旧浏览器中出现错误:

Uncaught ReferenceError: Promise is not defined

因此,我决定为未定义的js编译器为ES3实现我自己的Promise类。只需在您的主要代码之前添加此代码,然后安全地使用Promise!

if(typeof Promise === "undefined"){
    function _classCallCheck(instance, Constructor) {
        if (!(instance instanceof Constructor)) { 
            throw new TypeError("Cannot call a class as a function"); 
        }
    }
    var Promise = function () {
        function Promise(main) {
            var _this = this;
            _classCallCheck(this, Promise);
            this.value = undefined;
            this.callbacks = [];
            var resolve = function resolve(resolveValue) {
                _this.value = resolveValue;
                _this.triggerCallbacks();
            };
            var reject = function reject(rejectValue) {
                _this.value = rejectValue;
                _this.triggerCallbacks();
            };
            main(resolve, reject);
        }
        Promise.prototype.then = function then(cb) {
            var _this2 = this;
            var next = new Promise(function (resolve) {
                _this2.callbacks.push(function (x) {
                    return resolve(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.catch = function catch_(cb) {
            var _this2 = this;
            var next = new Promise(function (reject) {
                _this2.callbacks.push(function (x) {
                    return reject(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.triggerCallbacks = function triggerCallbacks() {
            var _this3 = this;
            this.callbacks.forEach(function (cb) {
                cb(_this3.value);
            });
        };
        return Promise;
    }();
}

28

当然,有许多方法,例如同步请求,promise,但是根据我的经验,我认为您应该使用回调方法。Java的异步行为是很自然的。因此,您的代码段可以重写一些:

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            myCallback(response);
        }
    });

    return result;
}

function myCallback(response) {
    // Does something.
}

5
关于回调或JavaScript,本来就没有异步的。
Aluan Haddad '18

19

除了了解代码之外,还有2个概念是理解JS如何处理回调和异步性的关键。(哪怕一个字?)

事件循环和并发模型

您需要注意三件事:队列; 事件循环和堆栈

概括地说,事件循环就像项目管理器一样,它一直在侦听要运行的任何功能,并在队列和堆栈之间进行通信。

while (queue.waitForMessage()) {
   queue.processNextMessage();
}

一旦收到运行某条消息的消息,它将添加到队列中。队列是等待执行的事物的列表(例如您的AJAX请求)。像这样想象:

 1. call foo.com/api/bar using foobarFunc
 2. Go perform an infinite loop
 ... and so on

当这些消息之一要执行时,它将消息从队列中弹出并创建一个堆栈,JS在执行该消息中的指令时需要执行堆栈。因此,在我们的示例中,它被告知致电foobarFunc

function foobarFunc (var) {
  console.log(anotherFunction(var));
}

因此,foobarFunc需要执行的任何操作(在我们的示例中anotherFunction)将被压入堆栈。执行,然后被遗忘-事件循环将移至队列中的下一件事(或侦听消息)

这里的关键是执行顺序。那是

什么时候会运行

当您使用AJAX呼叫外部方或运行任何异步代码(例如setTimeout)时,JavaScript依赖于响应才能继续。

最大的问题是,它将何时获得响应?答案是我们不知道-事件循环正在等待该消息说“嘿,我快跑”。如果JS只是同步地等待该消息,则您的应用程序将冻结,并且将变得很糟糕。因此,JS在等待消息添加回队列的同时继续执行队列中的下一项。

这就是为什么在异步功能中我们使用了称为callbacks的东西。从字面上看,这有点像一个承诺。就像我保证在某个时候返回某些内容一样, jQuery使用称为deffered.done deffered.fail和的特定回调deffered.always。你可以在这里看到他们

因此,您需要做的是传递一个函数,该函数应在传递给它的数据的某个点执行。

因为回调不是立即执行,而是在以后执行,所以将引用传递给未执行的函数很重要。所以

function foo(bla) {
  console.log(bla)
}

因此大多数时候(但并非总是如此),您foo不会foo()

希望这会有所道理。当您遇到这样令人困惑的事情时,我强烈建议您完整阅读文档以至少了解它。这将使您成为更好的开发人员。


18

使用ES2017,您应该将此作为函数声明

async function foo() {
    var response = await $.ajax({url: '...'})
    return response;
}

并像这样执行它。

(async function() {
    try {
        var result = await foo()
        console.log(result)
    } catch (e) {}
})()

或Promise语法

foo().then(response => {
    console.log(response)

}).catch(error => {
    console.log(error)

})

第二个功能可以重用吗?
Zum Dummi

如果调用oncolse,log,如何使用结果?那时不是所有内容都进入控制台了吗?
肯·英格拉姆
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.