通俗易懂之Promise!

398 阅读8分钟

假设打开一个页面时,需要依次完成三件事:

  1. 获取用户信息
  2. 根据用户信息查询订单
  3. 根据订单加载商品详情

如果全部依赖回调函数,代码很快就会变成这样:

getUser(user => {
  getOrders(user.id, orders => {
    getProduct(orders[0].productId, product => {
      console.log(product)
    })
  })
})

代码越写越深,错误处理也越来越麻烦。这就是常说的“回调地狱”。

Promise 的价值,就是把这种层层嵌套的异步代码,整理成更清晰的链式流程。

本文会带你掌握:

  • Promise 的三种状态
  • thencatchfinally
  • Promise 链式调用的规则
  • allallSettledraceany 的区别
  • async/await 与 Promise 的关系

一、Promise 到底是什么?

JavaScript 主线程一次只能执行一个任务,但网络请求、定时器和浏览器事件显然不能一直阻塞主线程。

这些耗时操作会交给浏览器或运行环境处理,完成后再通知 JavaScript。

Promise 本质上是一个用来描述异步操作最终结果的对象。

你可以把它理解成一张“取餐小票”:

  • 刚下单:等待中
  • 餐做好了:成功
  • 制作失败:拒绝

对应到 Promise,就是三种状态:

状态含义
pending等待中
fulfilled操作成功
rejected操作失败

二、创建一个 Promise

Promise 是构造函数,需要通过 new 创建实例:

const promise = new Promise((resolve, reject) => {
  // 执行异步操作
})

构造函数接收一个执行器函数,执行器函数又接收两个参数:

  • resolve(value):将 Promise 变为成功状态
  • reject(reason):将 Promise 变为失败状态

例如:

const promise = new Promise((resolve, reject) => {
  const requestSuccess = true

  if (requestSuccess) {
    resolve('数据请求成功')
  } else {
    reject(new Error('数据请求失败'))
  }
})

需要特别注意:Promise 执行器会立即、同步执行。

console.log('开始')

new Promise(resolve => {
  console.log('执行 Promise')
  resolve()
})

console.log('结束')

输出结果为:

开始
执行 Promise
结束

Promise 并不会自动让代码变成异步。真正异步的是网络请求、定时器等具体操作。


三、Promise 状态一旦改变,就不会再次变化

Promise 的状态只能发生下面两种变化:

pending → fulfilled
pending → rejected

状态一旦确定,就不能再次改变:

const promise = new Promise((resolve, reject) => {
  resolve('成功')
  reject(new Error('失败'))
  resolve('再次成功')
})

promise.then(value => {
  console.log(value)
})

最终只会输出:

成功

第一次调用 resolve 后,Promise 已经变为 fulfilled,后面的 rejectresolve 都不会再生效。


四、用 then 获取成功结果

then 用于注册 Promise 成功或失败后的处理函数:

promise.then(
  value => {
    console.log('成功:', value)
  },
  reason => {
    console.log('失败:', reason)
  }
)

第一个参数处理成功结果,第二个参数处理失败原因。

不过实际开发中,更推荐使用 then + catch

promise
  .then(value => {
    console.log('成功:', value)
  })
  .catch(error => {
    console.error('失败:', error)
  })

这样不仅结构更清晰,catch 还能捕获前面 then 中抛出的错误。


五、真正理解 Promise 链式调用

假设有三个请求,后一个请求依赖前一个请求的结果。

回调写法可能是这样:

getData('/api/user?id=1', user => {
  getData(`/api/orders?userId=${user.id}`, orders => {
    getData(`/api/product?id=${orders[0].productId}`, product => {
      console.log(product)
    })
  })
})

如果 getData 返回 Promise,就可以改写为:

getData('/api/user?id=1')
  .then(user => {
    return getData(`/api/orders?userId=${user.id}`)
  })
  .then(orders => {
    return getData(`/api/product?id=${orders[0].productId}`)
  })
  .then(product => {
    console.log(product)
  })
  .catch(error => {
    console.error('请求失败:', error)
  })

嵌套结构被展开成了一条清晰的执行链。

为什么可以一直调用 then?

因为每次调用 then,都会返回一个新的 Promise:

const nextPromise = promise.then(value => {
  return value
})

console.log(nextPromise instanceof Promise) // true

新 Promise 的状态,取决于回调函数的执行结果。

情况一:返回普通值

Promise.resolve(1)
  .then(value => {
    return value + 1
  })
  .then(value => {
    console.log(value) // 2
  })

回调返回普通值时,下一个 Promise 会变为成功,成功值就是这个返回值。

情况二:返回另一个 Promise

Promise.resolve()
  .then(() => {
    return new Promise(resolve => {
      setTimeout(() => resolve('请求完成'), 1000)
    })
  })
  .then(value => {
    console.log(value)
  })

链条会等待返回的 Promise 得到结果,再继续向后执行。

情况三:抛出错误

Promise.resolve()
  .then(() => {
    throw new Error('处理数据时出错')
  })
  .catch(error => {
    console.error(error.message)
  })

只要回调中抛出错误,下一个 Promise 就会变为 rejected

因此,写 Promise 链时最重要的一条规则是:

需要把结果交给下一步时,一定要 return


六、catch:统一处理链条中的错误

下面这段代码中,then 内访问了一个不存在的变量:

Promise.resolve('ok')
  .then(value => {
    console.log(value)
    console.log(notDefined)
  })
  .catch(error => {
    console.error('捕获错误:', error.message)
  })

catch 不仅能捕获原始 Promise 的失败,也能捕获前面任意一个 then 中抛出的错误。

它基本等价于:

promise.then(undefined, error => {
  console.error(error)
})

但需要注意,catch 并不是让错误消失,而是让我们有机会处理失败。

如果 catch 正常返回,后面的 Promise 链还可以继续执行:

Promise.reject(new Error('请求失败'))
  .catch(error => {
    console.error(error.message)
    return '使用默认数据'
  })
  .then(value => {
    console.log(value) // 使用默认数据
  })

如果希望错误继续向后传递,需要再次抛出:

.catch(error => {
  console.error('记录错误:', error)
  throw error
})

七、finally:无论成功失败都会执行

有些操作无论请求成功还是失败都必须执行,例如:

  • 关闭加载动画
  • 恢复按钮状态
  • 释放资源

这时可以使用 finally

showLoading()

getData('/api/user')
  .then(data => {
    console.log(data)
  })
  .catch(error => {
    console.error(error)
  })
  .finally(() => {
    hideLoading()
  })

finally 不接收成功值或失败原因,它通常只负责执行清理工作。


八、Promise.all:全部成功才算成功

当多个异步任务互不依赖,可以同时发起:

const userPromise = getData('/api/user')
const menuPromise = getData('/api/menu')
const messagePromise = getData('/api/messages')

Promise.all([userPromise, menuPromise, messagePromise])
  .then(([user, menu, messages]) => {
    console.log(user, menu, messages)
  })
  .catch(error => {
    console.error('至少有一个请求失败:', error)
  })

Promise.all 的特点:

  • 所有任务成功,整体才成功
  • 返回值顺序与传入顺序一致
  • 任意一个任务失败,整体立即失败

它适合“缺一不可”的场景,例如页面必须同时拿到用户、权限和配置数据后才能渲染。


九、Promise.allSettled:无论成功失败,都等到最后

如果我们希望知道每个任务的最终结果,可以使用 Promise.allSettled

const tasks = [
  Promise.resolve('用户信息加载成功'),
  Promise.reject(new Error('广告加载失败')),
  Promise.resolve('推荐内容加载成功')
]

Promise.allSettled(tasks).then(results => {
  console.log(results)
})

结果结构类似:

[
  {
    status: 'fulfilled',
    value: '用户信息加载成功'
  },
  {
    status: 'rejected',
    reason: Error('广告加载失败')
  },
  {
    status: 'fulfilled',
    value: '推荐内容加载成功'
  }
]

它适合“部分失败也不影响整体”的场景,例如批量上传、多个模块独立加载。


十、Promise.race:谁先结束,就采用谁的结果

race 是“赛跑”的意思。

它会采用第一个落定的 Promise,无论这个 Promise 是成功还是失败:

const request = new Promise(resolve => {
  setTimeout(() => resolve('请求成功'), 3000)
})

const timeout = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('请求超时')), 2000)
})

Promise.race([request, timeout])
  .then(value => {
    console.log(value)
  })
  .catch(error => {
    console.error(error.message) // 请求超时
  })

这里决定结果的是完成速度,与 Promise 在数组中的位置无关。

Promise.race 经常被用来实现请求超时控制。


十一、Promise.any:只要一个成功即可

Promise.any 会返回第一个成功的结果:

const serverA = Promise.reject(new Error('服务器 A 失败'))
const serverB = Promise.resolve('服务器 B 返回数据')
const serverC = Promise.resolve('服务器 C 返回数据')

Promise.any([serverA, serverB, serverC])
  .then(value => {
    console.log(value) // 服务器 B 返回数据
  })
  .catch(error => {
    console.error(error)
  })

只有全部 Promise 都失败时,Promise.any 才会失败,并抛出 AggregateError

它适合多个备用服务之间“任意一个成功即可”的场景。


十二、四种组合方法怎么选?

方法成功条件失败条件典型场景
Promise.all全部成功任意一个失败多个数据缺一不可
Promise.allSettled永远成功返回结果列表不会因单个任务失败而失败批量任务、独立模块
Promise.race最先落定的是成功最先落定的是失败超时控制、竞速
Promise.any任意一个成功全部失败多个备用服务

一句话记忆:

  • all:一个都不能少
  • allSettled:无论成败都要结果
  • race:谁最快听谁的
  • any:谁先成功用谁

十三、Promise.resolve 与 Promise.reject

Promise.resolve

创建一个成功状态的 Promise:

Promise.resolve('成功').then(value => {
  console.log(value)
})

如果传入的本身就是 Promise,Promise.resolve 会采用它的最终状态:

const original = Promise.resolve('ok')
const result = Promise.resolve(original)

console.log(result === original) // true

Promise.reject

创建一个失败状态的 Promise:

Promise.reject(new Error('操作失败'))
  .catch(error => {
    console.error(error.message)
  })

实际开发中,建议使用 Error 对象作为失败原因,这样可以保留错误信息和调用栈。


十四、async/await:Promise 的另一种写法

async/await 并没有取代 Promise,它只是让 Promise 代码看起来更像同步代码。

前面的链式调用:

getData('/api/user?id=1')
  .then(user => {
    return getData(`/api/orders?userId=${user.id}`)
  })
  .then(orders => {
    return getData(`/api/product?id=${orders[0].productId}`)
  })
  .then(product => {
    console.log(product)
  })
  .catch(error => {
    console.error(error)
  })

可以改写为:

async function loadProduct() {
  try {
    const user = await getData('/api/user?id=1')
    const orders = await getData(`/api/orders?userId=${user.id}`)
    const product = await getData(
      `/api/product?id=${orders[0].productId}`
    )

    console.log(product)
  } catch (error) {
    console.error('请求失败:', error)
  }
}

loadProduct()

需要记住三点:

  1. async 函数一定会返回 Promise
  2. await 通常写在 async 函数中
  3. await 遇到 rejected Promise 时会抛出异常,可以使用 try...catch 捕获

多个没有依赖关系的任务,不要逐个 await

// 串行执行,速度较慢
const user = await getUser()
const menu = await getMenu()

可以让它们并行执行:

const [user, menu] = await Promise.all([
  getUser(),
  getMenu()
])

十五、最后总结

学完 Promise,至少要记住下面几件事:

  1. Promise 有 pendingfulfilledrejected 三种状态
  2. 状态一旦从 pending 改变,就不能再次改变
  3. Promise 执行器会立即同步执行,Promise 本身不等于异步
  4. 每次调用 then 都会返回一个新的 Promise
  5. Promise 链中需要把结果交给下一步时,记得 return
  6. catch 可以统一处理链条中的失败和异常
  7. finally 适合执行清理操作
  8. 多个独立任务可以使用 allallSettledraceany
  9. async/await 本质上仍然建立在 Promise 之上

真正掌握 Promise,不是记住所有 API,而是理解一个核心:

每一步都返回一个结果,下一步根据这个结果继续执行。

当你理解了这条规则,Promise 链、错误传递和 async/await 就都会变得清晰。

我的个人博客:XieJinYang 的博客

如果这篇文章帮你理清了 Promise,欢迎点赞、收藏。下一次遇到异步代码时,希望你不再被层层回调绕晕。