我有一个foo发出Ajax请求的函数。我如何从中返回响应foo?
foo
我尝试从success回调中返回值,以及将响应分配给函数内部的局部变量并返回该局部变量,但这些方法均未真正返回响应。
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`.
问题 在一中的Ajax代表异步。这意味着发送请求(或更确切地说接收响应)已从正常执行流程中删除。在你的示例中,$.ajax立即返回并且在调用return result;作为success回调传递的函数之前执行下一条语句。
$.ajax
return result
这是一个类比,希望可以使同步流和异步流之间的区别更加清晰:
假设你打给朋友一个电话,并请他为你找东西。尽管可能要花一些时间,但你还是要等电话并凝视太空,直到你的朋友给你所需的答案。
当你进行包含“正常”代码的函数调用时,也会发生相同的情况:
function findItem() { var item; while(item_not_found) { // search } return item; } var item = findItem(); // Do something with item doSomethingElse();
即使findItem执行可能需要很长时间,但之后的任何代码var item = findItem();也必须等到函数返回结果后才能执行。
findItem
var item = findItem()
你出于相同的原因再次致电给你的朋友。但是这次你告诉他你很着急,他应该用你的手机给你回电。你挂断电话,离开房子,然后按计划做。一旦你的朋友给你回电,你就可以处理他提供给你的信息。
这正是你执行Ajax请求时发生的事情。
findItem(function(item) { // Do something with item }); doSomethingElse();
无需等待响应,而是立即继续执行,并执行Ajax调用后的语句。为了最终获得响应,你提供了一个在收到响应后立即调用的函数,即回调函数(注意什么?回叫?)。在调用之后执行的所有语句都将在调用回调之前执行。
拥抱JavaScript的异步特性!尽管某些异步操作提供了同步对应项(“ Ajax”也是如此),但通常不鼓励使用它们,尤其是在浏览器上下文中。
你问为什么不好?
JavaScript在浏览器的UI线程中运行,任何长时间运行的进程都将锁定UI,从而使其无响应。此外,JavaScript的执行时间有上限,浏览器会询问用户是否继续执行。
所有这些确实是糟糕的用户体验。用户将无法判断一切是否正常。此外,对于连接速度较慢的用户,效果会更糟。
在下面的内容中,我们将研究三个互为基础的不同解决方案:
async/await
then()
在当前浏览器和节点7+中,所有这三个功能均可用。
2017年发布的ECMAScript版本引入了对异步功能的语法级支持。借助async和await,你可以以“同步样式”编写异步。该代码仍然是异步的,但更易于阅读/理解。
async
await
async/await建立在promise之上:async函数总是返回promise。await“取消包装”承诺,并导致承诺被解决的价值,或者如果承诺被拒绝,则抛出错误。
promise
重要提示:你只能await在async函数内部使用。目前,await尚不支持顶层,因此你可能必须进行异步IIFE(立即调用函数表达式)才能启动async上下文。
你可以阅读更多关于async和await的MDN。
这是一个基于以上延迟的示例:
// 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); })();
当前的浏览器和节点版本支持async/await。你还可以通过使用再生器(或使用再生器的工具,例如Babel)将代码转换为ES5来支持较旧的环境。
让函数接受回调 回调只是传递给另一个函数的一个函数。其他函数可以随时调用传递的函数。在异步过程的上下文中,只要异步过程完成,就会调用回调。通常,结果将传递给回调。
在问题的示例中,你可以foo接受回调并将其用作success回调。所以这
var result = foo(); // Code that depends on 'result'
becomes
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,因为这是我们定义回调的方式)。
callback
Ajax
你还可以在将响应传递给回调之前对其进行处理:
function foo(callback) { $.ajax({ // ... success: function(response) { // For example, filter the response callback(filtered_response); } }); }
使用回调编写代码比看起来容易。毕竟,浏览器中的JavaScript是受事件驱动的(DOM事件)。接收Ajax响应不过是一个事件。 当你必须使用第三方代码时,可能会遇到困难,但是大多数问题可以通过思考应用程序流程来解决。
该承诺API是ECMAScript的6(ES2015)的新功能,但它有很好的浏览器支持了。还有许多实现标准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摇滚-JavaScript Promises
延迟对象是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 });
Side note: Promise gotchas
请记住,承诺和递延对象只是将来价值的容器,它们不是价值本身。例如,假设你具有以下内容:
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,然后像用户已登录一样继续进行。
$.ajax()
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”调用 如前所述,某些异步操作具有同步的对应对象。我不主张使用它们,但是出于完整性考虑,这是执行同步调用的方法:
如果你直接使用XMLHTTPRequest对象,请false作为第三个参数传递给.open。
如果使用jQuery,则可以将async选项设置为false。请注意,自jQuery 1.8起不推荐使用此选项。然后,你仍然可以使用success回调或访问jqXHR对象的responseText属性:
function foo() { var jqXHR = $.ajax({ //... async: false }); return jqXHR.responseText; }
如果使用任何其他的jQuery的Ajax的方法,例如$.get,$.getJSON等等,必须将其改为$.ajax(因为你只能传递配置参数$.ajax)。
$.get
$.getJSON
当心!不可能发出同步JSONP请求。JSONP本质上始终是异步的(还有一个甚至不考虑此选项的原因)。