Promise 我手写过几遍才记住:then 为什么强制异步、executor 和 then 回调的执行时机差在哪、thenable 展平和循环引用怎么处理。
Q1: Promise 的历史?为什么要有它?
1
回调地狱时代
早期的 JS 依赖回调函数处理异步。如果多个异步操作有依赖关系,代码会变成不断向右缩进的”回调地狱”,极难维护,且错误处理非常容易遗漏。
2
致命痛点: 控制反转与信任问题
当你把回调函数传给第三方库执行时,你无法确定它会调用几次(可能调多次、可能不调)。你失去了对代码执行权力的控制(控制反转)。
3
ES6 引入 Promise
Promise 提出了一个革命性的思想:不把回调传给第三方,而是让第三方返回一个”承诺”对象。拿到承诺后,我们自己决定在它成功或失败时做什么。
4
核心机制: 状态机模型 + 控制反转的再反转
Promise 是一个单向不可逆的有限状态机(pending → fulfilled/rejected)。状态一旦改变,永远定格。
5
关键洞察: then 回调强制异步
即使 Promise 已经 fulfilled,
.then 的回调也会被强制放入微任务队列,保证执行顺序一致性(防 Zalgo 现象)。Promise 将异步操作的控制权从第三方接管回了调用者手中,并通过状态机保证了执行的唯一性和可靠性。
🚫 没有 Promise (回调时代)
你去餐厅点餐,点完后你把自己家的钥匙给了服务员,跟他说:“做好了去我家帮我把菜摆好”。如果服务员是个粗心的人,他可能去你家摆了三次,也可能根本没去。
✅ 引入 Promise
你去餐厅点餐,服务员给你一个”取餐器”(Promise)。你拿着取餐器回座位。取餐器只有两种最终状态:响了(做好了)或者显示异常(做砸了)。一旦响了,你自己去取餐。你牢牢掌握着主动权。
Q2: Promise 的本质与核心机制是什么?
Promise 的本质是一个状态机,核心逻辑只有 3 步:状态机的三态转换规则
Promise 严格遵循有限状态机模型,只有两种转换路径:thenable 对象与 Promise 的区别
thenable 是任何具有 .then 方法的对象,它不是真正的 Promise,但可以被 Promise.resolve() 识别和展开:
Q3: then 回调为什么必须是异步的?
很多人只知道.then 是微任务,但实际更在意的是:如果 Promise 已经同步 resolve 了,为什么 V8 依然要把 .then 强行扔进微任务队列?
如果 .then 的回调在 Promise 已 fulfilled 时同步执行、在 pending 时异步执行,代码的行为就会因为内部状态的时序不同而产生极其诡异的偏差(Zalgo 现象)。为了保证执行顺序的绝对一致性,ECMA-262 的 PerformPromiseThen 无论 Promise 当前是什么状态,都会先创建一个 reaction 挂到该 Promise 上:若 Promise 已经 settle,则立即由 TriggerPromiseReactions 把对应的 PromiseReactionJob(V8 内部对应 PromiseReactionJobTask)加入微任务队列;若还是 pending,则先把 reaction 挂起,等它 settle 时再由 TriggerPromiseReactions 入队。也就是说,then 回调永远不会同步执行。
Zalgo 现象与一致性保证
“Zalgo”是指异步 API 有时同步执行、有时异步执行,导致代码行为不可预测:微任务队列的入队时机
Promise 的.then 回调在以下时机入队微任务队列:
Q4: Promise API 核心对比
基础梳理
经典场景:如果传入空数组会怎样?
Promise.all([]) 会立即 resolve 一个空数组。这是规范行为——“全部成功”在没有任务时自然就是成功的。
Promise.all 的并发控制
Promise.all 没有并发限制,所有 Promise 同时发起。在实际项目中,需要手动实现并发控制:
Promise.any 的 AggregateError
当Promise.any 的所有 Promise 都 reject 时,会抛出 AggregateError:
Q5: 手写 Promise A+:核心逻辑
手写 Promise 的关键不是背代码,而是理解四件事:状态机、回调队列、异步调度、thenable 展平。resolvePromise 的四种情况
resolvePromise 需要处理 .then 回调返回值的所有可能情况:
Promise A+ 规范的核心要求
Promise A+ 规范(Promises/A+)定义了.then 方法的行为标准:
.then必须返回一个新的 Promise(支持链式调用)onFulfilled和onRejected必须异步执行(微任务)onFulfilled和onRejected必须作为函数调用(this为undefined)resolvePromise必须处理 thenable 展平和循环引用
Q6: async/await:Generator + Promise 的语法糖
await 的本质是 yield + Promise.resolve(value).then()。当 Promise resolve 后,自动执行器调用 gen.next(value) 恢复执行。错误通过 gen.throw(err) 注入到 generator 内部,所以 await 的错误能被 try/catch 捕获。
async/await 的执行顺序
await 下面的代码会被打包成微任务,这是理解 async/await 执行顺序的关键:
await右边的函数立刻同步执行(输出 2)await下面的代码被打包进隐形的Promise.then()(输出 3 是微任务)- 所以
await下面的代码一定在当前同步代码之后执行(输出 4 在输出 3 之前)
async/await 输出题图解
面试中最经典的 async/await 输出题,逐步拆解:await 的本质转换公式
记住这个转换公式,所有 async/await 输出题都能秒杀:错误处理的最佳实践
async/await 的错误处理有两种模式:Q7: Promise 状态吸收是什么?
Promise 的状态吸收(State Absorption)是指:Promise 一旦从 pending 转变为 fulfilled 或 rejected,后续所有的resolve 或 reject 调用都会被静默忽略,不会抛出任何错误。 这是 ECMAScript 规范中 NewPromiseResolveThenableJob 与 PromiseResolveFunctions 配合的结果——状态机模型保证了 Promise 的状态只能改变一次,永远不可逆。
状态吸收在实际业务中的影响
状态吸收并非只是”面试冷知识”,它在真实项目中会直接影响你对竞态条件的处理:Q8: 如何实现并发任务控制?
Promise.all 没有并发限制,所有 Promise 同时发起。在实际项目中,批量请求 100 个接口会瞬间打满浏览器的同域并发连接数(Chrome 限制 6 个),导致后面的请求排队等待,甚至触发服务端限流。并发任务控制的核心思想是:维护一个固定大小的执行池,池满时用 Promise.race 等待任意一个完成后再加入新任务。
更简洁的实现:parallelLimit 函数
p-limit 的设计思路
社区中最流行的并发控制库p-limit 的核心原理与此类似,但它使用队列来管理等待中的任务,避免了多余的 Promise.race 微任务:
🔍 底层源码:V8 是如何实现 Promise 的?
V8 中 Promise 的微任务到底是怎么入队的?
V8 内部使用 Torque 语言实现 Promise(详见 V8 源码:src/builtins/promise-then.tq):
async/await 在底层到底是什么?
async/await 在 V8 内部被直接编译为 Generator 状态机 + 自动执行器。await expr 被转换为 yield Promise.resolve(expr),自动执行器在 Promise resolve 后调用 gen.next(res) 恢复执行,在 reject 时调用 gen.throw(err) 注入错误。
Q9: try/catch 能捕获 Promise 错误吗?什么可以捕获?
这是面试中最高频的 Promise 错误处理问题。很多人以为try/catch 万能,但它对异步的 Promise rejection 根本无能为力。
try/catch 能捕获 Promise 错误吗?
结论:try/catch 不能捕获异步的 Promise rejection,只能捕获同步错误和 await 的 rejection。
根本原因是 try/catch 是同步的——它在当前调用栈执行完毕后就结束了。而 Promise 的 reject 是异步的(微任务),发生在 try/catch 已经退出之后:
什么可以捕获 Promise 错误?
有四种方式可以捕获 Promise 的 rejection: 方式 1:.catch() 方法
async/await + try/catch
unhandledrejection 事件(全局兜底)
Promise.allSettled vs Promise.all
Promise 错误处理最佳实践
规则 1:永远不要让 Promise rejection 无人处理try/catch 只能捕获同步错误和 await 的 rejection;异步的 Promise rejection 必须用 .catch() 或 unhandledrejection 事件捕获。最佳实践是:链尾 .catch() 兜底 + 局部 .catch() 做降级。及格回答 vs 满分回答
问题:“说说 Promise 的核心原理,以及它解决了什么问题?”
及格回答(60-70分):“Promise 是用来解决回调地狱的。它是状态机,三种状态只能单向转换。then 的回调异步执行(微任务)。支持链式调用。Promise.all 全部成功才 resolve,race 取第一个完成的。”问题出在:背了规则但不理解设计,缺乏底层深度。 满分回答(90-100分):
“Promise 解决的核心问题不仅是链式调用,更是解决了控制反转与信任问题。作为单向不可逆的有限状态机,它保证了状态一旦变更为兑现(fulfilled)或拒绝(rejected),就只会执行一次。[1]”
“then回调强制异步执行。根据 ECMA-262 规范,即使 Promise 已经兑现,PerformPromiseThen也会创建一个PromiseReactionJob(V8 内部对应PromiseReactionJobTask)放入微任务队列,绝对保证执行顺序一致性(防 Zalgo 现象)。手写 Promise 时最难点在于resolvePromise的 thenable 展平机制——只要对象有.then就按 A+ 2.3.3 递归解析,并检测循环引用。[2]”
“async/await 不是什么全新的运行时机制,它在 V8 内部被编译为 Generator 状态机 + 自动执行器。await expr被转换为yield Promise.resolve(expr)。错误通过gen.throw(err)重新注入到函数体内,这就是为什么await的错误能被try/catch捕获。[3]“
常见追问
Promise.all 的实现原理?
Promise.all 的实现原理?
答题要点:
点出核心细节:如何保证返回顺序、如何判断结束。高分回答:
“核心步骤有三:1) 遍历 promises 数组,用
Promise.resolve() 包裹每一个元素以兼容非 Promise 值。2) 使用 index 将结果存入 results 数组,而不是使用 push,这是为了强行保证返回结果的顺序与传入顺序一致。3) 用一个独立的 count 变量跟踪完成数,当 count === len 时执行最终的 resolve(results)。任何一个拒绝则直接触发外层的 reject(fail-fast)。”resolvePromise 为什么要处理循环引用?
resolvePromise 为什么要处理循环引用?
答题要点:
体现对规范安全性的理解。高分回答:
“如果
.then 的回调返回了 promise 自身(比如 const p = p1.then(() => p)),如果没有循环引用检测,程序就会陷入无限递归死等自己的状态改变。规范明确要求,在 resolvePromise 内部必须检测 if (promise2 === x),一旦命中必须立刻拒绝,并抛出一个 TypeError: Chaining cycle detected。”async/await 中的 await 到底干了什么事?
async/await 中的 await 到底干了什么事?
答题要点:
不要只答”等待”,要说出暂停和微任务包转。高分回答:
“
await 右侧调用的函数会立刻同步执行。而 await 下面的所有代码,都会被打包塞进一个隐形的 Promise.then() 里,变成一个微任务去排队!所以在 Event Loop 中,遇到 await 并不是线程卡住了,而是当前协程让出控制权,将后续代码作为微任务注册到该 Promise 上。”🔥 值穿透, 状态不可逆, 隐式包装三大陷阱?
🔥 值穿透, 状态不可逆, 隐式包装三大陷阱?
答题要点:
用代码推演三个经典陷阱。高分回答:
“三个经典陷阱:
-
值穿透:
.then接收非函数时发生值穿透。Promise.resolve(1).then(2).then(console.log)输出1。 -
状态不可逆:
resolve('success'); throw new Error('error')——throw 被忽略,因为状态已经 fulfilled。 -
隐式包装:
.then(() => new Error('error'))不会触发 catch,因为return Error被包装为成功的 Promise。只有throw或return rejectedPromise才会触发 catch。”
Promise.all 和 Promise.allSettled 的区别?
Promise.all 和 Promise.allSettled 的区别?
答题要点:
点出 fail-fast 行为和容错能力的差异。高分回答:
“
Promise.all 是 fail-fast 策略——任何一个 Promise reject,整个 Promise.all 立刻 reject,其他 Promise 的结果被忽略。这在批量请求场景下会导致”一个失败全部失败”的问题。Promise.allSettled 是 ES2020 引入的,它会等待所有 Promise 都敲定(fulfilled 或 rejected),然后返回一个包含每个结果状态的数组。适合”不放弃任何一个结果”的场景,比如批量上传文件,即使部分失败也要知道哪些成功哪些失败。如何实现 Promise 的并发控制?
如何实现 Promise 的并发控制?
答题要点:
手写一个带并发限制的 Promise 调度器。高分回答:
“更优雅的方案是使用 p-limit 这类库,或使用 ES2024 的
Promise.all 没有并发限制,所有 Promise 同时发起。如果一次请求 100 个接口,会瞬间打满浏览器的并发连接数(Chrome 同域限制 6 个),导致后面的请求排队等待。实现思路:维护一个执行池,大小为 limit。当池满时,用 Promise.race 等待任意一个完成后,再加入新的任务。Promise.withResolvers() 简化实现。”🔥 如何取消一个已经发起的 Promise?
🔥 如何取消一个已经发起的 Promise?
答题要点:
说明 Promise 本身不可取消,需要借助 AbortController。高分回答:
“Promise 本身设计上没有取消机制——状态一旦确定就不可逆。但可以通过以下方式实现’取消’效果:方案 1:AbortController(推荐)方案 2:竞态取消方案 3:Promise.race 与哨兵值
Promise 链式调用的错误处理最佳实践?
Promise 链式调用的错误处理最佳实践?
答题要点:
说明 .catch() 的位置对错误捕获范围的影响。高分回答:
“Promise 链式调用中,位置 2:中间(局部错误处理)最佳实践:
.catch() 的位置决定了它能捕获哪些错误:位置 1:链尾(捕获整个链的错误)- 链尾
.catch()兜底,防止未捕获的 Promise rejection - 中间
.catch()做降级处理,保证链继续执行 - 避免在
.then()中嵌套 Promise,保持链式调用的扁平化”
🔥 什么是 Promise 的状态吸收(state absorption)?
🔥 什么是 Promise 的状态吸收(state absorption)?
答题要点:
Promise 的状态一旦确定就不可变;resolve 一个 Promise 时会发生状态传递,外层 Promise 采纳 状态吸收在链式调用中的关键影响:与状态不可逆的区别:状态不可逆强调的是’同一个 Promise 不能 settle 两次’;状态吸收强调的是’
then 回调返回的那个 Promise 的状态,而不是由”最后一个 settle 的 Promise”决定。高分回答:
“Promise 的状态吸收是指:当 resolve() 接收到一个 Promise(或 thenable)时,新 Promise 的状态会完全’吸收’该 Promise 的状态——它不会立即 fulfilled,而是等到被吸收的 Promise 也 settle 后,才以相同的值或原因变成对应的状态。这是 resolvePromise 内部机制的核心体现:resolve(p) 时,p 的状态会传递给外层 Promise,外层不会立即 fulfilled’。两者结合保证了 Promise 链中值和状态传递的确定性。“延伸阅读与引用参考
- 🔗 ECMAScript 规范:Promise Objects:ECMA-262 官方规范,定义了 Promise 状态机、
PerformPromiseThen算法与PromiseReactionJob微任务入队机制。[1] - 📖 V8 Blog: Faster async/await with promises:V8 团队 2018 年发布的官方博文,解释了引入”fast async”优化后,await 不再额外创建多余 Promise,从而显著提升 async/await 性能的底层原理。[2]
- 🟢 Jake Archibald: Tasks, microtasks, queues and schedules:Chrome 开发者布道师 2015 年的神作,用交互式 Demo 深刻讲解了 Promise 微任务的执行时序与浏览器渲染的交错关系。[3]
- 📘 Promises/A+ 规范:社区标准,定义了
.then方法的行为准则、thenable 展平机制与循环引用检测,手写 Promise 的权威参考。 - 📗 Dan Abramov: How Does setState Know What to Do?:Dan Abramov 在 overreacted.io 上的深度博文,用 Promise 类比解释了 React 的命令模式与状态注入机制。
