如何从异步调用返回响应 [英] How to return the response from an asynchronous call

查看:39
本文介绍了如何从异步调用返回响应的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个函数 foo 可以发出异步请求.如何从 foo 返回响应/结果?

我正在尝试从回调中返回值,并将结果分配给函数内部的一个局部变量并返回该变量,但这些方法都没有真正返回响应(它们都返回 undefined 或任何变量 result 的初始值).

接受回调的异步函数示例(使用jQuery的ajax函数)

function foo() {变量结果;$.ajax({网址:'...',成功:功能(响应){结果 = 响应;//返回响应;//<- 我也试过了}});返回结果;//它总是返回 `undefined`}

使用 Node.js 的示例:

function foo() {变量结果;fs.readFile("path/to/file", function(err, data) {结果=数据;//返回数据;//<- 我也试过了});返回结果;//它总是返回 `undefined`}

使用 promise 的 then 块的示例:

function foo() {变量结果;获取(网址).然后(功能(响应){结果 = 响应;//返回响应;//<- 我也试过了});返回结果;//它总是返回 `undefined`}

解决方案

→ 有关不同示例的异步行为的更一般说明,请参阅 为什么我的变量在我在函数内部修改它?- 异步代码参考

→ 如果您已经了解问题,请跳至以下可能的解决方案.

问题

Ajax 中的 A 代表 异步.这意味着发送请求(或者更确切地说是接收响应)已经脱离了正常的执行流程.在您的示例中,$.ajax 立即返回,下一个语句 return result; 在您作为 success 回调传递的函数之前执行甚至打电话.

这是一个类比,希望可以更清楚地区分同步流和异步流:

同步

想象一下,您给朋友打电话,让他​​为您查找一些东西.虽然这可能需要一段时间,但您会等待电话并凝视太空,直到您的朋友给您所需的答案.

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

function findItem() {变量项目;而(item_not_found){//搜索}归还物品;}var item = findItem();//对 item 做一些事情doSomethingElse();

即使 findItem 可能需要很长时间来执行,任何在 var item = findItem(); 之后的代码都必须等待直到函数返回结果.

异步

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

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

findItem(function(item) {//对项目做一些事情});doSomethingElse();

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


解决方案

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

你问为什么不好?

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

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

在下文中,我们将研究三种不同的解决方案,它们都建立在彼此之上:

  • Promises with async/await(ES2017+,如果您使用转译器或再生器,则可在旧浏览器中使用)
  • 回调(在节点中流行)
  • Promises with then()(ES2015+,如果您使用众多 Promise 库之一,则可在旧浏览器中使用)

所有三个都在当前浏览器和节点 7+ 中可用.


ES2017+: Promises with async/await

2017 年发布的 ECMAScript 版本为异步函数引入了语法级支持.在asyncawait 的帮助下,您可以以同步风格"编写异步.代码仍然是异步的,但更容易阅读/理解.

async/await 建立在 promise 之上:async 函数总是返回一个 promise.await 解包"一个承诺,要么导致承诺被解决的价值,要么在承诺被拒绝时抛出错误.

重要提示:您只能在 async 函数内使用 await.目前,尚不支持顶级 await,因此您可能需要创建一个异步 IIFE (立即调用函数表达式) 以启动async 上下文.

您可以阅读更多关于asyncawait 在 MDN 上.

下面是一个例子,详细说明了上面的delay函数findItem():

//使用将返回承诺的superagent".var superagent = require('superagent')//这不是声明为`async`,因为它已经返回了一个promise函数延迟(){//`delay` 返回一个承诺返回新的承诺(功能(解决,拒绝){//只有 `delay` 能够解决或拒绝承诺设置超时(函数(){解决(42);//3秒后,解析值为42的promise}, 3000);});}异步函数 getAllBooks() {尝试 {//获取当前用户的图书ID列表var bookIDs = await superagent.get('/user/books');//等待 3 秒(只是为了这个例子)等待延迟();//获取每本书的信息return await superagent.get('/books/ids='+JSON.stringify(bookIDs));} 捕捉(错误){//如果任何等待的承诺被拒绝,这个 catch 块//将捕获拒绝原因返回空;}}//启动一个 IIFE 以在顶层使用 `await`(异步函数(){让书籍 = 等待 getAllBooks();控制台日志(书籍);})();

当前的浏览器node 版本支持 async/await.您还可以通过在 regenerator(或使用 regenerator,例如 Babel).


让函数接受回调

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

在问题示例中,您可以让 foo 接受回调并将其用作 success 回调.所以这个

var 结果 = foo();//取决于结果"的代码

变成

foo(function(result) {//取决于结果"的代码});

这里我们定义了函数inline"但您可以传递任何函数引用:

function myCallback(result) {//取决于结果"的代码}富(我的回调);

foo 本身定义如下:

function foo(callback) {$.ajax({//...成功:回调});}

callback 将引用我们在调用时传递给 foo 的函数,并将其传递给 success.IE.一旦 Ajax 请求成功,$.ajax 将调用 callback 并将响应传递给回调(可以用 result 引用,因为这就是我们定义回调的方式).

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

function foo(callback) {$.ajax({//...成功:功能(响应){//例如,过滤响应回调(filtered_response);}});}

使用回调编写代码比看起来更容易.毕竟,浏览器中的 JavaScript 是高度事件驱动的(DOM 事件).接收 Ajax 响应只不过是一个事件.当您必须使用第三方代码时可能会出现困难,但大多数问题都可以通过考虑应用程序流程来解决.


ES2015+:使用 then()

Promise API 是一个新的ECMAScript 6 (ES2015) 的特性,但它已经有很好的浏览器支持.还有许多库实现了标准的 Promises API 并提供了额外的方法来简化异步函数的使用和组合(例如,蓝鸟).

承诺是未来价值的容器.当 promise 收到值(resolved)或被取消(rejected)时,它会通知它的所有侦听器"谁想要访问此值.

与普通回调相比的优势在于,它们允许您解耦代码并且更易于编写.

以下是使用 Promise 的示例:

function delay() {//`delay` 返回一个承诺返回新的承诺(功能(解决,拒绝){//只有 `delay` 能够解决或拒绝承诺设置超时(函数(){解决(42);//3秒后,解析值为42的promise}, 3000);});}延迟().then(function(v) {//`delay` 返回一个 promise控制台日志(v);//解析后记录该值}).catch(函数(v){//或者如果被拒绝则做其他事情//(在本例中不会发生,因为没有调用`reject`).});

.as-console-wrapper { max-height: 100% !important;顶部:0;}

应用于我们的 Ajax 调用,我们可以使用这样的承诺:

function ajax(url) {返回新的承诺(功能(解决,拒绝){var xhr = new XMLHttpRequest();xhr.onload = 函数(){解决(this.responseText);};xhr.onerror = 拒绝;xhr.open('GET', url);xhr.send();});}ajax("https://jsonplaceholder.typicode.com/todos/1").then(函数(结果){控制台日志(结果);//代码取决于结果}).catch(函数(){//发生错误});

.as-console-wrapper { max-height: 100% !important;顶部:0;}

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

有关承诺的更多信息:HTML5 摇滚 - JavaScript 承诺.>

旁注:jQuery 的延迟对象

延迟对象 是 jQuery 对承诺的自定义实现(在 Promise API 标准化之前).它们的行为几乎与 Promise 类似,但公开的 API 略有不同.

jQuery 的每个 Ajax 方法都已经返回一个延迟对象";(实际上是一个延迟对象的承诺)你可以从你的函数中返回:

function ajax() {返回 $.ajax(...);}ajax().done(function(result) {//代码取决于结果}).失败(函数(){//发生错误});

旁注:承诺陷阱

请记住,promise 和延迟对象只是未来值的容器,它们不是值本身.例如,假设您有以下内容:

function checkPassword() {返回 $.ajax({网址:'/密码',数据: {用户名:$('#username').val(),密码:$('#password').val()},类型:'POST',数据类型:'json'});}如果(检查密码()){//告诉用户他们已登录}

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

但修复很简单:

checkPassword().done(function(r) {如果 (r) {//告诉用户他们已登录} 别的 {//告诉用户他们的密码是错误的}}).失败(功能(x){//告诉用户发生了不好的事情});


不推荐:同步Ajax";电话

正如我所提到的,一些(!)异步操作具有同步对应物.我不提倡使用它们,但为了完整起见,以下是执行同步调用的方式:

没有 jQuery

如果您直接使用 XMLHttpRequest 对象,请传递 false 作为 .open 的第三个参数.

jQuery

如果你使用jQuery,你可以设置asyncfalse 的选项.请注意,此选项自 jQuery 1.8 起不推荐使用.然后,您仍然可以使用 success 回调或访问 jqXHR 对象:

function foo() {var jqXHR = $.ajax({//...异步:假});返回 jqXHR.responseText;}

如果你使用任何其他的jQuery Ajax方法,比如$.get$.getJSON等,你必须把它改成$.ajax(因为您只能将配置参数传递给 $.ajax).

注意!不可能进行同步 JSONP要求.JSONP 本质上始终是异步的(甚至不考虑此选项的另一个原因).

I have a function foo which makes an asynchronous request. How can I return the response/result from foo?

I am trying to return the value from the callback, as well as assigning the result to a local variable inside the function and returning that one, but none of those ways actually return the response (they all return undefined or whatever the initial value of the variable result is).

Example of an asynchronous function that accepts a callback (using jQuery's ajax function)

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

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

Example using the then block of a promise:

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 asynchronous behaviour with different examples, 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.

The problem

The A in Ajax stands for asynchronous. That means sending the request (or rather receiving the response) is taken out of the normal execution flow. In your example, $.ajax returns immediately and the next statement, return result;, is executed before the function you passed as success callback was even called.

Here is an analogy which hopefully makes the difference between synchronous and asynchronous flow clearer:

Synchronous

Imagine you make a phone call to a friend and ask him to look something up for you. Although it might take a while, you wait on the phone and stare into space, until your friend gives you the answer that you needed.

The same is happening when you make a function call containing "normal" code:

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

var item = findItem();

// Do something with item
doSomethingElse();

Even though findItem might take a long time to execute, any code coming after var item = findItem(); has to wait until the function returns the result.

Asynchronous

You call your friend again for the same reason. But this time you tell him that you are in a hurry and he should call you back on your mobile phone. You hang up, leave the house, and do whatever you planned to do. Once your friend calls you back, you are dealing with the information he gave to you.

That's exactly what's happening when you do an Ajax request.

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

Instead of waiting for the response, the execution continues immediately and the statement after the Ajax call is executed. To get the response eventually, you provide a function to be called once the response was received, a callback (notice something? call back ?). Any statement coming after that call is executed before the callback is called.


Solution(s)

Embrace the asynchronous nature of JavaScript! While certain asynchronous operations provide synchronous counterparts (so does "Ajax"), it's generally discouraged to use them, especially in a browser context.

Why is it bad do you ask?

JavaScript runs in the UI thread of the browser and any long-running process will lock the UI, making it unresponsive. Additionally, there is an upper limit on the execution time for JavaScript and the browser will ask the user whether to continue the execution or not.

All of this results in a really bad user experience. The user won't be able to tell whether everything is working fine or not. Furthermore, the effect will be worse for users with a slow connection.

In the following we will look at three different solutions that are all building on top of each other:

  • Promises with async/await (ES2017+, available in older browsers if you use a transpiler or regenerator)
  • Callbacks (popular in node)
  • Promises with then() (ES2015+, available in older browsers if you use one of the many promise libraries)

All three are available in current browsers, and node 7+.


ES2017+: Promises with async/await

The ECMAScript version released in 2017 introduced syntax-level support for asynchronous functions. With the help of async and await, you can write asynchronous in a "synchronous style". The code is still asynchronous, but it's easier to read/understand.

async/await builds on top of promises: an async function always returns a promise. await "unwraps" a promise and either result in the value the promise was resolved with or throws an error if the promise was rejected.

Important: You can only use await inside an async function. Right now, top-level await isn't yet supported, so you might have to make an async IIFE (Immediately Invoked Function Expression) to start an async context.

You can read more about async and await on MDN.

Here is an example that elaborates the delay function findItem() above:

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

Current browser and node versions support async/await. You can also support older environments by transforming your code to ES5 with the help of regenerator (or tools that use regenerator, such as Babel).


Let functions accept callbacks

A callback is when function 1 is passed to function 2. Function 2 can call function 1 whenever it is ready. In the context of an asynchronous process, the callback will be called whenever the asynchronous process is done. Usually, the result is passed to the callback.

In the example of the question, you can make foo accept a callback and use it as success callback. So this

var result = foo();
// Code that depends on 'result'

becomes

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

Here we defined the function "inline" but you can pass any function reference:

function myCallback(result) {
    // Code that depends on 'result'
}

foo(myCallback);

foo itself is defined as follows:

function foo(callback) {
    $.ajax({
        // ...
        success: callback
    });
}

callback will refer to the function we pass to foo when we call it and we pass it on to success. I.e. once the Ajax request is successful, $.ajax will call callback and pass the response to the callback (which can be referred to with result, since this is how we defined the callback).

You can also process the response before passing it to the callback:

function foo(callback) {
    $.ajax({
        // ...
        success: function(response) {
            // For example, filter the response
            callback(filtered_response);
        }
    });
}

It's easier to write code using callbacks than it may seem. After all, JavaScript in the browser is heavily event-driven (DOM events). Receiving the Ajax response is nothing else but an event. Difficulties could arise when you have to work with third-party code, but most problems can be solved by just thinking through the application flow.


ES2015+: Promises with then()

The Promise API is a new feature of ECMAScript 6 (ES2015), but it has good browser support already. There are also many libraries which implement the standard Promises API and provide additional methods to ease the use and composition of asynchronous functions (e.g., bluebird).

Promises are containers for future values. When the promise receives the value (it is resolved) or when it is canceled (rejected), it notifies all of its "listeners" who want to access this value.

The advantage over plain callbacks is that they allow you to decouple your code and they are easier to compose.

Here is an example of using 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);
  });
}

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

Applied to our Ajax call we could use promises like this:

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

Describing all the advantages that promise offer is beyond the scope of this answer, but if you write new code, you should seriously consider them. They provide a great abstraction and separation of your code.

More information about promises: HTML5 rocks - JavaScript Promises.

Side note: jQuery's deferred objects

Deferred objects are jQuery's custom implementation of promises (before the Promise API was standardized). They behave almost like promises but expose a slightly different API.

Every Ajax method of jQuery already returns a "deferred object" (actually a promise of a deferred object) which you can just return from your function:

function ajax() {
    return $.ajax(...);
}

ajax().done(function(result) {
    // Code depending on result
}).fail(function() {
    // An error occurred
});

Side note: Promise gotchas

Keep in mind that promises and deferred objects are just containers for a future value, they are not the value itself. For example, suppose you had the following:

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
}

This code misunderstands the above asynchronous issues. Specifically, $.ajax() doesn't freeze the code while it checks the '/password' page on your server - it sends a request to the server and while it waits, it immediately returns a jQuery Ajax Deferred object, not the response from the server. That means the if statement is going to always get this Deferred object, treat it as true, and proceed as though the user is logged in. Not good.

But the fix is easy:

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


Not recommended: Synchronous "Ajax" calls

As I mentioned, some(!) asynchronous operations have synchronous counterparts. I don't advocate their use, but for completeness' sake, here is how you would perform a synchronous call:

Without jQuery

If you directly use a XMLHttpRequest object, pass false as third argument to .open.

jQuery

If you use jQuery, you can set the async option to false. Note that this option is deprecated since jQuery 1.8. You can then either still use a success callback or access the responseText property of the jqXHR object:

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

If you use any other jQuery Ajax method, such as $.get, $.getJSON, etc., you have to change it to $.ajax (since you can only pass configuration parameters to $.ajax).

Heads up! It is not possible to make a synchronous JSONP request. JSONP by its very nature is always asynchronous (one more reason to not even consider this option).

这篇关于如何从异步调用返回响应的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆