Promise
Promise 异步处理,提供更优雅的方式(链式调用)解决了 callback hell 的问题,并且更易于处理错误
- promise 有三种状态,状态间的转移是不可逆的,只能有
pengding to resolved或者pending to rejected两种变化pendingresolvedrejected
then函数接收两个参数,分别是成功回调和失败回调,最终会返回一个 promise,支持链式调用catch捕获错误finally不论最后是哪种状态,最终都会调用这个回调
function myPromise(url) {
return new Promise(function (resolve, reject) {
fetch(url)
.then((response) => response.json())
.then((data) => {
resolve(data);
})
.catch((err) => {
reject(err);
});
});
}
myPromise("https://api.github.com/users")
.then((data) => {
console.log(data);
})
.catch((err) => {
console.error(err);
});
手写 Promise 可以参考 https://docs.zhouweibin.top/docs/code/promise/
常用方法
注意:以下所有方法最终返回的都是一个新的 Promise 实例,这里主要对比它们 resolve 或 reject 时携带的数据结构(返回值)。
Promise.resolve(value)创建一个 resolved 的 promise。- 常见场景:将同步代码“包装”成异步的 Promise 对象(鸭子类型)。比如你有一个缓存函数,如果命中缓存直接返回同步数据,未命中则发起异步请求,为了保持外部调用方统一使用
await或.then,命中缓存时就可以直接return Promise.resolve(cacheData)。
- 常见场景:将同步代码“包装”成异步的 Promise 对象(鸭子类型)。比如你有一个缓存函数,如果命中缓存直接返回同步数据,未命中则发起异步请求,为了保持外部调用方统一使用
Promise.reject(error)创建一个 rejected 的 promise。- 常见场景:在拦截器(如 Axios 的请求拦截器)中,如果校验不通过(比如 Token 过期),需要直接阻断请求流程并抛出错误,此时可以
return Promise.reject(new Error('未登录'))。
- 常见场景:在拦截器(如 Axios 的请求拦截器)中,如果校验不通过(比如 Token 过期),需要直接阻断请求流程并抛出错误,此时可以
Promise.all(promises)。- 特点:并发处理(Concurrent),等待所有 promise 成功。如果有任意一个 reject,整体立刻 reject。注意:JS 是单线程的,这里的并发是指“同时发起多个异步任务”,而不是多线程的并行(Parallel)。底层其实和
forEach遍历给每个 promise 绑定.then的原理是一致的。 - 成功返回值:一个数组
[res1, res2, ...],结果数组的顺序与传入的 promises 数组顺序完全一致(即使实际完成的先后顺序不同)。 - 失败返回值:第一个被 reject 的 error 对象。
- 特点:并发处理(Concurrent),等待所有 promise 成功。如果有任意一个 reject,整体立刻 reject。注意:JS 是单线程的,这里的并发是指“同时发起多个异步任务”,而不是多线程的并行(Parallel)。底层其实和
Promise.allSettled(promises)。- 特点:等待所有 promise 结束(不论成功或失败)。永远不会被 reject。
- 成功返回值:一个对象数组。每个对象描述对应 promise 的结果,格式为
{ status: 'fulfilled', value: res }或{ status: 'rejected', reason: err }。
Promise.race(promises)。- 特点:赛跑机制,只等待第一个状态改变(settled)的 promise。
- 返回值:第一个完成的 promise 的结果(可能是 resolve 的值,也可能是 reject 的 error)。
Promise.any(promises)。- 特点:只等待第一个成功(fulfilled)的 promise。只有当所有的 promise 都失败时,整体才会失败。
- 成功返回值:第一个成功 resolve 的值。
- 失败返回值:如果全都 reject,返回一个特殊的
AggregateError对象,其errors属性是一个数组,包含了所有 promise 的错误信息。
Promise.try(callback, ...args)。(ES2025 提案阶段,常作为高级面试题)痛点背景:在处理一个不知道是同步还是异步的函数时,为了统一错误处理流程,我们以前可能会用
Promise.resolve().then(() => callback())来包裹它,但这会导致原本的同步代码被迫推迟到微任务队列中执行。// 假设这是第三方库提供的函数,你不知道它是同步还是异步
const unknownFn = () => console.log("我是同步代码,但我被推迟了");
// 以前的“蹩脚”包裹法
Promise.resolve().then(() => unknownFn());
console.log("我反而先执行了");
// 输出顺序:
// "我反而先执行了"
// "我是同步代码,但我被推迟了" (被丢进微任务了)特点与作用:
Promise.try接收一个函数(同步或异步皆可)。如果是同步函数,它会立即(同步)执行该函数,并将其结果包裹为 Promise;如果是异步函数,则正常返回 Promise。它完美实现了“同步代码同步执行,异步代码异步执行,且统一通过.catch()捕获错误”。使用示例:
const unknownFn = () => console.log("我同步执行啦");
// 现在的完美写法:
Promise.try(unknownFn)
.then((res) => console.log("执行成功"))
.catch((err) => console.log("捕获到了:", err.message));
console.log("我是后面的同步代码");
// 输出顺序:
// "我同步执行啦" (立刻同步执行)
// "我是后面的同步代码"
// "执行成功" (then 回调进入微任务)
Promise.withResolver()。ES2024 新引入的 api,返回一个 Promise 实例和对应的 resolve 和 reject。可以看下面的例子理解一下
这是一个带取消功能的定时器实现,参考自 https://modyqyw.top/blogs/2023/07/promise-with-resolvers
function cancellableTimeout(ms) {
let cancel;
const promise = new Promise((resolve, reject) => {
const timeoutId = setTimeout(resolve, ms);
cancel = () => {
clearTimeout(timeoutId);
reject(new Error("timeout cancelled"));
};
});
return { cancel, promise };
}
可以看到需要到回调里操作 resolve 和 reject,有一定的嵌套,使用体验不是很好,下面用 withResolver 优化下
function cancellableTimeout(ms) {
const { promise, resolve, reject } = Promise.withResolvers();
const timeoutId = setTimeout(resolve, ms);
const cancel = () => {
clearTimeout(timeoutId);
reject(new Error("timeout cancelled"));
};
return { promise, cancel };
}
Promise 与 Event Loop 结合的执行顺序
在面试中,常考“写出下列代码的输出顺序”一类题目。核心原则如下:
new Promise(executor)中的executor是同步执行的。Promise.prototype.then()或catch()会将回调函数推入微任务队列(Microtask Queue)。- 状态固化(不可逆):如果 Promise 已经是
resolved状态,再调用resolve()会被忽略;但即使是被忽略的后续代码(同步代码)依然会执行完毕。 async/await的底层转换:遇到await时,会先执行await右侧的表达式(同步),然后将await下面的所有代码打包成一个.then()放入微任务队列中。
进阶考点:微任务交替 (Tick 拆分) 与 Promise 解析机制
这是一个容易在面试中做错的“变态级”细节:如果在
then的回调中返回了一个新的 Promise,或者await了一个 Promise,它会产生额外的微任务消耗(即交替现象)。Promise.resolve()
.then(() => {
console.log(1);
// 注意这里:返回了一个 Promise!
return Promise.resolve(2);
})
.then((res) => {
console.log(res);
});
Promise.resolve()
.then(() => {
console.log("a");
})
.then(() => {
console.log("b");
})
.then(() => {
console.log("c");
});输出顺序:
1 -> a -> b -> c -> 2为什么 2 会在 c 之后才输出? 根据 ECMAScript 规范,当在
then中return Promise.resolve(2)时,引擎并不能直接把2传给下一个then,而是会隐式地调用两次额外的微任务来“解开(Unwrap)”这个 Promise。这会导致它被往后推迟了两个 Tick。这就是所谓的“交替现象”。底层伪代码拆解(硬核理解): 我们可以把
return Promise.resolve(2)在 V8 底层的动作,想象成被强行插入了两个隐藏的then:// 你的代码:
Promise.resolve()
.then(() => {
console.log(1);
return Promise.resolve(2);
})
.then((res) => console.log(res));
// V8 底层实际执行的等价逻辑(多出了两个隐藏的 Tick):
Promise.resolve()
.then(() => {
console.log(1);
let p = Promise.resolve(2);
// 隐藏的 Tick 1:V8 发现返回值是个 Promise,自动给你加上一个 then
p.then((value) => {
// 隐藏的 Tick 2:V8 拿到具体的值了,准备把外层的 Promise 给 resolve 掉
Promise.resolve().then(() => {
// 终于把 2 传给最外层的 then 了!
外层Promise_Resolve(value);
});
});
})
.then((res) => console.log(res));因为多出了这两个隐藏的微任务(Tick 1 对应打印
b的那一轮,Tick 2 对应打印c的那一轮),所以真正打印2的动作被延后到了第三轮。