logo
Published on

返回异步调用中的响应值的方法

Authors
  • Name
    Twitter

→ 如需获取异步行为和不同示例的更加一般的解释,请参见 为什么我在函数内部修改变量后它没有改变?- 异步代码参考

→ 如果您已经了解了问题所在,请直接跳转到下面的解决方案。

问题

Ajax中的 A 代表 异步

这意味着发送请求(或接收响应)被取出了正常的执行流。在您的示例中,$.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+ 中都可用。


ES2017+: 使用 async/await 的 Promises

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

async/await 构建在 Promises 之上:一个 async 函数总是返回一个 Promise。await 解包 Promise,并返回 Promise 解析的值或抛出一个拒绝的错误。

重要: 你只能在 async 函数内部或在 JavaScript 模块 中使用 await。在模块外部不支持顶级 await,因此如果不使用模块,你可能需要使用异步 IIFE (立即调用函数表达式) 来开始异步上下文。

你可以在 MDN 上阅读更多关于 asyncawait 的内容。

以下是一个 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 响应无非是一个事件。问题可能在于与第三方代码一起工作,但大多数问题可以通过深入思考应用程序流程来解决。


ES2015+: 使用 then() 的 Promises

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 的本质就是异步的(这是不推荐使用这个方法的又一原因)。