提问者:小点点

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


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

共3个答案

匿名用户

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

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

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

这里有一个类比,希望能使同步流和异步流之间的区别更清楚:

想象一下,你给一个朋友打了一个电话,让他帮你找些东西。 虽然这可能需要一段时间,但你在电话里等着,凝视着天空,直到你的朋友给出你需要的答案。

当您进行包含“普通”代码的函数调用时也会发生同样的情况:

function findItem() {
    var item;
    while(item_not_found) {
        // search
    }
    return item;
}

var item = findItem();

// Do something with item
doSomethingElse();

尽管findItem可能需要很长时间执行,但是var item=findItem();之后的任何代码都必须等待函数返回结果。

你又以同样的理由打电话给你的朋友。 但这次你告诉他你很赶时间,他应该用你的手机给你回电话。 你挂断电话,离开房子,做你想做的事。 一旦你的朋友给你回电话,你就是在处理他给你的信息。

这正是执行Ajax请求时所发生的情况。

findItem(function(item) {
    // Do something with item
});
doSomethingElse();

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

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

你问为什么不好?

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

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

下面我们将介绍三种不同的解决方案,它们都是在彼此的基础上构建的:

  • 承诺使用async/await(ES2017+,如果您使用transpiler或regenerator,则可在较旧的浏览器中使用)
  • 回调(节点中常用)
  • 使用then()(ES2015+,如果您使用许多promise库之一,则可在较旧的浏览器中获得)

目前的浏览器和Node7+中都有这三个版本。

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

async/await构建在承诺之上:async函数总是返回一个承诺。 await“展开”承诺,如果承诺被拒绝,则会产生用于解析承诺的值或抛出错误。

重要提示:您只能在async函数中使用await。 目前还不支持顶级的await,因此您可能必须创建一个异步生命(立即调用的函数表达式)来启动async上下文。

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

下面是一个建立在上述延迟之上的示例:

// 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。 您还可以通过在regenerator(或使用regenerator的工具,如Babel)的帮助下将代码转换为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响应只是一个事件。
当您必须使用第三方代码时可能会出现困难,但是大多数问题可以通过考虑应用程序流来解决。

Promise API是ECMAScript6(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承诺

延迟对象是jQuery对承诺的自定义实现(在承诺API标准化之前)。 它们的行为几乎像承诺一样,但公开了一个稍微不同的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延迟对象,而不是服务器的响应。 这意味着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
});

正如我提到的,一些(!) 异步操作有同步对应的操作。 我不提倡使用它们,但是为了完整起见,下面是您执行同步调用的方法:

如果直接使用XMLHttpRequest对象,请将false作为第三个参数传递给.open

如果使用jQuery,可以将async选项设置为false。 请注意,自从jQuery1.8以来,该选项已被弃用。 然后,您仍然可以使用success回调或访问jqXHR对象的responsetext属性:

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

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

抬头! 不可能发出同步JSONP请求。 JSONP本质上总是异步的(甚至不考虑这个选项的又一个原因)。

匿名用户

您的代码应该大致如下:

function foo() {
    var httpRequest = new XMLHttpRequest();
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
    return httpRequest.responseText;
}

var result = foo(); // always ends up being 'undefined'

FelixKling为使用jQuery for AJAX的人编写了一个答案,我决定为不使用jQuery for AJAX的人提供一个替代方案。

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

这是另一个答案对“问题的解释”的简短总结,如果你看完这个还不确定,那就看那个。

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

这意味着当您返回时,您定义的侦听器还没有执行,这意味着您返回的值还没有定义。

这里有一个简单的类比

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

(小提琴)

null

这个问题的一个可能的解决方案是重新编码,告诉你的程序当计算完成时该做什么。

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. 重新构造代码以正确使用回调。

至于同步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);
}

让函数接受回调。 在示例中,可以使代码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入门指南。

匿名用户

XMLHttpRequest2(首先阅读Benjamin Gruenbaum和Felix Kling的答案)

如果您不使用jQuery,并且想要一个简单的XMLHttpRequest2,它可以在现代浏览器和移动浏览器上运行,我建议您这样使用它:

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. 还有一些我不记得的其他情况使XMLHttpRequest1很烦人。

有两种方法可以获得此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高级功能

它支持所有现代浏览器。 因为XMLHttpRequest2已经存在,所以我可以确认我正在使用这种方法。 在我使用的所有浏览器上,我从来没有遇到过任何类型的问题。

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

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

话虽如此。。。为什么不简单点呢?

正如注释中提到的,错误和错误的使用 同步确实完全打破了答案的要点。 哪一种是正确使用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传递,它也不会抛出错误。

在第一种情况下,错误位于this.statusText下的DisplayAjax()内,因为方法不允许

在第二种情况下,它简单地起作用。 您必须在服务器端检查您是否传递了正确的post数据。

不允许跨域自动引发错误。

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

只有this.type设置为error。

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

因此:如果您能够正确复制和粘贴URL,就不需要进行错误检查。 ;)

PS:作为第一个测试,我写了x(“x”,displayAjax)。。。,它完全得到了响应。。。??? 所以我检查了HTML所在的文件夹,有一个名为'X.xml'的文件。 因此,即使您忘记了文件的扩展名,XMLHttpRequest2也会找到它。 我笑了

同步读取文件

别那么做。

如果您想阻塞浏览器一段时间,请同步加载一个很大的.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 Only.。。

但是您可以使用XMLHttpRequest2做很多事情:

我制作了一个下载管理器(使用简历,文件阅读器,文件系统两边的范围),使用画布的各种图像大小转换器,使用base64images填充web SQL数据库等等。 但在这些情况下,您应该只为此目的创建一个函数。。。 有时你需要一个blob,数组缓冲区,你可以设置头,重写mimetype,还有很多。。。

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

相关问题