如何返回来自异步调用的响应?

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

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

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

问题

Ajax中的A代表异步 。 这意味着发送请求(或接收响应)将从正常执行流程中取出。 在你的例子中, $.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();之后的任何代码都会执行var item = findItem(); 必须等到函数返回结果。

异步

你出于同样的原因再次给你的朋友打电话。 但是这次你告诉他你很匆忙,他应该用手机给你回电话。 你挂断电话,离开家,做你计划要做的事情。 一旦你的朋友给你回电话,你正在处理他给你的信息。

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

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

执行程序不会等待响应,而是执行Ajax调用之后的语句。 为了最终得到响应,您提供了一个函数,一旦接收到响应,就会调用一个函数(注意某事?回调?)。 在调用之前执行的任何语句都会在调用回调之前执行。



解决方案(S)

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

你问为什么不好?

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

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

下面我们将看看三种不同的解决方案,它们都是建立在彼此之上的:

  • 承诺async/await (ES2017 +,如果您使用转换器或再生器,则可在旧版浏览器中使用)
  • 回调 (在节点中流行)
  • then()承诺 (ES2015 +,如果您使用许多承诺库之一,则可以在旧版浏览器中使用)
  • 所有这三个都可以在当前的浏览器和节点7+中使用。


    ES2017 +:承诺与async/await

    2017年发布的新ECMAScript版本引入了对异步函数的语法级支持。 借助asyncawait ,您可以在“同步风格”中编写异步代码。 尽管没有错误:代码仍然是异步的,但它更易于阅读/理解。

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

    重要提示:您只能在async功能中使用await 。 这意味着,在最高层,你仍然需要直接处理承诺。

    您可以在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 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 。 您还可以借助再生器(或使用再生器的工具,如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响应不是别的,而是一个事件。
    当您需要使用第三方代码时,可能会遇到困难,但大多数问题都可以通过思考应用程序流程来解决。


    ES2015 +:承诺然后()

    Promise 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调用中,我们可以使用如下的promise:

    function ajax(url) {
      return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.onload = function() {
          resolve(this.responseText);
        };
        xhr.onerror = reject;
        xhr.open('GET', url);
        xhr.send();
      });
    }
    
    ajax("/echo/json")
      .then(function(result) {
        // Code depending on result
      })
      .catch(function() {
        // An error occurred
      });
    

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

    有关承诺的更多信息:HTML5 rocks - JavaScript Promises

    附注:jQuery的延期对象

    延迟对象是jQuery的promise的自定义实现(在Promise 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 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本质上总是异步的(还有一个理由甚至没有考虑这个选项)。


    如果你在你的代码中没有使用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的人写了一个答案,我决定为不是的人提供一个替代方案。

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


    你在面对什么

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

    AJAX中的A代表异步 。 这意味着发送请求(或接收响应)将从正常执行流程中取出。 在你的例子中, .send立即返回,下.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”。 (小提琴)。

    可能的解决方案

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

  • 使AJAX调用同步(让我们称之为SJAX)。
  • 重构您的代码以使用回调正常工作。
  • 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并进行相应的操作来进一步扩展(创建失败处理程序等)。 有效解决我们的问题。

    如果您仍然很难理解,请阅读MDN的AJAX入门指南。


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

    如你看到的:

  • 它比列出的所有其他功能更短。
  • 回调直接设置(所以不需要额外的不必要的关闭)。
  • 它使用新的onload(所以你不必检查readystate &&状态)
  • 还有其他一些我不记得使XMLHttpRequest 1令人讨厌的情况。
  • 有两种方法可以获得此Ajax调用的响应(三个使用XMLHttpRequest var名称):

    最简单的:

    this.response
    

    或者,如果由于某种原因, bind()回调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);
    

    正如你所看到的,我没有实现同步......这是一件坏事。

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


    正如在评论中提到的那样,错误&&同步的使用确实彻底打破了答案的重点。 以适当的方式使用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,在这种情况下,每个浏览器都会抛出一个错误。

    错误处理程序可能有用,如果您设置自定义标题,设置响应类型为BLOB数组缓冲区或任何....

    即使您通过'POSTAPAPAP'作为方法,它也不会抛出错误。

    即使您将'fdggdgilfdghfldj'作为formdata传递,它也不会抛出错误。

    在第一种情况下,错误位于this.statusText下的displayAjax()内部,因为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做很多事情:

    我做了一个下载管理器(使用resume,filereader,filesystem两边都有range),使用canvas的各种图像缩放转换器,使用base64images填充websql数据库等等......但是在这些情况下,你应该只为此目的创建一个函数...有时你需要一个blob,数组缓冲区,你可以设置标题,覆盖mimetype,还有更多...

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

    链接地址: http://www.djcxy.com/p/33813.html

    上一篇: How do I return the response from an asynchronous call?

    下一篇: Should a function have only one return statement?