Skip to main content

Promise

Promise 异步处理,提供更优雅的方式(链式调用)解决了 callback hell 的问题,并且更易于处理错误

  • promise 有三种状态,状态间的转移是不可逆的,只能有 pengding to resolved 或者 pending to rejected 两种变化
    • pending
    • resolved
    • rejected
  • 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.reject(error) 创建一个 rejected 的 promise。

    • 常见场景:在拦截器(如 Axios 的请求拦截器)中,如果校验不通过(比如 Token 过期),需要直接阻断请求流程并抛出错误,此时可以 return Promise.reject(new Error('未登录'))
  • Promise.all(promises)

    • 特点:并发处理(Concurrent),等待所有 promise 成功。如果有任意一个 reject,整体立刻 reject。注意:JS 是单线程的,这里的并发是指“同时发起多个异步任务”,而不是多线程的并行(Parallel)。底层其实和 forEach 遍历给每个 promise 绑定 .then 的原理是一致的。
    • 成功返回值:一个数组 [res1, res2, ...],结果数组的顺序与传入的 promises 数组顺序完全一致(即使实际完成的先后顺序不同)。
    • 失败返回值:第一个被 reject 的 error 对象。
  • 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 结合的执行顺序

在面试中,常考“写出下列代码的输出顺序”一类题目。核心原则如下:

  1. new Promise(executor) 中的 executor同步执行的。
  2. Promise.prototype.then()catch() 会将回调函数推入微任务队列(Microtask Queue)
  3. 状态固化(不可逆):如果 Promise 已经是 resolved 状态,再调用 resolve() 会被忽略;但即使是被忽略的后续代码(同步代码)依然会执行完毕。
  4. 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 规范,当在 thenreturn 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 的动作被延后到了第三轮。