// 这个 promise 一秒钟后会被完成
var promise = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve("hello")
}, 1000)
promise.then(function(value) {
// 一秒钟后输出 "hello"
console.log(value)
Promise 对于处理异步 API 很有用,例如 m.request。
异步 API 通常需要花费很长时间来执行,因此使用 return 同步返回函数的值会太耗费时间。它们会在后台执行,在此期间 JavaScript 可以执行其他代码。当请求完成后,会用返回的结果来调用函数。
m.request 的执行需要时间,因为它会向远程服务器发送 HTTP 请求并等待响应,由于网络延迟,可能需要花费几百毫秒。
Promise 可以链式调用。then 回调函数的返回值可以作为下一个 then 回调的参数。这样可以把代码重构成较小的函数:
function getUsers() {return m.request("/api/v1/users")}
// 避免这种用法:因为很难对功能进行测试
getUsers().then(function(users) {
var firstTen = users.slice(0, 9)
var firstTenNames = firstTen.map(function(user) {return user.firstName + " " + user.lastName})
alert(firstTenNames)
// 推荐这种用法:测试小函数会比较容易
function getFirstTen(items) {return items.slice(0, 9)}
function getUserName(user) {return user.firstName + " " + user.lastName}
function getUserNames(users) {return users.map(getUserName)}
getUsers()
.then(getFirstTen)
.then(getUserNames)
.then(alert)
在上面经过重构的代码中,getUsers() 返回一个 Promise,我们链式调用了 3 个回调函数。当 getUsers() 完成后,getFirstTen 函数会被调用,且传入用户列表作为它的第一个参数,并返回前 10 个用户的列表。这 10 个用户的列表会作为 getUserNames 的第一个参数传入,并返回用户名列表。最后,弹出提示框显示用户名列表。
在上面的原始代码中,对功能进行测试会很困难,因为你必须发送一个 HTTP 请求才能运行代码,且函数的最后调用了 alert()。
在重构的版本中,没有必要对 getUserName 中 firstName 和 lastName 之间是否添加了空格这种功能进行测试。
Promise 可以和其他 Promise 合并。这个功能使我们可以平铺嵌套的 Promise,使代码更易管理。
function searchUsers(q) {return m.request("/api/v1/users/search", {data: {q: q}})}
function getUserProjects(id) {return m.request("/api/v1/users/" + id + "/projects")}
// 避免这种用法:嵌套太深
searchUsers("John").then(function(users) {
getUserProjects(users[0].id).then(function(projects) {
var titles = projects.map(function(project) {return project.title})
alert(titles)
// 建议这种用法:扁平的代码
function getFirstId(items) {return items[0].id}
function getProjectTitles(projects) {return projects.map(getProjectTitle)}
function getProjectTitle(project) {return project.title}
searchUsers("John")
.then(getFirstId)
.then(getUserProjects)
.then(getProjectTitles)
.then(alert)
在重构过的代码中,getFirstId 返回一个 id,并把该 id 作为第一个参数传入 getUserProjects,而 getUserProjects 又返回一个完成项目列表的 Promise。这个 Promise 是被合并的,所以 getProjectTitles 的第一个参数不是 Promise,而是项目列表。getProjectTitles 返回标题列表,这个列表最终会被显示在提示框中。
Promise 可以把错误传递到适当的错误处理函数。
searchUsers("John")
.then(getFirstId)
.then(getUserProjects)
.then(getProjectTitles)
.then(alert)
.catch(function(e) {
console.log(e)
这是之前的例子,并加上了错误处理。当网络断开时,searchUsers 函数会执行失败,导致产生错误。在这种情况下,不会触发 .then 回调,但 .catch 回调会把错误输出到控制台。
如果 getUserProjects 中的请求失败,getProjectTitles 和 alert 也不会被调用,.catch 回调会记录错误。
如果 searchUsers 没有返回结果,错误处理程序也会捕获空引用异常,且 getFirstId 会尝试访问一个不存在的数组项的id 属性。
由于这些错误都是语义化的,容易保持每个函数足够小,且可测试,而不需要到处使用 try/catch。
有时,你已经有一个值,但希望把它包裹在 Promise 中。Promise.resolve 和 Promise.reject 就是为了这个目的而存在的。
// 这个列表是从 localStorage 中读取的
var users = [{id: 1, firstName: "John", lastName: "Doe"}]
// `users` 存在与否取决于 localStorage 是否存在数据
var promise = users ? Promise.resolve(users) : getUsers()
promise
.then(getFirstTen)
.then(getUserNames)
.then(alert)
某些情况下,你可能需要并行发送 HTTP 请求,并在所有请求完成后执行代码。这可以通过 Promise.all 来实现:
Promise.all([
searchUsers("John"),
searchUsers("Mary"),
.then(function(data) {
// data[0] 是名字是 John 的用户数组
// data[1] 是名字是 Mary 的用户数组
// 返回值等效于 [
// getUserNames(data[0]),
// getUserNames(data[1]),
return data.map(getUserNames)
.then(alert)
上面的例子中,同时进行了两次用户搜索。一旦这两个搜索完成,我们会从两次搜索的结果中获取所有 userName,并显示在提示框中。
这个例子同时说明了小函数的另一个好处:我们可以重用上面创建的 getUserNames 函数。
回调是处理异步计算的另一种机制。对于会执行多次的异步计算,使用回调会更合适(例如,onscroll 事件处理)。
但是,对于只会执行一次的异步计算,使用 Promise 可以更好的重构代码,减少代码的嵌套。