Appearance
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+ 规范核心要点(手写前必背):
- Promise 有
pending/fulfilled/rejected三种状态,只能单向流转一次,不可逆。 - 必须提供
then(onFulfilled, onRejected),两个参数都是可选的;不是函数时必须被忽略(这就是值穿透与异常穿透的规范出处)。 then的回调必须异步执行(放入微任务),不能在当前同步流程中直接调用。then必须返回一个新的 Promise,支持链式调用。- 回调返回值
x需要走统一的resolvePromise(x)处理:x === promise2→ 抛TypeError(循环引用);x是对象/函数且x.then是函数 → 当作 thenable 继续递归;- 其他普通值 → 直接
resolve(x)。
- 同一个 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/finallyqueueMicrotaskMutationObserverawait之后的代码
一轮循环流程(纯文本图):
text
┌──────────────────────────────────────────────┐
│ 1. 取出 1 个宏任务,执行到完毕 │
│ ↓ │
│ 2. 清空「全部」微任务队列 │
│ (执行过程中新产生的微任务也要一起清空) │
│ ↓ │
│ 3. 判断是否需要渲染 → UI 渲染(重排/重绘) │
│ ↓ │
│ 4. 开启下一轮循环,回到第 1 步 │
└──────────────────────────────────────────────┘执行流程拆解(按这四步背):
- 执行同步代码: 跑完 script 整体代码,期间遇到异步任务分类处理 —— 宏任务交给定时器/事件/请求线程,到点后入宏任务队列;微任务直接入微任务队列。
- 清空微任务队列: JS 执行栈清空后,按入队顺序执行所有微任务;执行中新产生的微任务会追加到当前队列尾部,本轮一起执行完。
- 渲染更新: 浏览器判断是否需要重排/重绘(并非每轮都渲染,由浏览器按帧率调度)。
- 取下一个宏任务: 从宏任务队列取一个任务执行,执行完回到第 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 自己返回的 promise2 | if (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 微任务) |
| 特有 API | requestAnimationFrame | setImmediate(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捕获。
连环追问链:
- 追问:
then传入null会发生什么?- 答:触发值穿透。
then内部判断参数不是函数时会用默认回调v => v,上一步的结果直接传给后面的then。
- 答:触发值穿透。
- 追问:
Promise.all/race/any/allSettled的区别?- 答:
all:全部成功才成功,任意一个失败立刻 reject(后续成功结果被丢弃)。race:第一个「完成」(无论成功失败)的结果即为最终结果。any:第一个成功就返回,全部失败才 reject,错误是AggregateError。allSettled:等全部执行结束,返回{status, value/reason}数组,永远不会 reject。
- 答:
- 追问:
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 微任务。
连环追问链:
- 追问:微任务队列只有一条吗?
queueMicrotask优先级更高吗?- 答:浏览器只有一条微任务队列,
queueMicrotask和Promise.then共用,执行顺序由入队顺序决定,没有额外优先级。Node 里process.nextTick是独立队列,会先于 Promise 微任务执行。
- 答:浏览器只有一条微任务队列,
- 追问:
setTimeout(fn, 0)为什么不一定 0 毫秒执行?- 答:
0只表示「尽快加入宏任务队列」。HTML 规范规定嵌套层级超过 5 层时最小延迟为 4ms;而且还要等当前宏任务和所有微任务跑完、前面排队的宏任务执行完,实际延迟一定大于 0。
- 答:
- 追问:
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。
连环追问链:
追问:
await后面是普通值(非 Promise)会发生什么?- 答:会被包装成已 resolved 的 Promise,后续代码依然进入微任务队列执行,不是同步往下走。
追问:多个
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()]);
追问:
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 自测: 任务数小于并发上限;某个任务失败是否影响其他任务;返回结果顺序与入参一致。