如何从异步调用返回响应?问题解决方案如果您没有在代码中使用jQuery,那么这个答案适合您您面临的问题Possible solutions

2022-08-29 21:45:36

如何从发出异步请求的函数返回响应/结果?foo

我试图从回调中返回值,以及将结果分配给函数内的局部变量并返回该局部变量,但这些方法实际上都没有返回响应 - 它们都返回或变量的初始值是什么。undefinedresult

接受回调的异步函数示例(使用 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`
}

答案 1

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

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

问题

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

下面是一个类比,希望它能使同步流和异步流之间的区别更清晰:

同步

想象一下,你给一个朋友打了个电话,让他帮你找点东西。虽然这可能需要一段时间,但你在电话上等待并盯着太空,直到你的朋友给你你需要的答案。

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

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

var item = findItem();

// Do something with item
doSomethingElse();

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

异步

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

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

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

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


解决方案

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

你问为什么不好?

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

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

在下文中,我们将介绍三种不同的解决方案,它们都是相互叠加的:

  • 带有异步/等待的承诺(ES2017+,如果您使用转译器或再生器,则在较旧的浏览器中可用)
  • 回调(在节点中很受欢迎)
  • promise with then() (ES2015+,如果您使用众多 promise 库之一,可在较旧的浏览器中使用)

这三者都在当前的浏览器和节点7+中可用。


ES2017+:具有异步/等待的承诺

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

async/await建立在承诺之上:函数总是返回承诺。 “解开”承诺,要么产生承诺被解析的值,要么在承诺被拒绝时抛出错误。asyncawait

重要:您只能在函数内部或 JavaScript 模块中使用。在模块之外不支持顶级,因此如果不使用模块,则可能必须创建异步 IIFE(立即调用的函数表达式)才能启动上下文。awaitasyncawaitasync

您可以在MDN上阅读有关异步等待的更多信息。

下面是一个详细说明上述延迟函数的示例: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 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);
})();

当前浏览器节点版本支持 。您还可以通过在再生器(或使用再生器的工具,如 Babel)的帮助下将代码转换为 ES5 来支持较旧的环境。async/await


让函数接受回调

回调是指将函数 1 传递给函数 2。函数 2 可以在函数 1 准备就绪时调用函数 1。在异步进程的上下文中,每当异步进程完成时,都会调用回调。通常,结果将传递给回调。

在问题的示例中,您可以使接受回调并将其用作回调。所以这个foosuccess

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将引用我们在调用它时传递给它的函数,并将其传递给 。即,一旦Ajax请求成功,就会调用并将响应传递给回调(可以使用 来引用,因为这是我们定义回调的方式)。foosuccess$.ajaxcallbackresult

您还可以在将响应传递给回调之前对其进行处理:

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) 的新功能,但它已经具有良好的浏览器支持。还有许多库实现了标准的承诺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).
  });
.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 的延迟对象

延迟对象是 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
}

此代码误解了上述异步问题。具体来说,在检查服务器上的“/password”页面时不会冻结代码 - 它向服务器发送请求,在等待时,它会立即返回jQuery Ajax Deferred对象,而不是来自服务器的响应。这意味着该语句将始终获取此延迟对象,将其视为 ,然后像用户登录一样继续。不好。$.ajax()iftrue

但修复方法很简单:

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 对象,请作为第三个参数传递给 .openfalse

jQuery

如果使用 jQuery,则可以将该选项设置为 。请注意,自 jQuery 1.8 起,此选项已弃用。然后,您仍然可以使用回调或访问 jqXHR 对象的属性:asyncfalsesuccessresponseText

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

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

小心!无法发出同步 JSONP 请求。JSONP本质上始终是异步的(甚至不考虑此选项的另一个原因)。


答案 2

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

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


您面临的问题

这是来自其他答案的“问题解释”的简短摘要,如果您在阅读本文后不确定,请阅读。

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

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

下面是一个简单的类比:

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

(小提琴)

The value of returned is since the part has not executed yet. AJAX acts like this, you're returning the value before the server got the chance to tell your browser what that value is.aundefineda=5

One possible solution to this problem is to code re-actively , telling your program what to do when the calculation completed.

function onComplete(a){ // When the code completes, do this
    alert(a);
}

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

This is called CPS. Basically, we're passing an action to perform when it completes, we're telling our code how to react when an event completes (like our AJAX call, or in this case the timeout).getFive

Usage would be:

getFive(onComplete);

Which should alert "5" to the screen. (Fiddle).

Possible solutions

There are basically two ways how to solve this:

  1. Make the AJAX call synchronous (let’s call it SJAX).
  2. Restructure your code to work properly with callbacks.

1. Synchronous AJAX - Don't do it!!

As for synchronous AJAX, don't do it! Felix's answer raises some compelling arguments about why it's a bad idea. To sum it up, it'll freeze the user's browser until the server returns the response and create a very bad user experience. Here is another short summary taken from MDN on why:

XMLHttpRequest supports both synchronous and asynchronous communications. In general, however, asynchronous requests should be preferred to synchronous requests for performance reasons.

In short, synchronous requests block the execution of code... ...this can cause serious issues...

If you have to do it, you can pass a flag. Here is how:

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. Restructure code

Let your function accept a callback. In the example code can be made to accept a callback. We'll be telling our code how to react when completes.foofoo

So:

var result = foo();
// Code that depends on `result` goes here

Becomes:

foo(function(result) {
    // Code that depends on `result`
});

Here we passed an anonymous function, but we could just as easily pass a reference to an existing function, making it look like:

function myHandler(result) {
    // Code that depends on `result`
}
foo(myHandler);

For more details on how this sort of callback design is done, check Felix's answer.

Now, let's define foo itself to act accordingly

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

(fiddle)

We have now made our foo function accept an action to run when the AJAX completes successfully. We can extend this further by checking if the response status is not 200 and acting accordingly (create a fail handler and such). Effectively it is solving our issue.

If you're still having a hard time understanding this, read the AJAX getting started guide at MDN.