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)
Cao Note