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

标签 javascript jquery ajax asynchronous

我有一个函数foo,它发出一个异步请求。如何从foo返回响应/结果?
我尝试从回调中返回值,并将结果分配给函数内的局部变量并返回该局部变量,但是这些方法均未真正返回响应(它们均返回undefined或变量result的初始值是)。
使用jQuery的ajax函数的示例:

function foo() {
    var result;

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

    return result; // It always returns `undefined`
}
使用node.js的示例:
function foo() {
    var result;

    fs.readFile("path/to/file", function(err, data) {
        result = data;
        // return data; // <- I tried that one as well
    });

    return result; // It always returns `undefined`
}
使用 promise 的then块的示例:
function foo() {
    var result;

    fetch(url).then(function(response) {
        result = response;
        // return response; // <- I tried that one as well
    });

    return result; // It always returns `undefined`
}

最佳答案

→ For a more general explanation of async behaviour with different examples, please see Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference

→ If you already understand the problem, skip to the possible solutions below.


问题
Ajax中的 代表asynchronous。这意味着发送请求(或更确切地说接收响应)已从正常执行流程中删除。在您的示例中,$.ajax立即返回,并且在调用作为return result;回调传递的函数之前,将执行下一条语句success
这是一个类比,希望可以使同步流和异步流之间的区别更加清晰:
同步
假设您打了一个电话给 friend ,并请他为您找东西。尽管可能要花一些时间,但您还是要在电话上凝视着太空,直到您的 friend 给您所需的答案为止。
当您进行包含“普通”代码的函数调用时,也会发生相同的情况:
function findItem() {
    var item;
    while(item_not_found) {
        // search
    }
    return item;
}

var item = findItem();

// Do something with item
doSomethingElse();
即使findItem可能需要很长时间才能执行,但var item = findItem();之后的任何代码都必须等到该函数返回结果之后。
异步
您出于相同的原因再次给您的 friend 打电话。但是这次您告诉他您很着急,他应该用您的手机给您回电。您挂断电话,离开房屋,然后按计划做。一旦您的 friend 给您回电,您就可以处理他提供给您的信息。
这正是您执行Ajax请求时发生的事情。
findItem(function(item) {
    // Do something with the item
});
doSomethingElse();
无需等待响应,而是立即继续执行,并执行Ajax调用后的语句。为了最终获得响应,您提供了一个在收到响应后立即调用的函数,即回调(注意什么?回叫?)。在调用之后执行的任何语句都将在调用回调之前执行。

解决方案
拥抱JavaScript的异步特性! 尽管某些异步操作提供了同步对应项(“Ajax”也是如此),但通常不建议使用它们,尤其是在浏览器上下文中。
你问为什么不好?
JavaScript在浏览器的UI线程中运行,任何长时间运行的进程都将锁定UI,从而使其无响应。此外,JavaScript的执行时间有上限,浏览器会询问用户是否继续执行。
所有这些都是非常糟糕的用户体验。用户将无法判断一切是否正常。此外,对于连接速度较慢的用户,效果会更糟。
在下面的内容中,我们将研究三个互为基础的不同解决方案:
  • 带有async/await的 promise (ES2017 +,如果使用转译器或再生器,则在较旧的浏览器中可用)
  • 回调(在节点中受欢迎)
  • 带有then()的 promise (ES2015 +,如果您使用多个Promise库之一,则可在旧版浏览器中使用)

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

    ES2017 +:带有 async/await 的 promise
    2017年发布的ECMAScript版本引入了对异步功能的语法级支持。借助asyncawait,您可以以“同步样式”编写异步代码。该代码仍然是异步的,但更易于阅读/理解。async/await建立在promise之上:async函数总是返回promise。 await“解包” promise ,或者导致 promise 被解决的值,或者如果 promise 被拒绝,则抛出错误。
    重要提示:您只能在await函数内使用async。目前,尚不支持顶层await,因此您可能必须进行异步IIFE(Immediately Invoked Function Expression)才能启动async上下文。
    您可以在MDN上阅读有关 async await 的更多信息。
    这是一个基于以上延迟的示例:
    // 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);
    })();
    
    当前的browsernode版本支持async/await。您还可以通过使用regenerator(或使用再生器的工具,例如Babel)将代码转换为ES5来支持较旧的环境。

    让函数接受回调
    函数1传递给函数2时即发生回调。函数2准备就绪后便可以调用函数1。在异步过程的上下文中,只要完成异步过程,就会调用回调。通常,结果将传递给回调。
    在问题的示例中,您可以使foo接受回调并将其用作success回调。所以这
    var result = foo();
    // Code that depends on 'result'
    
    变成
    foo(function(result) {
        // Code that depends on 'result'
    });
    
    在这里,我们定义了函数“内联”,但是您可以传递任何函数引用:
    function myCallback(result) {
        // Code that depends on 'result'
    }
    
    foo(myCallback);
    
    foo本身定义如下:
    function foo(callback) {
        $.ajax({
            // ...
            success: callback
        });
    }
    
    callback将引用我们在调用时传递给foo的函数,并将其传递给success。 IE。一旦Ajax请求成功,$.ajax将调用callback并将响应传递给回调(可以通过result进行引用,因为这是我们定义回调的方式)。
    您还可以在将响应传递给回调之前对其进行处理:
    function foo(callback) {
        $.ajax({
            // ...
            success: function(response) {
                // For example, filter the response
                callback(filtered_response);
            }
        });
    }
    
    使用回调编写代码比看起来容易。毕竟,浏览器中的JavaScript很大程度上是事件驱动的(DOM事件)。接收Ajax响应不过是一个事件。
    当您必须使用第三方代码时,可能会遇到困难,但是大多数问题可以通过思考应用程序流程来解决。

    ES2015 +:带有then()的 promise
    Promise API是ECMAScript 6(ES2015)的新功能,但它已经有了不错的browser support。还有许多实现标准Promises API的库,并提供其他方法来简化异步函数(例如bluebird)的使用和组合。
    promise 是 future 值(value)的容器。当promise收到该值(已解决)或被取消(拒绝)时,它会通知要访问此值的所有“监听器”。
    与普通回调相比,优点是它们使您可以解耦代码,并且更易于编写。
    这是使用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调用,我们可以使用如下 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
      });
    
    描述promise提供的所有优点超出了此答案的范围,但是如果您编写新代码,则应认真考虑它们。它们为您的代码提供了很好的抽象和分离。
    有关 promise 的更多信息:HTML5 rocks - JavaScript Promises
    旁注:jQuery的延迟对象
    Deferred objects是jQuery的promise的自定义实现(在Promise API标准化之前)。它们的行为几乎像promise,但是暴露了稍微不同的API。
    jQuery的每个Ajax方法都已经返回了“延迟对象”(实际上是对延迟对象的 promise ),您可以从函数中返回它:
    function ajax() {
        return $.ajax(...);
    }
    
    ajax().done(function(result) {
        // Code depending on result
    }).fail(function() {
        // An error occurred
    });
    
    旁注: promise 陷阱
    请记住, promise 和递延对象仅仅是获得 future 值(value)的容器,而不是值(value)本身。例如,假设您具有以下内容:
    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 objectresponseText属性:
    function foo() {
        var jqXHR = $.ajax({
            //...
            async: false
        });
        return jqXHR.responseText;
    }
    
    如果使用其他jQuery Ajax方法(例如$.get$.getJSON等),则必须将其更改为$.ajax(因为只能将配置参数传递给$.ajax)。
    抬头! 无法进行同步JSONP请求。 JSONP就其本质而言始终是异步的(还有一个甚至不考虑此选项的原因)。

    关于javascript - 如何从异步调用返回响应?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66474418/

    相关文章:

    java - 如何显示在 html 中存储为字符串数组的图像?

    java - 如何从 JarClassLoader 和 Applet Launcher 调用返回字符串数据的 applet 方法

    javascript - 使用 jQuery 检测滚动条是否仍然困难?

    javascript - 防止 select2-dropdown 在接近动画时被分离

    javascript - 使用下拉按钮更新 Bootstrap 拆分按钮文本和功能

    javascript - AJAX 返回代码 200 但引发错误

    javascript - 根据浏览器窗口大小调整 JS 模式对话框

    javascript - TypeError : Object. defineProperty 在非对象 JQUERY 上调用

    javascript - 我的 ajax 查询 CODEIGNITER 出现错误

    php - 如何在进入数据库后触发请求?