Skip to content

Day2 JS 异步:Promise、async/await、浏览器事件循环 ​

📖 基础知识讲解【基础原理 + 高级深挖知识点】 ​

基础原理 ​

1. 先搞清楚:JS 为什么需要异步 ​

  • 进程: 操作系统调度任务、分配资源的单位;线程: 进程内的执行单元,共享进程资源。
  • 浏览器是多进程架构: 主进程(界面管理)、渲染进程(浏览器内核)、网络进程(资源请求)、GPU 进程(图形渲染)、插件进程。
  • 渲染进程的关键线程:
线程职责关键结论
GUI 渲染线程解析 DOM/CSS、布局、绘制与 JS 引擎线程互斥,JS 执行时渲染被挂起
JS 引擎线程解析执行 JS(单线程)长任务会直接卡死页面
定时器触发线程为 setTimeout / setInterval 计时计时结束才把回调丢进宏任务队列
事件触发线程管理事件队列点击、滚动等回调由它入队
异步请求线程处理 XHR / fetch请求完成后把回调入队
Web Worker独立工作线程不能操作 DOM,靠 postMessage 通信

核心结论: JS 执行是单线程的,但「计时、网络请求、事件监听」是浏览器其他线程在做,它们完成后把回调交给任务队列,由事件循环调度回主线程执行 —— 这就是异步的本质。

⚠️ 异步任务的来源不同:宏任务由宿主环境(浏览器/Node)提供,微任务由语言本身(ECMAScript)提供,这是区分二者最本质的标准。

2. Promise 三种状态 ​

  • 状态: pending(等待)、fulfilled(成功)、rejected(失败)。
  • 核心特点: 状态只能由 pending 变为 fulfilled 或 rejected,一旦变更不可逆,且结果值随状态一起固定。
方法作用返回值是否修改前序结果
then接收成功回调(可选失败)新的 Promise不修改,返回值决定新 Promise 的结果
catch捕获链路中的异常新的 Promise不修改,等价 then(null, onRejected)
finally无论成败都执行新的 Promise不修改前序 Promise 的结果(除非抛错)
js
Promise.resolve(1)
  .then((v) => v + 1) // 2
  .finally(() => "被忽略的返回值") // 不改变结果,继续传 2
  .then((v) => console.log(v)); // 2

⚠️ finally 的回调返回值会被忽略,但如果回调内部抛出异常或返回被 reject 的 Promise,新 Promise 会变为 rejected。

js
Promise.resolve(1)
  .then((v) => v + 1) // fulfilled 值 = 2
  .finally(() => {
    throw new Error("finally里面抛错!"); // ⚠️抛异常,覆盖状态
  })
  .then((v) => {
    console.log(v); // 不会执行!因为现在是reject
  })
  .catch((err) => {
    console.log(err.message); // 输出:finally里面抛错!
  });

Promise finally源码:

  • finally内部其实就then的实现,回调分别给了then成功和失败,所有回调始终会执行
js
/**
 * 给Promise原型挂载finally
 * @param {Function} onFinally
 * @returns Promise
 */
Promise.prototype.finally = function (onFinally) {
  // this 就是调用finally的那个promise
  const p = this;

  // finally的参数如果不是函数,要忽略,原样返回原promise
  if (typeof onFinally !== "function") {
    return p.then(onFinally, onFinally);
  }

  return p.then(
    // 成功分支
    (value) => {
      // 执行onFinally,等待它完成(onFinally可能返回Promise)
      return Promise.resolve(onFinally()).then(() => {
        // onFinally执行完,把原来的value继续传下去
        return value;
      });
    },
    // 失败分支
    (reason) => {
      return Promise.resolve(onFinally()).then(() => {
        // onFinally执行完,继续抛出原来的错误
        throw reason;
      });
    },
  );
};

Promise A+ 规范核心要点(手写前必背):

  1. Promise 有 pending / fulfilled / rejected 三种状态,只能单向流转一次,不可逆。
  2. 必须提供 then(onFulfilled, onRejected),两个参数都是可选的;不是函数时必须被忽略(这就是值穿透与异常穿透的规范出处)。
  3. then 的回调必须异步执行(放入微任务),不能在当前同步流程中直接调用。
  4. then 必须返回一个新的 Promise,支持链式调用。
  5. 回调返回值 x 需要走统一的 resolvePromise(x) 处理:
    • x === promise2 → 抛 TypeError(循环引用);
    • x 是对象/函数且 x.then 是函数 → 当作 thenable 继续递归;
    • 其他普通值 → 直接 resolve(x)。
  6. 同一个 Promise 的 resolve / reject 只能生效一次,thenable 的回调也要用 called 标志防止重复调用。

Promise 的优缺点(面试常作为开场铺垫):

维度内容
优点链式调用解决回调地狱;catch 统一收口错误;all / race 等组合能力支持并行编排
缺点无法取消(状态不可逆);不写 catch 时错误会被静默忽略;简单异步场景反而增加样板代码

3. async / await ​

  • 本质: Promise 的语法糖,用同步的写法表达异步流程。
  • await: 只会「暂停」当前 async 函数内部后续代码的执行,不会阻塞浏览器主线程,外部代码继续往下跑。
  • 返回值: async 函数的返回值自动包装为 Promise;函数内抛出的异常等价于 Promise.reject。
js
async function fn() {
  console.log("1 同步执行");
  await null; // 非 Promise 也会被包装,后续代码进入微任务
  console.log("3 微任务中执行");
}
fn();
console.log("2 外部代码继续执行");
// 输出顺序:1 → 2 → 3

等价转写(理解 await 最快的方式):

js
// 写法 A:async / await
async function example() {
  await someAsync();
  console.log("Done");
}

// 写法 B:完全等价的 Promise 形式
function example() {
  return Promise.resolve(someAsync()).then(() => {
    console.log("Done");
  });
}

记住一句话:await 表达式 之前的代码是同步执行的,await 之后的代码相当于被塞进了 .then 回调(微任务)。

4. 浏览器事件循环 ​

宏任务(MacroTask):

  • script 整体代码
  • setTimeout / setInterval
  • I/O、UI 事件回调
  • MessageChannel、setImmediate(非标准)

微任务(MicroTask):

  • Promise.then / catch / finally
  • queueMicrotask
  • MutationObserver
  • await 之后的代码

一轮循环流程(纯文本图):

text
┌──────────────────────────────────────────────┐
│ 1. 取出 1 个宏任务,执行到完毕                │
│            ↓                                 │
│ 2. 清空「全部」微任务队列                     │
│    (执行过程中新产生的微任务也要一起清空)    │
│            ↓                                 │
│ 3. 判断是否需要渲染 → UI 渲染(重排/重绘)     │
│            ↓                                 │
│ 4. 开启下一轮循环,回到第 1 步                │
└──────────────────────────────────────────────┘

执行流程拆解(按这四步背):

  1. 执行同步代码: 跑完 script 整体代码,期间遇到异步任务分类处理 —— 宏任务交给定时器/事件/请求线程,到点后入宏任务队列;微任务直接入微任务队列。
  2. 清空微任务队列: JS 执行栈清空后,按入队顺序执行所有微任务;执行中新产生的微任务会追加到当前队列尾部,本轮一起执行完。
  3. 渲染更新: 浏览器判断是否需要重排/重绘(并非每轮都渲染,由浏览器按帧率调度)。
  4. 取下一个宏任务: 从宏任务队列取一个任务执行,执行完回到第 2 步,循环往复。

宏任务 vs 微任务对比:

特性宏任务微任务
提供方宿主环境(浏览器 / Node)语言本身(ECMAScript)
触发时机DOM 渲染后DOM 渲染前
示例setTimeout、I/O、UI 事件Promise.then、MutationObserver
每轮数量只取 1 个清空整个队列(含执行中新增的)

经典顺序题:

js
console.log("script start"); // 1

setTimeout(() => console.log("setTimeout"), 0); // 宏任务

Promise.resolve()
  .then(() => console.log("promise1")) // 微任务
  .then(() => console.log("promise2")); // 微任务

queueMicrotask(() => console.log("queueMicrotask"));

console.log("script end"); // 2

// script start → script end → promise1 → queueMicrotask → promise2 → setTimeout

注意:queueMicrotask 并非「优先级更高」,而是入队顺序决定执行顺序,它和 Promise.then 共用同一条微任务队列。上例中 promise2 在第一个 then 执行完后才入队,所以排在 queueMicrotask 之后。

✨ 高级深挖知识点 ​

1. Promise 异常穿透 ​

链式调用中间若没有失败回调,错误会一路向后传递,直到遇到第一个能处理它的 catch / onRejected。

js
Promise.reject(new Error("boom"))
  .then((v) => console.log("不会执行", v))
  .then((v) => console.log("也不会执行", v))
  .catch((e) => console.log("统一捕获:", e.message)); // 统一捕获:boom

原理: then 在未传 onRejected 时,内部默认行为等价于 err => { throw err },异常继续传给下一个 Promise。

2. Promise 值穿透 ​

then 传入非函数参数(如 null、数字、字符串)时会被忽略,原值直接向后传递。

js
Promise.resolve(10)
  .then(null) // 值穿透
  .then(666) // 值穿透(非函数)
  .then((v) => console.log(v)); // 10

原理: then 内部做类型判断,非函数时使用默认回调 v => v。

3. then 返回 Promise 为什么「慢 2 拍」 ​

then 回调里 return 一个 Promise,相当于多出一个 Promise 实例需要被展开,规范里要额外消耗 2 个微任务时序(一次把 thenable 包装成 job,一次 then 回调入队)。

js
Promise.resolve()
  .then(() => {
    console.log(0);
    return Promise.resolve(4); // 返回 Promise,慢 2 拍
  })
  .then((res) => console.log(res));

Promise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2))
  .then(() => console.log(3))
  .then(() => console.log(5))
  .then(() => console.log(6));

// 输出:0 1 2 3 4 5 6

对比:如果写成 return 4(普通值),输出就是 0 1 4 2 3 5 6。面试被问「为什么 4 跑到 3 后面」,答「返回 thenable 多走两次微任务排队」即可。

4. 多条 Promise 链为什么交替执行 ​

多条已经 fulfilled 的 Promise 链同时发起链式调用时,每条链每轮只入队一个 then 回调,所以表现为交替执行 —— 这是防止单条链长期霸占微任务队列的必然结果。

js
Promise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2))
  .then(() => console.log(3));
Promise.resolve()
  .then(() => console.log(10))
  .then(() => console.log(20))
  .then(() => console.log(30));
Promise.resolve()
  .then(() => console.log(100))
  .then(() => console.log(200))
  .then(() => console.log(300));

// 输出:1 10 100 2 20 200 3 30 300

原理: 第一轮只有 3 个 then 回调在队列里,它们执行时才把下一级 then 回调入队,入队顺序决定了「按层横向」而不是「按链纵向」执行。

5. resolvePromise 的三个必踩坑(手写加分项) ​

坑场景处理方式
循环引用then 回调 return 了 then 自己返回的 promise2if (promise2 === x) reject(new TypeError(...))
取 then 属性时抛错返回值用 Object.defineProperty 定义了会抛异常的 then getter用 try-catch 包裹取值,catch 里 reject(e)
不规范的 thenable第三方 Promise 既 resolve 又 reject,或多次调用回调用 called 标志保证只生效一次
js
// 坑 1:循环引用
const p = Promise.resolve("ok");
const p2 = p.then(() => p2); // TypeError: Chaining cycle detected

// 坑 2:取 then 抛错
const x = {};
Object.defineProperty(x, "then", {
  get() {
    throw new Error("取 then 就报错");
  },
});
Promise.resolve()
  .then(() => x)
  .catch((e) => console.log(e.message));

6. Node.js 事件循环 6 个阶段 ​

text
   ┌───────────────────────────┐
┌─>│         timers            │  setTimeout / setInterval 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │   pending callbacks       │  上一轮延迟的系统级回调(如 TCP 错误)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     idle / prepare        │  仅内部使用
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │          poll             │  等待并处理 I/O 事件(核心阶段)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │          check            │  setImmediate 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤     close callbacks       │  socket.on('close') 等
   └───────────────────────────┘

与浏览器的核心差异:

对比项浏览器Node.js(v11+)
循环结构宏任务 → 微任务 → 渲染6 个阶段依次流转,无渲染阶段
微任务时机一个宏任务执行完清空微任务队列每个阶段切换时都会清空微任务队列
特有任务无process.nextTick(优先级高于 Promise 微任务)
特有 APIrequestAnimationFramesetImmediate(check 阶段执行)

Node v11 之前,timers 阶段会把同一阶段内的多个定时器回调全部跑完再清微任务;v11 之后与浏览器行为对齐(每个回调后清一次)。面试说清版本差异是加分项。

7. Promise 取消方案 ​

Promise 原生不支持 cancel(状态不可逆,无法中途撤回)。常用方案:

js
// 方案一:AbortController + fetch(推荐,能真正中断请求)
const controller = new AbortController();
fetch("/api/list", { signal: controller.signal }).catch((e) => {
  if (e.name === "AbortError") console.log("请求已取消");
});
controller.abort();

// 方案二:Promise.race 包装(只是忽略结果,不中断底层任务)
function withCancel(promise) {
  let cancel;
  const cancelPromise = new Promise((_, reject) => {
    cancel = () => reject(new Error("CANCELLED"));
  });
  return { promise: Promise.race([promise, cancelPromise]), cancel };
}

区别要讲清:AbortController 是真正中断底层请求;Promise.race 只是丢弃结果,底层任务仍在执行。

8. 微任务饥饿问题 ​

微任务队列必须完全清空才会进入渲染阶段。若微任务里递归产生新的微任务,会导致页面永久卡死。

js
function loop() {
  Promise.resolve().then(loop); // ❌ 微任务无限自增,页面彻底卡死
}
// 需要让出主线程时应使用宏任务:setTimeout(loop, 0)

❓ 高频面试题【基础简答 + 连环追问链 + 精简口述答案 + 项目关联话术】 ​

题 1:Promise 有什么特点?then / catch / finally 的执行顺序?什么是异常穿透? ​

精简口述答案:

Promise 有 pending、fulfilled、rejected 三种状态,状态变更不可逆。then 处理成功回调,catch 捕获链路异常,finally 一定会执行且不改变前序 Promise 的结果;三者都返回新的 Promise,所以可以链式调用。异常穿透指的是链式调用中间没有写失败回调时,异常会一路向后传递,直到被第一个 catch 捕获。

连环追问链:

  1. 追问:then 传入 null 会发生什么?
    • 答:触发值穿透。then 内部判断参数不是函数时会用默认回调 v => v,上一步的结果直接传给后面的 then。
  2. 追问:Promise.all / race / any / allSettled 的区别?
    • 答:
      • all:全部成功才成功,任意一个失败立刻 reject(后续成功结果被丢弃)。
      • race:第一个「完成」(无论成功失败)的结果即为最终结果。
      • any:第一个成功就返回,全部失败才 reject,错误是 AggregateError。
      • allSettled:等全部执行结束,返回 {status, value/reason} 数组,永远不会 reject。
  3. 追问:Promise.all 的结果顺序和返回快慢有关吗?
    • 答:无关。结果按传入数组的下标顺序填充,内部通过 result[idx] = res 保证顺序稳定。

✅ 项目关联话术(按真实经历替换):

业务首页有多个互不依赖的接口,我用 Promise.all 并行请求,把原本串行的等待时间压缩到最慢那一个接口的耗时;后来运营配置接口偶发超时导致整页白屏,改成 Promise.allSettled,拿到全部结果后对失败项做降级兜底渲染。

题 2:浏览器事件循环流程是什么?宏微任务有哪些?Node 和浏览器的差异? ​

精简口述答案:

浏览器一轮事件循环是:取出一个宏任务执行到完毕 → 清空整个微任务队列(包含执行中新产生的微任务)→ 判断是否渲染 → 开启下一轮。宏任务有 script、setTimeout、I/O、UI 事件;微任务有 Promise.then、queueMicrotask、MutationObserver 以及 await 之后的代码。Node 事件循环分 timers、pending callbacks、idle/prepare、poll、check、close callbacks 六个阶段,每个阶段切换时都会清空微任务队列,而不是整轮结束才清空,并且 process.nextTick 的优先级高于 Promise 微任务。

连环追问链:

  1. 追问:微任务队列只有一条吗?queueMicrotask 优先级更高吗?
    • 答:浏览器只有一条微任务队列,queueMicrotask 和 Promise.then 共用,执行顺序由入队顺序决定,没有额外优先级。Node 里 process.nextTick 是独立队列,会先于 Promise 微任务执行。
  2. 追问:setTimeout(fn, 0) 为什么不一定 0 毫秒执行?
    • 答:0 只表示「尽快加入宏任务队列」。HTML 规范规定嵌套层级超过 5 层时最小延迟为 4ms;而且还要等当前宏任务和所有微任务跑完、前面排队的宏任务执行完,实际延迟一定大于 0。
  3. 追问:requestAnimationFrame 属于宏任务还是微任务?
    • 答:都不属于。它是在渲染阶段之前执行的独立回调队列,每帧执行一次,适合做动画和分帧渲染。

✅ 项目关联话术(按真实经历替换):

项目有一个上万条数据的大列表渲染,一次性 v-for 渲染导致长任务阻塞主线程、页面卡死。我用 requestAnimationFrame 做分帧渲染,每帧只渲染固定条数,把单个长任务拆成多个短任务,让浏览器有机会在帧间完成渲染和响应交互。

题 3:async / await 的原理是什么?await 报错如何捕获? ​

精简口述答案:

async / await 是 Promise + Generator 的语法糖,可以理解为自动执行的 Generator(类似 co 库)。await 后面的表达式会被 Promise.resolve 包装,await 会暂停当前 async 函数内部的执行并把后续代码放进微任务,让出主线程但不阻塞外部代码。await 的 Promise 变为 rejected 等价于抛出异常,需要用 try-catch 包裹,或者在返回的 Promise 上 .catch。

连环追问链:

  1. 追问:await 后面是普通值(非 Promise)会发生什么?

    • 答:会被包装成已 resolved 的 Promise,后续代码依然进入微任务队列执行,不是同步往下走。
  2. 追问:多个 await 串行,如何改成并行提速?

    • 答:先同时发起请求拿到 Promise 实例,再统一 await;或者直接用 Promise.all。

      js
      // ❌ 串行:总耗时 = t1 + t2
      const a = await getA();
      const b = await getB();
      
      // ✅ 并行:总耗时 = max(t1, t2)
      const [a2, b2] = await Promise.all([getA(), getB()]);
  3. 追问:for await...of 和 forEach 里用 await 有什么区别?

    • 答:forEach 的回调是独立函数,外层函数不会等待它内部的 await,相当于并发触发且无法保证顺序;需要串行必须用 for / for await...of。

✅ 项目关联话术(按真实经历替换):

下单流程的接口有依赖关系,前一个接口返回的 token 要作为后一个接口的入参,用 async / await 串行书写比 Promise 链式更直观;而商详页的若干个独立接口我统一改成 Promise.all 并行,首屏时间下降明显。

题 4:执行顺序分析专题(必须能手推) ​

案例 1:async 函数 + Promise 构造器 ​

js
console.log(1);
async function asyncFn() {
  console.log(2);
  await console.log(3); // await 后面的表达式同步执行,后续代码进微任务
  console.log(4);
}
setTimeout(() => console.log(5), 0);
const promise = new Promise((resolve) => {
  console.log(6); // 构造器同步执行
  resolve(7);
});
promise.then(console.log);
asyncFn();
console.log(8);

// 同步:1 6 2 3 8 → 微任务:7 4 → 宏任务:5
// 输出:1 6 2 3 8 7 4 5

推导要点: new Promise 的执行器是同步的;promise.then 先于 asyncFn() 注册,所以 7 排在 4 前面。

案例 2:宏任务里产生微任务 ​

js
console.log("main start");

setTimeout(() => {
  console.log("T1:宏任务");
  Promise.resolve().then(() => console.log("T2:微任务"));
});

new Promise((resolve) => {
  console.log("T3:Promise构造");
  setTimeout(() => {
    console.log("T4:宏任务");
    resolve("T6"); // 此时 then 回调才入微任务队列
    Promise.resolve().then(() => console.log("T5:微任务"));
  }, 300);
}).then(console.log);

// 输出:main start → T3:Promise构造 → T1:宏任务 → T2:微任务 → T4:宏任务 → T6 → T5:微任务

推导要点: 每执行完一个宏任务就要清空微任务队列,所以 T2 紧跟 T1;resolve('T6') 先于 T5 入队,故 T6 先输出。

案例 3:多链交替 + 返回 Promise ​

js
Promise.resolve()
  .then(() => console.log(1))
  .then(() => {
    console.log(3);
    return Promise.resolve(7); // 慢 2 拍
  })
  .then(console.log);

Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(4))
  .then(() => console.log(5))
  .then(() => console.log(6))
  .then(() => console.log(8));

// 输出:1 2 3 4 5 6 7 8

案例 4:最经典的那道题 ​

js
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end"); // 进微任务
}
async function async2() {
  console.log("async2");
}

console.log("script start");
setTimeout(() => console.log("setTimeout"), 0);
async1();
new Promise((resolve) => {
  console.log("promise1");
  resolve();
}).then(() => console.log("promise2"));
console.log("script end");

// 输出:script start → async1 start → async2 → promise1 → script end
//      → async1 end → promise2 → setTimeout

精简口述答案(统一解题模板):

我的分析套路固定三步:第一步把所有同步代码(含 new Promise 执行器、await 右侧表达式、async 函数 await 之前的代码)先跑完;第二步按入队顺序清空微任务队列,注意 then 回调只有在前一个 then 执行后才入队,返回 Promise 还要多等 2 拍;第三步取一个宏任务执行,执行完再清一次微任务,循环到队列为空。

✅ 项目关联话术(按真实经历替换):

之前排查过一个「弹窗关闭动画偶发丢失」的问题,原因是我们在 Promise.then 里直接改状态再读 offsetHeight,微任务阶段 DOM 还没渲染,拿到的是旧布局。定位到事件循环时序后,把读取逻辑挪进 requestAnimationFrame,等渲染帧再取布局就稳定了。

✍️ 手撕代码【核心实现 + 边界 Case 自测清单 + 变种拓展题】 ​

题 1:简易版 MyPromise(面试必背) ​

核心实现:

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

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

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

    try {
      executor(resolve, reject);
    } catch (e) {
      // executor 同步抛错,直接 reject
      reject(e);
    }
  }

  then(onFulfilled, onRejected) {
    // 值穿透 + 异常穿透的关键:非函数时使用默认回调
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) => v;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (err) => {
            throw err;
          };

    const p2 = new MyPromise((resolve, reject) => {
      // 用 setTimeout 模拟异步(真实规范应为微任务,可用 queueMicrotask)
      const handleFulfilled = () => {
        setTimeout(() => {
          try {
            resolve(onFulfilled(this.value));
          } catch (e) {
            reject(e);
          }
        });
      };

      const handleRejected = () => {
        setTimeout(() => {
          try {
            // 注意是 resolve:失败回调的返回值让新 Promise 转为成功
            resolve(onRejected(this.reason));
          } catch (e) {
            reject(e);
          }
        });
      };

      if (this.state === "fulfilled") handleFulfilled();
      if (this.state === "rejected") handleRejected();
      if (this.state === "pending") {
        // 异步 resolve 场景:先把回调存起来
        this.onFulfilledCallbacks.push(handleFulfilled);
        this.onRejectedCallbacks.push(handleRejected);
      }
    });

    return p2;
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  finally(cb) {
    return this.then(
      (v) => {
        cb();
        return v; // 不改变成功结果
      },
      (err) => {
        cb();
        throw err; // 不吞掉失败原因
      },
    );
  }

  static resolve(val) {
    return val instanceof MyPromise
      ? val
      : new MyPromise((resolve) => resolve(val));
  }

  static reject(err) {
    return new MyPromise((_, reject) => reject(err));
  }
}

✅ 边界 Case 自测:

  • executor 同步抛错 → 自动 reject。
  • resolve / reject 多次调用 → 仅第一次生效(状态不可逆)。
  • 异步 resolve(如 setTimeout 里调用)→ 回调先入队,状态变更后统一触发。
  • 链式 then 多次调用 → 每次返回新 Promise。
  • then(null) → 值穿透;中间无 catch → 异常穿透到末尾。

🔁 变种拓展题: 把 setTimeout 换成 queueMicrotask,让执行时机符合 Promise A+ 规范(微任务而非宏任务);再补充 resolvePromise 处理返回值是 thenable / Promise 的情况。

A+ 版 resolvePromise(通过官方测试的关键一环):

js
/**
 * @param {*} x        then 回调的返回值
 * @param {*} promise2 then 返回的新 Promise 实例
 * @param {*} resolve  promise2 的 resolve
 * @param {*} reject   promise2 的 reject
 */
function resolvePromise(x, promise2, resolve, reject) {
  // 1. 循环引用:return 了自己
  if (promise2 === x) {
    return reject(new TypeError("Chaining cycle detected 循环引用"));
  }

  // 2. 对象或函数都可能是 thenable
  if ((typeof x === "object" && x !== null) || typeof x === "function") {
    let called; // 防止别人实现的 Promise 既成功又失败 / 多次调用
    try {
      const then = x.then; // 取 then 可能触发 getter 抛错
      if (typeof then === "function") {
        then.call(
          x,
          (y) => {
            if (called) return;
            called = true;
            // y 可能还是 Promise,递归直到普通值
            resolvePromise(y, promise2, resolve, reject);
          },
          (r) => {
            if (called) return;
            called = true;
            reject(r);
          },
        );
      } else {
        // 只是普通对象,直接成功
        if (called) return;
        called = true;
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 3. 普通值直接成功
    resolve(x);
  }
}

接入方式: 把 then 里的 resolve(onFulfilled(this.value)) 改成:

js
const x = onFulfilled(this.value);
resolvePromise(x, p2, resolve, reject);

注意 p2 必须在 queueMicrotask / setTimeout 回调里访问,否则同步阶段 p2 还未赋值完成会报 undefined。

🧪 规范自测: npm i promises-aplus-tests -g,然后执行 promises-aplus-tests promise.js(文件需 module.exports = MyPromise 并实现 Promise.deferred),872 个用例全绿才算真正符合 A+。

js
MyPromise.defer = MyPromise.deferred = function () {
  const dfd = {};
  dfd.promise = new MyPromise((resolve, reject) => {
    dfd.resolve = resolve;
    dfd.reject = reject;
  });
  return dfd;
};

题 2:手写 Promise.all ​

核心实现:

js
Promise.myAll = function (promiseList) {
  return new Promise((resolve, reject) => {
    // 非可迭代对象直接 reject
    if (!Array.isArray(promiseList)) {
      return reject(new TypeError("argument must be an array"));
    }

    const len = promiseList.length;
    if (len === 0) return resolve([]); // 空数组立刻成功

    const result = new Array(len);
    let count = 0;

    promiseList.forEach((p, idx) => {
      // Promise.resolve 兼容传入普通值
      Promise.resolve(p).then((res) => {
        result[idx] = res; // 按下标写入,保证顺序
        if (++count === len) resolve(result);
      }, reject); // 任意失败立刻 reject
    });
  });
};

✅ 边界 Case 自测: 入参包含普通非 Promise 值;空数组立即 resolve;任意一项失败立刻 reject;结果顺序与入参顺序一致(与返回快慢无关)。

🔁 变种拓展题: 手写 allSettled 与 race。

js
Promise.myAllSettled = function (list) {
  return Promise.myAll(
    list.map((p) =>
      Promise.resolve(p).then(
        (value) => ({ status: "fulfilled", value }),
        (reason) => ({ status: "rejected", reason }),
      ),
    ),
  );
};

Promise.myRace = function (list) {
  return new Promise((resolve, reject) => {
    // 空数组永远 pending,与原生一致
    list.forEach((p) => Promise.resolve(p).then(resolve, reject));
  });
};

题 3:sleep 函数 + 并发控制 ​

核心实现:

js
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function run() {
  console.log("start");
  await sleep(1000);
  console.log("end"); // 1 秒后输出
}

🔁 变种拓展题: 实现并发请求池,限制同时进行的任务数量(高频进阶题)。

js
async function limitRequest(tasks, limit = 3) {
  const results = [];
  const executing = new Set();

  for (const [idx, task] of tasks.entries()) {
    const p = Promise.resolve().then(() => task());
    results[idx] = p;
    executing.add(p);
    // 任务结束后从执行池移除
    p.finally(() => executing.delete(p));

    if (executing.size >= limit) {
      await Promise.race(executing); // 等任意一个完成再补位
    }
  }

  return Promise.all(results);
}

✅ 边界 Case 自测: 任务数小于并发上限;某个任务失败是否影响其他任务;返回结果顺序与入参一致。