- Published on
返回异步调用中的响应值的方法
- Authors
- Name
→ 如需获取异步行为和不同示例的更加一般的解释,请参见 为什么我在函数内部修改变量后它没有改变?- 异步代码参考
→ 如果您已经了解了问题所在,请直接跳转到下面的解决方案。
问题
这意味着发送请求(或接收响应)被取出了正常的执行流。在您的示例中,$.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 the item
})
doSomethingElse()
执行在响应到达之前立即继续,Ajax 调用后的语句被执行。为了最终得到响应,你提供了一个函数,一旦响应收到,它就会被调用,这个函数叫做 回调。在调用之后的任何语句在回调之前被执行。
解决方案
接受 JavaScript 的异步特性! 虽然某些异步操作提供同步的对应操作(例如 "Ajax"),但在浏览器上下文中使用它们通常是不推荐的。
为什么这会不好呢?
JavaScript 运行在浏览器的 UI 线程中,任何长时间运行的进程都会锁定 UI,导致其无响应。此外,JavaScript 的执行时间有上限,浏览器会询问用户是否继续执行。
所有这些都带来了非常糟糕的用户体验。用户无法判断一切是否正常运行。对于网速较慢的用户来说,效果会更糟。
下面我们将看到三种不同的解决方案,这三种方案都是建立在彼此之上的:
- 使用
async/await
的 Promises (ES2017+,如果使用转译器或再生器,也可以在旧版浏览器中使用) - 回调函数 (在 Node.js 中很流行)
- 使用
then()
的 Promises (ES2015+,如果使用其中一个 Promise 库,也可以在旧版浏览器中使用)
这三种方法在当前的浏览器和 node 7+ 中都可用。
async/await
的 Promises
ES2017+: 使用 2017 年发布的 ECMAScript 版本引入了对异步函数的语法级支持。借助 async
和 await
,你可以以“同步风格”编写异步代码。代码仍然是异步的,但更易读和理解。
async/await
构建在 Promises 之上:一个 async
函数总是返回一个 Promise。await
解包 Promise,并返回 Promise 解析的值或抛出一个拒绝的错误。
重要: 你只能在 async
函数内部或在 JavaScript 模块 中使用 await
。在模块外部不支持顶级 await
,因此如果不使用模块,你可能需要使用异步 IIFE (立即调用函数表达式) 来开始异步上下文。
你可以在 MDN 上阅读更多关于 async
和 await
的内容。
以下是一个 elaborates _delay_
函数 findItem()
的示例:
// 使用 'superagent' 将返回一个 promise
var superagent = require('superagent')
// 这不是被声明为 `async`,因为它已经返回一个 promise
function delay() {
// `delay` 返回一个 promise
return new Promise(function (resolve, reject) {
// 只有 `delay` 能够解析或拒绝 promise
setTimeout(function () {
resolve(42) // 3 秒后,用值 42 解析 promise
}, 3000)
})
}
async function getAllBooks() {
try {
// 获取当前用户的书籍 ID 列表
var bookIDs = await superagent.get('/user/books')
// 等待 3 秒(仅为了这个示例)
await delay()
// 获取每本书的信息
return superagent.get('/books/ids=' + JSON.stringify(bookIDs))
} catch (error) {
// 如果任何一个 awaited promise 被拒绝,这个 catch 块将捕获拒绝原因
return null
}
}
// 启动一个 IIFE 来在顶级使用 `await`
;(async function () {
let books = await getAllBooks()
console.log(books)
})()
当前版本的 浏览器 和 node 支持 async/await
。你也可以通过使用 regenerator(或使用 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'
})
在这里我们内联定义了该函数,但你可以传递任何函数引用:
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) {
// 例如,过滤响应
callback(filtered_response)
},
})
}
使用回调编写代码比看起来容易。毕竟,浏览器中的 JavaScript 是高度事件驱动的(DOM 事件)。接收 Ajax 响应无非是一个事件。问题可能在于与第三方代码一起工作,但大多数问题可以通过深入思考应用程序流程来解决。
then() 的 Promises
ES2015+: 使用Promise API 是 ECMAScript 6 (ES2015) 的新功能,但它已经有很好的 浏览器支持。还有很多实现标准 Promises API 的库,并提供额外的方法以简化异步函数的使用和组合(例如 bluebird)。
Promises 是未来值的容器。当 Promise 接收到值(被解析)或取消(被拒绝)时,它会通知所有想要访问此值的“监听者”。
它们相对于纯回调函数的优势在于可以解耦代码,并且更易于组合。
以下是使用 Promise 的示例:
function delay() {
// `delay` 返回一个 promise
return new Promise(function (resolve, reject) {
// 只有 `delay` 能够解析或拒绝 promise
setTimeout(function () {
resolve(42) // 3 秒后,用值 42 解析 promise
}, 3000)
})
}
delay()
.then(function (v) {
// `delay` 返回一个 promise
console.log(v) // 一旦解析,就记录值
})
.catch(function (v) {
// 或者,如果被拒绝,做其他事情
// (在此示例中不会发生,因为 `reject` 未被调用)
})
应用到我们的 Ajax 调用中,可以这样使用 Promises:
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) // 依赖于结果的代码
})
.catch(function () {
// 发生错误
})
详细描述 Promise 提供的所有优点超出了本文的范围,但如果你编写新的代码,你应该认真考虑它们。它们提供了很大的代码抽象和分离。
更多关于 Promise 的信息:HTML5 rocks - JavaScript Promises。
旁注:jQuery 的 deferred 对象
Deferred 对象 是 jQuery 的 Promise 自定义实现(在 Promise API 标准化之前)。它们的行为几乎与 Promises 相同,但暴露了略有不同的 API。
jQuery 的每个 Ajax 方法已经返回一个 “deferred 对象”(实际上是 deferred
对象的 promise
),你可以从函数中返回它:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// 依赖于结果的代码
}).fail(function() {
// 发生错误
});
旁注:Promise 的问题
请记住,Promises 和 deferred 对象只是未来值的容器,它们不是值本身。例如,假设你有如下代码:
function checkPassword() {
return $.ajax({
url: '/password',
data: {
username: $('#username').val(),
password: $('#password').val(),
},
type: 'POST',
dataType: 'json',
})
}
if (checkPassword()) {
// 告诉用户他们已登录
}
这段代码错误地处理了上述异步问题。具体来说,$.ajax()
在检查你服务器上的 '/password' 页面时不会冻结代码执行——它发送一个请求到服务器,而在等待时立即返回一个 jQuery Ajax Deferred 对象,而不是服务器的响应。这意味着 if
语句总是会得到这个 Deferred 对象,将其视为 true
,并认为用户已经登录。这不好。
但修复非常简单:
checkPassword()
.done(function (r) {
if (r) {
// 告诉用户他们已登录
} else {
// 告诉用户他们的密码不正确
}
})
.fail(function (x) {
// 告诉用户发生了错误
})
不推荐:同步的 “Ajax” 调用
正如我所提到的,有些(!)异步操作有同步的对应操作。我不推荐使用,但为了完整性,这里展示如何执行同步调用:
不使用 jQuery
如果你直接使用 XMLHttpRequest
对象,传递 false
作为 .open
的第三个参数。
使用 jQuery
如果你使用 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
)。
注意! 不可能进行同步的 JSONP 请求。JSONP 的本质就是异步的(这是不推荐使用这个方法的又一原因)。