第一次接触Promise时,我盯着那个.then()链看了足足半小时——这玩意儿怎么就能把回调地狱给"拍平"了呢?后来才明白,Promise本质上就是个状态机,它的秘密全藏在三个状态里:pending、fulfilled和rejected。想象你点外卖的场景:下单后是"等待中"(pending),商家接单变成"已完成"(fulfilled),要是商家退单就是"已拒绝"(rejected)。这个状态变化有两条铁律:
来看个典型的状态流转案例:
javascript复制const orderFood = new Promise((resolve, reject) => {
// 模拟商家处理时间
setTimeout(() => Math.random() > 0.5 ? resolve('红烧牛肉面') : reject('卖完了'), 1000)
})
orderFood
.then(food => console.log(`吃到${food}`))
.catch(reason => console.log(`失败原因:${reason}`))
这里有个实战中容易踩的坑:未处理的rejection。如果忘记写catch,控制台会报UnhandledPromiseRejectionWarning。我建议用这两种方式防御:
javascript复制// 方式1:全局捕获
window.addEventListener('unhandledrejection', e => {
e.preventDefault()
console.error('抓到漏网之鱼:', e.reason)
})
// 方式2:链式兜底
promise.catch(() => {}) // 空catch吃掉错误
.then()的链式调用是Promise最迷人的特性,但也是新手最容易翻车的地方。有次我调试这样的代码:
javascript复制fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/posts?uid=${user.id}`))
.then(posts => console.log(posts)) // 这里拿到的是Response对象!
发现问题了吗?第二个then里返回的是新的Promise,需要在下一个then里继续处理。正确的写法应该是:
javascript复制fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/posts?uid=${user.id}`))
.then(res => res.json()) // 需要再次解析
.then(posts => console.log(posts))
链式调用的核心规则:
javascript复制Promise.resolve('foo')
.then() // 这里跳过
.then(console.log) // 依然输出'foo'
javascript复制Promise.reject('err')
.then(() => console.log('不会执行'))
.then(() => console.log('也不会执行'))
.catch(console.error) // 在这里捕获
实战建议:永远返回。在每个then里都return,能避免大半的链式调用问题:
javascript复制getUser()
.then(user => {
console.log(user)
return user // 显式返回
})
.then(user => getOrders(user.id))
除了常见的.then,Promise还藏着几个利器。有次我需要处理第三方SDK返回的可能是Promise也可能是普通值的情况,这时Promise.resolve()就派上用场了:
javascript复制function safeAsync(data) {
return Promise.resolve(data).then(processData) // 无论data是否是Promise都安全
}
更实用的Promise.all在处理并行请求时堪称神器。去年优化项目时,我发现这样能提升30%的加载速度:
javascript复制async function loadDashboard() {
// 并行请求互不阻塞
const [user, orders, messages] = await Promise.all([
fetch('/user'),
fetch('/orders'),
fetch('/messages')
])
// ...处理数据
}
但要注意两个坑:
对于第一个问题,可以用Promise.allSettled:
javascript复制const results = await Promise.allSettled([
fetch('/api1'),
fetch('/api2')
])
const successfulData = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value)
真实项目中,最痛的不是写Promise,而是控制并发。有次我写的爬虫因为同时发起上千请求直接把服务器搞崩了,这才明白并发控制的重要性。后来我用这个简单实现解决了问题:
javascript复制async function parallelWithLimit(tasks, limit) {
const executing = new Set()
const results = []
for (const task of tasks) {
const p = Promise.resolve().then(task)
results.push(p)
executing.add(p)
p.finally(() => executing.delete(p))
if (executing.size >= limit) {
await Promise.race(executing)
}
}
return Promise.all(results)
}
另一个性能优化点是取消机制。原生的Promise没法取消,但我们可以用AbortController实现:
javascript复制const controller = new AbortController()
fetch('/api', {
signal: controller.signal
}).catch(e => {
if (e.name === 'AbortError') {
console.log('请求被取消')
}
})
// 需要取消时
controller.abort()
对于复杂场景,我推荐使用这些优化策略:
javascript复制const fetchCache = new Map()
async function cachedFetch(url) {
if (fetchCache.has(url)) {
return fetchCache.get(url)
}
const promise = fetch(url).then(res => res.json())
fetchCache.set(url, promise)
return promise
}
Promise的错误处理就像玩扫雷,稍有不慎就会炸得面目全非。我总结出这几个最佳实践:
黄金法则:永远用catch处理rejection,哪怕只是日志记录
javascript复制dangerousOperation()
.then(handleSuccess)
.catch(err => {
console.error(err)
return fallbackValue // 提供降级值
})
错误边界:在不同层级处理不同类型的错误
javascript复制class ApiClient {
async getData() {
try {
const raw = await fetch('/api')
return this.parseData(raw)
} catch (err) {
if (err instanceof NetworkError) {
throw new RetryableError(err)
}
throw err
}
}
}
终极防御:给Promise加上超时控制
javascript复制function withTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('超时')), timeout)
)
])
}
在Node.js环境还要注意这个特殊处理:
javascript复制process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的rejection:', reason)
// 最好记录到监控系统
})
真正理解Promise最好的方式就是自己实现一个简化版。下面这个实现涵盖了核心功能:
javascript复制class MyPromise {
constructor(executor) {
this.state = 'pending'
this.value = undefined
this.callbacks = []
const resolve = value => {
if (this.state !== 'pending') return
// 处理thenable对象
if (value && typeof value.then === 'function') {
return value.then(resolve, reject)
}
this.state = 'fulfilled'
this.value = value
this.callbacks.forEach(cb => this._handle(cb))
}
const reject = reason => {
if (this.state !== 'pending') return
this.state = 'rejected'
this.value = reason
this.callbacks.forEach(cb => this._handle(cb))
}
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this._handle({
onFulfilled: typeof onFulfilled === 'function' ? onFulfilled : null,
onRejected: typeof onRejected === 'function' ? onRejected : null,
resolve,
reject
})
})
}
_handle(callback) {
if (this.state === 'pending') {
this.callbacks.push(callback)
return
}
const cb = this.state === 'fulfilled'
? callback.onFulfilled
: callback.onRejected
if (!cb) {
const action = this.state === 'fulfilled'
? callback.resolve
: callback.reject
action(this.value)
return
}
try {
const result = cb(this.value)
callback.resolve(result)
} catch (err) {
callback.reject(err)
}
}
static resolve(value) {
return new MyPromise(resolve => resolve(value))
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason))
}
}
这个实现揭示了几个关键点:
虽然async/await让异步代码看起来像同步,但底层还是Promise。有个常见的误解:
javascript复制async function getData() {
const res = await fetch('/api') // 这里会暂停吗?
console.log('获取到数据')
return res.json()
}
实际上,await只是语法糖,函数执行遇到await时:
更惊艳的是两者的组合用法。比如实现自动重试:
javascript复制async function retry(fn, retries = 3, delay = 1000) {
try {
return await fn()
} catch (err) {
if (retries <= 0) throw err
await new Promise(res => setTimeout(res, delay))
return retry(fn, retries - 1, delay * 2) // 指数退避
}
}
或者制作请求队列:
javascript复制class RequestQueue {
constructor(concurrency = 1) {
this.queue = []
this.workers = Array(concurrency).fill(Promise.resolve())
}
add(request) {
return new Promise((resolve, reject) => {
this.queue.push({ request, resolve, reject })
this._work()
})
}
async _work() {
const worker = this.workers.find(w => w.isIdle)
if (!worker || !this.queue.length) return
worker.isIdle = false
const { request, resolve, reject } = this.queue.shift()
try {
resolve(await request())
} catch (err) {
reject(err)
} finally {
worker.isIdle = true
this._work()
}
}
}
在实际项目中,Promise的应用远不止简单的异步处理。比如在Vue组件中,我常用这种模式处理异步数据:
javascript复制export default {
data: () => ({
user: null,
loading: false,
error: null
}),
methods: {
async fetchUser() {
this.loading = true
try {
this.user = await getUser()
} catch (err) {
this.error = err
// 上报错误到监控系统
trackError(err)
} finally {
this.loading = false
}
}
}
}
对于表单提交这类场景,还需要考虑竞态条件:
javascript复制let submitLock = null
async function handleSubmit() {
if (submitLock) return
submitLock = new Promise(async (resolve) => {
try {
await submitForm()
showSuccess()
} catch (err) {
showError(err)
} finally {
submitLock = null
resolve()
}
})
return submitLock
}
在React中,结合useEffect清理异步操作:
javascript复制function UserProfile({ id }) {
const [user, setUser] = useState(null)
useEffect(() => {
let isMounted = true
const abortController = new AbortController()
fetchUser(id, { signal: abortController.signal })
.then(data => isMounted && setUser(data))
.catch(err => isMounted && console.error(err))
return () => {
isMounted = false
abortController.abort()
}
}, [id])
return <div>{user?.name}</div>
}
这些实战经验让我明白,真正用好Promise需要理解其本质,同时结合具体框架特性。每次遇到异步问题,先问自己:
想清楚这些问题,代码的健壮性就能提升一个档次。