Skip to content

Promise 与异步编程 ​

回调地狱 ​

在 Promise 出现之前,异步操作依赖回调函数,多层嵌套导致代码难以维护:

js
getData("/api/user", function (user) {
  getData("/api/orders/" + user.id, function (orders) {
    getData("/api/details/" + orders[0].id, function (details) {
      console.log(details)
    })
  })
})
getData("/api/user", function (user) {
  getData("/api/orders/" + user.id, function (orders) {
    getData("/api/details/" + orders[0].id, function (details) {
      console.log(details)
    })
  })
})

Promise ​

Promise 是 ES6 引入的异步编程解决方案,用于解决回调地狱,本质上是一个状态机。

三种状态 ​

  • pending(进行中)— 初始状态
  • fulfilled(已成功)— 调用 resolve()
  • rejected(已失败)— 调用 reject()

状态一旦改变就不可逆:pending → fulfilled 或 pending → rejected

基本用法 ​

js
const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = true
    if (success) {
      resolve("操作成功")
    } else {
      reject("操作失败")
    }
  }, 1000)
})

promise
  .then((value) => console.log(value))
  .catch((error) => console.error(error))
  .finally(() => console.log("无论成功失败都会执行"))
const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = true
    if (success) {
      resolve("操作成功")
    } else {
      reject("操作失败")
    }
  }, 1000)
})

promise
  .then((value) => console.log(value))
  .catch((error) => console.error(error))
  .finally(() => console.log("无论成功失败都会执行"))

静态方法 ​

方法说明
Promise.resolve(value)返回一个 resolved 状态的 Promise
Promise.reject(reason)返回一个 rejected 状态的 Promise
Promise.all(iterable)所有成功才成功,一个失败则失败(快速失败)
Promise.allSettled(iterable)等所有 Promise 完成,无论成功失败
Promise.race(iterable)返回最先完成的 Promise(无论成功或失败)
Promise.any(iterable)返回第一个成功的 Promise,全部失败才 reject

Promise.all ​

js
const p1 = fetch("/api/user")
const p2 = fetch("/api/orders")
const p3 = fetch("/api/products")

Promise.all([p1, p2, p3])
  .then(([user, orders, products]) => {
    // 三个请求都成功
  })
  .catch((error) => {
    // 任何一个失败
  })
const p1 = fetch("/api/user")
const p2 = fetch("/api/orders")
const p3 = fetch("/api/products")

Promise.all([p1, p2, p3])
  .then(([user, orders, products]) => {
    // 三个请求都成功
  })
  .catch((error) => {
    // 任何一个失败
  })

Promise.allSettled ​

js
Promise.allSettled([p1, p2, p3]).then((results) => {
  results.forEach((result) => {
    if (result.status === "fulfilled") {
      console.log(result.value)
    } else {
      console.error(result.reason)
    }
  })
})
Promise.allSettled([p1, p2, p3]).then((results) => {
  results.forEach((result) => {
    if (result.status === "fulfilled") {
      console.log(result.value)
    } else {
      console.error(result.reason)
    }
  })
})

Promise.race ​

常用于请求超时控制:

js
function fetchWithTimeout(url, timeout = 5000) {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("请求超时")), timeout)
    ),
  ])
}
function fetchWithTimeout(url, timeout = 5000) {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("请求超时")), timeout)
    ),
  ])
}

Promise 链式调用 ​

then 方法返回一个新的 Promise,因此可以链式调用:

js
fetch("/api/user")
  .then((res) => res.json())
  .then((user) => fetch("/api/orders/" + user.id))
  .then((res) => res.json())
  .then((orders) => console.log(orders))
  .catch((err) => console.error(err))
fetch("/api/user")
  .then((res) => res.json())
  .then((user) => fetch("/api/orders/" + user.id))
  .then((res) => res.json())
  .then((orders) => console.log(orders))
  .catch((err) => console.error(err))

错误处理 ​

  • catch 可以捕获链中任意一个 Promise 的 reject
  • 在 then 中抛出的异常也会被后续 catch 捕获
  • 推荐在所有 Promise 链末尾加上 catch
js
Promise.resolve()
  .then(() => {
    throw new Error("出错了")
  })
  .catch((err) => console.log(err.message)) // "出错了"
Promise.resolve()
  .then(() => {
    throw new Error("出错了")
  })
  .catch((err) => console.log(err.message)) // "出错了"

async/await ​

async/await 是 ES2017 引入的语法糖,本质上是 Promise + Generator,让异步代码看起来像同步代码。

async 函数 ​

  • async 函数总是返回一个 Promise
  • 如果返回值不是 Promise,会被 Promise.resolve() 包裹
js
async function foo() {
  return "hello"
}
foo().then(console.log) // "hello"
async function foo() {
  return "hello"
}
foo().then(console.log) // "hello"

await 表达式 ​

  • await 会暂停当前 async 函数的执行,等待 Promise 完成
  • await 只能在 async 函数内部使用(模块顶层也可以)
  • await 后面通常跟一个 Promise,如果不是 Promise 会转为 Promise
js
async function getData() {
  try {
    const user = await fetch("/api/user").then((r) => r.json())
    const orders = await fetch("/api/orders/" + user.id).then((r) => r.json())
    console.log(orders)
  } catch (error) {
    console.error("请求失败", error)
  }
}
async function getData() {
  try {
    const user = await fetch("/api/user").then((r) => r.json())
    const orders = await fetch("/api/orders/" + user.id).then((r) => r.json())
    console.log(orders)
  } catch (error) {
    console.error("请求失败", error)
  }
}

并发请求 ​

多个无依赖的异步操作应并发执行:

js
// 错误的串行方式
const user = await fetchUser()
const orders = await fetchOrders()
const products = await fetchProducts()

// 正确的并发方式
const [user, orders, products] = await Promise.all([
  fetchUser(),
  fetchOrders(),
  fetchProducts(),
])
// 错误的串行方式
const user = await fetchUser()
const orders = await fetchOrders()
const products = await fetchProducts()

// 正确的并发方式
const [user, orders, products] = await Promise.all([
  fetchUser(),
  fetchOrders(),
  fetchProducts(),
])

for...of 与 await ​

按顺序处理数组中的异步操作:

js
async function processUrls(urls) {
  for (const url of urls) {
    const data = await fetch(url)
    console.log(data)
  }
}
async function processUrls(urls) {
  for (const url of urls) {
    const data = await fetch(url)
    console.log(data)
  }
}

注意

不要在循环中使用 forEach 配合 await,forEach 不会等待 Promise 完成

js
// 错误示例 - forEach 不等待
urls.forEach(async (url) => {
  const data = await fetch(url) // 不会等待
})

// 正确做法
for (const url of urls) {
  const data = await fetch(url)
}
// 错误示例 - forEach 不等待
urls.forEach(async (url) => {
  const data = await fetch(url) // 不会等待
})

// 正确做法
for (const url of urls) {
  const data = await fetch(url)
}

顶层 await ​

ES2022 允许在模块顶层直接使用 await:

js
// 仅在 type="module" 的脚本中可用
const response = await fetch("/api/config")
const config = await response.json()
export default config
// 仅在 type="module" 的脚本中可用
const response = await fetch("/api/config")
const config = await response.json()
export default config

微任务与宏任务 ​

详见 [事件循环(Event Loop)](事件循环(Event Loop).md)

常见面试题 ​

手写 Promise ​

js
class MyPromise {
  constructor(executor) {
    this.status = "pending"
    this.value = undefined
    this.reason = undefined
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []

    const resolve = (value) => {
      if (this.status === "pending") {
        this.status = "fulfilled"
        this.value = value
        this.onFulfilledCallbacks.forEach((fn) => fn())
      }
    }

    const reject = (reason) => {
      if (this.status === "pending") {
        this.status = "rejected"
        this.reason = reason
        this.onRejectedCallbacks.forEach((fn) => fn())
      }
    }

    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }

  then(onFulfilled, onRejected) {
    // 略:完整实现参考 Promise/A+ 规范
  }
}
class MyPromise {
  constructor(executor) {
    this.status = "pending"
    this.value = undefined
    this.reason = undefined
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []

    const resolve = (value) => {
      if (this.status === "pending") {
        this.status = "fulfilled"
        this.value = value
        this.onFulfilledCallbacks.forEach((fn) => fn())
      }
    }

    const reject = (reason) => {
      if (this.status === "pending") {
        this.status = "rejected"
        this.reason = reason
        this.onRejectedCallbacks.forEach((fn) => fn())
      }
    }

    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }

  then(onFulfilled, onRejected) {
    // 略:完整实现参考 Promise/A+ 规范
  }
}

输出顺序题 ​

js
console.log(1)
setTimeout(() => console.log(2), 0)
Promise.resolve().then(() => console.log(3))
console.log(4)
// 输出: 1, 4, 3, 2
// 原因: 同步 > 微任务(Promise.then) > 宏任务(setTimeout)
console.log(1)
setTimeout(() => console.log(2), 0)
Promise.resolve().then(() => console.log(3))
console.log(4)
// 输出: 1, 4, 3, 2
// 原因: 同步 > 微任务(Promise.then) > 宏任务(setTimeout)