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

标签 javascript jquery ajax asynchronous

我有一个发出Ajax请求的函数foo。如何从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`.

最佳答案

→有关不同示例的异步行为的更一般说明,请参见Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference
  
  →如果您已经理解问题,请跳至下面的可能解决方案。


问题

Ajax中的A代表asynchronous。这意味着发送请求(或更确切地说接收响应)已从正常执行流程中删除。在您的示例中,$.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 +,如果您使用许多promise库之一,则在较旧的浏览器中可用)


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



ES2017 +:带有async/await的承诺

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

async/await建立在promise之上:async函数总是返回promise。 await“取消包装”承诺,并导致承诺被解决的值或如果承诺被拒绝则引发错误。

重要提示:您只能在await函数内使用async。目前,尚不支持顶级await,因此您可能必须进行异步IIFE(Immediately Invoked Function Expression)才能启动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);
})();


当前的browsernode版本支持async/await。您还可以通过在regenerator(或使用再生器的工具,例如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
    });
}


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 +:带有then()的承诺

Promise API是ECMAScript 6(ES2015)的新功能,但已经具有良好的browser support。还有许多实现标准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 rocks - JavaScript Promises

旁注:jQuery的延迟对象

Deferred objects是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 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/59446989/

相关文章:

jquery - 如果 li 在 ajax 加载的内容中有 ul

javascript - 我如何在 moment.js 中执行每周警报/功能

php - 基于mysql表中唯一电子邮件地址的唯一代码?

javascript - 在循环中等待嵌套的 Promise

javascript - 在 d3.js 中更新数据时无法使 .exit() 和 .enter() 正常工作

javascript - 具有动态 ID 的目标特定元素但其他元素不相似

Jquery UI 选项卡在 AngularJS 中不起作用

javascript - Angular Firebase $createUser 函数返回错误

jQuery:当div变得可见时如何为其绑定(bind)事件?

php - 单击按钮使用 jquery 或 javascript 或 php 更改 &lt;script&gt; 标签内的文本