如何从异步调用返回响应

2022-02-11 00:00:00 asynchronous javascript ajax

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

承诺挡路使用示例:

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`
}

推荐答案

<块引用>

→有关不同示例异步行为的更一般说明,请参阅Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference

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

问题

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

这里有一个类比,希望能更清楚地区分同步流和异步流:

同步

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

当您进行包含";Normal&Quot;code:

的函数调用时,也会发生同样的情况
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 the item
});
doSomethingElse();

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


解决方案

接受JavaScript的异步本质!虽然某些异步操作提供同步对等项(AJAX&qot;也是如此),但通常不鼓励使用它们,尤其是在浏览器上下文中。

您问为什么不好?

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

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

在以下内容中,我们将介绍三种不同的解决方案,它们都是建立在彼此之上的:

  • 承诺async/await(ES2017+,如果您使用转换机或再生器,在较旧的浏览器中可用)
  • 回调(节点热门)
  • Promises withthen()(ES2015+,如果您使用众多Promise库中的一个,则可在较旧的浏览器中使用)

这三个都可以在当前浏览器和Node 7+中使用。


ES2017+:承诺<[200-11]>

2017年发布的ECMAScript版本引入了对异步函数的语法级支持。借助asyncawait,您可以用同步风格编写异步代码。代码仍然是异步的,但更易于阅读/理解。

async/await构建在承诺之上:async函数总是返回承诺。await展开承诺(&Q;),结果为承诺解析时使用的值,或者在承诺被拒绝时抛出错误。

重要提示:您只能在async函数内部使用await。目前尚不支持顶级await,因此您可能需要创建异步生命(Immediately Invoked Function Expression)才能启动async上下文。

您可以在MDN上阅读有关<[200-14]>和<[200-15]>的更多信息。

这里有一个示例,详细说明了上面的Delay函数findItem()

// 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);
})();
当前browser和node版本支持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'
});

在这里我们定义了函数";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+:承诺then()

Promise API是ECMAScript 6(ES2015)的一个新特性,但已经有了很好的browser support。还有许多库实现了标准的Promises API,并提供了其他方法来简化异步函数的使用和组合(例如,bluebird)。

承诺是未来值的容器。承诺收到该值(已解析)或取消(已拒绝)时,它会通知要访问此值的所有";侦听器(&q;)。

与普通回调相比,优势在于它们允许您将代码解耦,并且更易于组合。

以下是使用承诺的示例:

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).
  });
.as-console-wrapper { max-height: 100% !important; top: 0; }

应用于我们的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("https://jsonplaceholder.typicode.com/todos/1")
  .then(function(result) {
    console.log(result); // Code depending on result
  })
  .catch(function() {
    // An error occurred
  });
.as-console-wrapper { max-height: 100% !important; top: 0; }

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

有关承诺的详细信息:HTML5 rocks - JavaScript Promises。

旁注:jQuery的延迟对象

Deferred objects是jQuery对Promise的自定义实现(Promise API标准化之前)。它们的行为几乎像承诺,但公开的API略有不同。

jQuery的每个Ajax方法都已经返回一个";延迟对象&qot;(实际上是延迟对象的承诺),您只需从函数返回:

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

不推荐:同步AJAX&qot;调用

正如我所说,有些(!)异步操作具有同步对等项。我不提倡使用它们,但是为了完整起见,下面是执行同步调用的方式:

不带jQuery

如果您直接使用<[200-39]>对象,请将false作为第三个参数传递给<[200-41]>。

jQuery

如果使用jQuery,可以将async选项设置为false。请注意,从jQuery 1.8开始,此选项已过时。 然后,您可以仍然使用success回调或访问jqXHR object:

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

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

平视!无法同步JSONP请求。JSONP本质上总是异步的(这是不考虑此选项的又一个原因)。

相关文章