从零手写一个 Promise:深入理解 JavaScript 异步编程的本质
每个前端和 Node.js 开发者每天都在用 Promise但有多少人真正理解它内部是怎么运转的今天我们不复述 MDN 文档而是从零开始亲手实现一个符合 Promises/A 规范的迷你 Promise把异步编程的本质看个通透。为什么你要关心 Promise 的实现你可能觉得我会用 async/await会用 .then()够了。 但你是否遇到过这些困惑为什么 new Promise 里的代码是立即执行的但 .then() 的回调却不是为什么 Promise 的 .then() 能链式调用而且能 穿透 返回值事件循环里Promise 的微任务到底排在哪个位置这些问题的答案全都藏在 Promise 的实现细节里。理解了它你就不再是 API 的使用者而是异步模型的掌控者。第一步Promise 的状态机模型Promise 的本质是一个有限状态机只有三种状态PENDING ──resolve──→ FULFILLED │ └──reject──→ REJECTED一旦从 PENDING 变成 FULFILLED 或 REJECTED就再也无法改变 —— 这就是 承诺 的含义。让我们先把骨架搭起来class MiniPromise { static PENDING pending; static FULFILLED fulfilled; static REJECTED rejected; constructor(executor) { this.status MiniPromise.PENDING; this.value undefined; // 成功的值 this.reason undefined; // 失败的原因 this.onFulfilledCallbacks []; // 成功回调队列 this.onRejectedCallbacks []; // 失败回调队列 // resolve 和 reject 用来改变状态 const resolve (value) { if (this.status MiniPromise.PENDING) { this.status MiniPromise.FULFILLED; this.value value; // 状态改变后执行所有排队的回调 this.onFulfilledCallbacks.forEach(fn fn()); } }; const reject (reason) { if (this.status MiniPromise.PENDING) { this.status MiniPromise.REJECTED; this.reason reason; this.onRejectedCallbacks.forEach(fn fn()); } }; // executor 立即执行这就是为什么 new Promise 里的代码同步运行 try { executor(resolve, reject); } catch (err) { reject(err); } } }注意一个关键细节executor(resolve, reject) 是在构造函数中同步调用的。这就解释了下面这段代码的行为console.log(1); new MiniPromise((resolve) { console.log(2); resolve(done); }); console.log(3); // 输出1 → 2 → 3 全是同步的第二步实现 .then() —— Promise 的灵魂.then() 是 Promise 最核心的方法它需要处理三种情况调用 .then() 时 Promise 已经 fulfilled → 直接执行 onFulfilled调用 .then() 时 Promise 已经 rejected → 直接执行 onRejected调用 .then() 时 Promise 还在 pending → 把回调存起来等状态改变再执行但还有一个最关键的要求根据 Promises/A 规范.then() 的回调必须异步执行即作为微任务。在浏览器中我们用 queueMicrotask在 Node.js 中可以用 process.nextTick。then(onFulfilled, onRejected) { // 参数穿透如果你不传回调值/错误要能传递给下一个 .then() onFulfilled typeof onFulfilled function ? onFulfilled : value value; onRejected typeof onRejected function ? onRejected : reason { throw reason; }; // 返回一个新的 Promise实现链式调用 const promise2 new MiniPromise((resolve, reject) { const handleFulfilled () { // 关键微任务异步执行 queueMicrotask(() { try { const x onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (err) { reject(err); } }); }; const handleRejected () { queueMicrotask(() { try { const x onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (err) { reject(err); } }); }; if (this.status MiniPromise.FULFILLED) { handleFulfilled(); } else if (this.status MiniPromise.REJECTED) { handleRejected(); } else { // 还在 PENDING先排队 this.onFulfilledCallbacks.push(handleFulfilled); this.onRejectedCallbacks.push(handleRejected); } }); return promise2; }这里有两个精妙的设计每次 .then() 都返回一个新 Promise —— 这就是链式调用的秘密。参数穿透 —— value value 和 reason { throw reason } 确保了即使你不写 .catch()错误也能沿着链条传递下去。第三步resolvePromise —— 处理 返回值是 Promise 的情况这是整个实现中最烧脑的部分。.then() 的回调可能返回各种东西普通值、另一个 Promise、甚至是一个 thenable 对象。规范对此有严格定义function resolvePromise(promise2, x, resolve, reject) { // 1. 不能返回自身否则死循环 if (promise2 x) { return reject(new TypeError(Chaining cycle detected!)); } // 2. 如果 x 是 MiniPromise 实例等它决议 if (x instanceof MiniPromise) { x.then(resolve, reject); return; } // 3. 如果 x 是 thenable有 .then 方法的对象/函数 // 这是为了兼容其他 Promise 库如 bluebird、Q if (x ! null (typeof x object || typeof x function)) { let called false; // 防止多次调用 try { const then x.then; if (typeof then function) { then.call( x, (y) { if (called) return; called true; // 递归解析因为 resolve 的值可能还是一个 Promise resolvePromise(promise2, y, resolve, reject); }, (r) { if (called) return; called true; reject(r); } ); } else { // x 是个普通对象直接 resolve resolve(x); } } catch (err) { if (called) return; called true; reject(err); } } else { // 4. x 是普通值直接 resolve resolve(x); } }called 标志位是一个防御性编程的细节如果外部 thenable 同时调用了 resolve 和 reject我们只认第一个。第四步补全常用静态方法有了核心骨架静态方法就是水到渠成// Promise.resolve static resolve(value) { if (value instanceof MiniPromise) return value; return new MiniPromise((resolve) resolve(value)); } // Promise.reject static reject(reason) { return new MiniPromise((_, reject) reject(reason)); } // Promise.all —— 全部成功才成功 static all(promises) { return new MiniPromise((resolve, reject) { const results []; let count 0; const list Array.from(promises); if (list.length 0) return resolve([]); list.forEach((p, i) { MiniPromise.resolve(p).then( (value) { results[i] value; // 注意用 i 保证顺序 if (count list.length) resolve(results); }, reject // 任一失败即失败 ); }); }); } // Promise.race —— 谁先决议就用谁 static race(promises) { return new MiniPromise((resolve, reject) { for (const p of promises) { MiniPromise.resolve(p).then(resolve, reject); } }); } // catch 和 finally 只是 then 的语法糖 catch(onRejected) { return this.then(null, onRejected); } finally(callback) { return this.then( value MiniPromise.resolve(callback()).then(() value), reason MiniPromise.resolve(callback()).then(() { throw reason; }) ); }注意 Promise.all 中 results[i] value 这一行 —— 它用索引 i 而非 push 来保证结果数组的顺序与输入一致这是一个容易被忽略的正确性细节。完整测试看看我们的 Promise 能不能跑起来// 测试 1基本链式调用 new MiniPromise((resolve) { setTimeout(() resolve(Hello), 100); }) .then(msg msg World) .then(msg { console.log(msg); // Hello World }); // 测试 2错误穿透 MiniPromise.reject(出错了) .then(v v) // 跳过 .then(v v) // 跳过 .catch(err console.log(捕获:, err)); // 捕获: 出错了 // 测试 3Promise.all MiniPromise.all([ MiniPromise.resolve(1), new MiniPromise(r setTimeout(() r(2), 50)), MiniPromise.resolve(3) ]).then(results { console.log(results); // [1, 2, 3] }); // 测试 4嵌套 Promise递归解析 new MiniPromise(resolve { resolve(new MiniPromise(r setTimeout(() r(深层值), 100))); }).then(v console.log(v)); // 深层值 // 测试 5事件循环验证 console.log(同步 1); new MiniPromise(r { console.log(同步 2); r(); }).then(() console.log(微任务)); console.log(同步 3); // 输出顺序同步 1 → 同步 2 → 同步 3 → 微任务回到最初的问题现在你可以回答开头的三个问题了Q1为什么 new Promise 里的代码立即执行但 .then() 的回调不是因为 executor(resolve, reject) 在构造函数中是同步调用的而 .then() 的回调被包裹在 queueMicrotask() 中作为微任务异步调度。Q2为什么 .then() 能链式调用并穿透返回值每次 .then() 都返回一个新的 Promise。回调的返回值会通过 resolvePromise 递归解析如果是普通值就 resolve如果是 Promise 就等它决议。穿透 是参数默认函数 value value 的功劳。Q3Promise 微任务在事件循环中的位置微任务队列在当前宏任务执行完毕后、下一个宏任务开始前清空。所以 .then() 回调总是在 setTimeout 之前执行。更进一步async/await 只是 Promise 的语法糖当你理解了 Promise 的内部机制async/await 就不再神秘// async/await async function fetchUser() { const res await fetch(/api/user); const data await res.json(); return data; } // 等价的 Promise 写法 function fetchUser() { return fetch(/api/user).then(res res.json()); }async 函数本质上是一个返回 Promise 的函数await 就是 .then() 的语法糖 —— 它把后续的代码放进了 .then() 的回调里。JavaScript 引擎帮你做了这个 编译 工作。总结我们用大约 150 行代码实现了一个基本完整的 Promise。回顾核心设计设计要素实现方式状态机PENDING → FULFILLED / REJECTED单向不可逆异步执行queueMicrotask 将回调放入微任务队列链式调用每次 .then() 返回新 Promise递归解析resolvePromise 处理返回值是 Promise 的情况错误穿透默认回调 reason { throw reason }兼容性thenable 协议检查 .then 方法读源码最好的方式不是去看别人的分析文章而是自己写一遍。 当你亲手处理了每一个边界情况那些曾经让你困惑的异步行为都会变成理所当然。

相关新闻

Detectron2 实例分割训练实战:COCO 数据注册、训练与评估

Detectron2 实例分割训练实战:COCO 数据注册、训练与评估

Detectron2 实例分割训练实战:COCO 数据注册、训练与评估 这篇教程根据我复现 Detectron2 实例分割流程时整理,重点演示如何安装 Detectron2、注册 COCO 格式数据集、训练 Mask R-CNN 并评估结果。 本文整理自我的学习和项目复现过程,尽量按…

2026/7/20 19:01:05 阅读更多 →
AI Agent 的终极形态会是什么

AI Agent 的终极形态会是什么

开篇一个残酷的事实我们今天讨论的 AI Agent,大概率不是它最终的样子。就像 2007 年 iPhone 发布时,没有人能预测到 2026 年手机会变成什么形态。我们现在看到的 Agent——Cursor 写代码、Claude 回答问题、AutoGPT 跑任务——都只是演进过程中的中间态。…

2026/7/22 6:03:00 阅读更多 →
AI行业洞察—从1700个岗位看大厂AI要什么人

AI行业洞察—从1700个岗位看大厂AI要什么人

一、摘要 我对AI行业内的岗位需求情况进行了调研(详见文章第四部分),核心结论如下: 1、26年的大厂AI人才招聘已经不是“算法科学家”的独角戏,AI应用在产运、工程、算法岗位方面都有增量岗位,招聘要求是围…

2026/7/22 6:03:47 阅读更多 →

最新新闻

C++大型项目头文件管理:从万能头文件到精准依赖的实战优化

C++大型项目头文件管理:从万能头文件到精准依赖的实战优化

1. 项目概述:万能头文件,是“神兵利器”还是“甜蜜陷阱”? 在任何一个有一定规模的C项目里,尤其是那些动辄几十万、上百万行代码的工程,头文件的管理和编译速度绝对是让开发者又爱又恨的话题。你肯定经历过这样的场景&…

2026/7/22 6:21:10 阅读更多 →
2026年桌面音频技术变革与8项效率提升方案

2026年桌面音频技术变革与8项效率提升方案

1. 2026年桌面音频技术的变革背景过去三年里,桌面音频技术正在经历从"够用就好"到"专业级平民化"的转型。我最近为三个音乐工作室升级设备时发现,传统需要数万元专业声卡才能实现的功能,现在用2000元左右的消费级设备配合…

2026/7/22 6:21:10 阅读更多 →
“智慧家园”环保宣传微信小程序

“智慧家园”环保宣传微信小程序

智慧家园环保宣传微信小程序的选题背景随着城市化进程加快和人口增长,环境问题日益严峻,包括空气污染、水资源短缺、垃圾围城等。公众环保意识虽有提升,但缺乏便捷的参与渠道和系统的知识获取方式。微信小程序凭借其轻量化、高普及率和强社交…

2026/7/22 6:21:10 阅读更多 →
嵌入式MPU内存保护单元:原理、配置与故障排查实战

嵌入式MPU内存保护单元:原理、配置与故障排查实战

1. MPU在嵌入式系统中的核心价值与设计哲学在嵌入式系统开发,尤其是汽车电子、工业控制这类对可靠性要求极高的领域,系统崩溃往往不是由复杂的算法错误直接导致,而是源于一些看似不起眼的内存访问越界。一个失控的指针、一个错误的任务栈溢出…

2026/7/22 6:21:10 阅读更多 →
C++实战:从零实现Windows桌面截图工具,掌握GDI核心API与位图处理

C++实战:从零实现Windows桌面截图工具,掌握GDI核心API与位图处理

1. 项目概述:为什么从桌面抓图开始学C?如果你刚开始接触C,或者已经学了一段时间语法,正愁找不到一个能串联起多个知识点的实战项目,那么“桌面抓图”绝对是一个绝佳的起点。这听起来像是一个简单的功能——不就是截个屏…

2026/7/22 6:21:10 阅读更多 →
eHRPWM高精度脉宽调制与ePWM同步机制在数字电源与电机控制中的应用

eHRPWM高精度脉宽调制与ePWM同步机制在数字电源与电机控制中的应用

1. 从“粗调”到“精雕”:为什么我们需要高精度PWM?在数字电源和电机驱动的世界里,PWM(脉宽调制)就是那个“开关指挥官”。它的任务很简单:生成一个方波信号,通过控制高电平(导通&am…

2026/7/22 6:20:09 阅读更多 →

日新闻

TI DSP系统配置模块SYSCFG详解:中断机制与主设备优先级配置实战

TI DSP系统配置模块SYSCFG详解:中断机制与主设备优先级配置实战

1. 项目概述与SYSCFG模块的核心价值在嵌入式系统,尤其是像TI C6000系列这样的高性能DSP开发中,我们常常会与芯片手册里那些密密麻麻的寄存器打交道。很多开发者可能更关注算法实现、内存优化或者外设驱动,但对于一个稳定、高效的系统而言&…

2026/7/22 0:00:26 阅读更多 →
微信Server酱:高到达率的应急通知方案实践

微信Server酱:高到达率的应急通知方案实践

1. 为什么我们需要"最次"的通知方案? 在数字化协作环境中,消息通知系统的重要性不言而喻明。但现实情况是,企业级通知方案往往需要复杂的API对接(如企业微信、钉钉、飞书),个人开发者的小项目又经…

2026/7/22 0:00:26 阅读更多 →
甲方要的“简洁“PPT,到底是简洁还是省事?

甲方要的“简洁“PPT,到底是简洁还是省事?

甲方说"简洁一点",乙方听到的是"少做几页"。甲方说"不要太复杂",乙方理解成"别放图表了"。结果交过去,甲方说"我说的简洁不是这个意思"。"简洁"这个词在PPT语境里,是…

2026/7/22 0:00:26 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/21 8:48:31 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/21 5:34:47 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/21 8:25:39 阅读更多 →

月新闻