从零手写一个 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/9/23 8:11:02 阅读更多 →
AI Agent 的终极形态会是什么

AI Agent 的终极形态会是什么

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

2026/9/22 4:10:08 阅读更多 →
AI行业洞察—从1700个岗位看大厂AI要什么人

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

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

2026/9/22 9:26:52 阅读更多 →

最新新闻

如何用 Ruffle 浏览器扩展在浏览器里重新播放 Flash:新手入门指南

如何用 Ruffle 浏览器扩展在浏览器里重新播放 Flash:新手入门指南

如何用 Ruffle 浏览器扩展在浏览器里重新播放 Flash:新手入门指南 【免费下载链接】ruffle A Flash Player emulator written in Rust 项目地址: https://gitcode.com/GitHub_Trending/ru/ruffle 打开老页面只剩一块灰底,还提示“需要安装 Flash”…

2026/9/25 4:54:50 阅读更多 →
TypeDoc @include 与 @includeCode 标签实战指南:在文档注释中嵌入外部文件、代码区域与行号片段

TypeDoc @include 与 @includeCode 标签实战指南:在文档注释中嵌入外部文件、代码区域与行号片段

开发工具文档 【免费下载链接】typedoc Documentation generator for TypeScript projects. 项目地址: https://gitcode.com/gh_mirrors/ty/typedoc 点击查看 免费下载 TypeDoc 的 {include} 标签族允许你在 TSDoc 文档注释或外部 Markdown 文档中直接嵌入仓库里的…

2026/9/25 4:54:50 阅读更多 →
Miniconda vs Anaconda:虚拟环境管理与PyTorch CUDA配置实战

Miniconda vs Anaconda:虚拟环境管理与PyTorch CUDA配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 4:54:50 阅读更多 →
STM32F407移植FreeRTOS与LwIP:从CubeMX配置到TCP通信实战

STM32F407移植FreeRTOS与LwIP:从CubeMX配置到TCP通信实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 4:54:50 阅读更多 →
从AI对话Demo到可演进Agent平台:架构设计与工程实践

从AI对话Demo到可演进Agent平台:架构设计与工程实践

开篇:从 AI 对话 Demo 到可演进的 Agent 平台这两年 AI 圈最热闹的词,一个是“AI”,一个是“Agent”。市面上 Demo 满天飞,今天一个聊天机器人,明天一个自动写周报的工具,后天又冒出个能帮你订机票的智能体…

2026/9/25 4:54:50 阅读更多 →
低功耗电压检测电路设计:MOS管如何让电池多活一年

低功耗电压检测电路设计:MOS管如何让电池多活一年

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 4:53:50 阅读更多 →

日新闻

AI元人文:从工具使用到思维重构的深度探索

AI元人文:从工具使用到思维重构的深度探索

最近半年我一直在琢磨一件事:AI元人文到底是什么?说白了,就是“用元视角重新审视人与AI的关系”,也在“探索AI如何反向逼着我们发现自己的思考边界”。标题里的“元探索”,在我看就是一层套一层的追问——当你用AI解决…

2026/9/25 0:00:41 阅读更多 →
Python+CNN车牌识别实战:从数据预处理到模型训练与部署

Python+CNN车牌识别实战:从数据预处理到模型训练与部署

简介:基于Python与卷积神经网络的车牌识别项目,面向计算机视觉初学者及智能交通开发者,目标是帮助用户掌握从数据预处理、模型构建到实际部署的完整流程。压缩包共25个文件,包含jpg/png图像样本、py训练脚本、md说明文档、dat数据…

2026/9/25 0:00:41 阅读更多 →
Vim基础操作全攻略:保存退出、模式切换与高频命令实战

Vim基础操作全攻略:保存退出、模式切换与高频命令实战

1. 项目概述1.1 核心需求解析今天聊聊Vim。写这个题目的原因是:几乎每个后端开发者、运维人员、数据工程师某天都会遇到一个场景——深夜加班,服务器登录界面只有黑底白字,编辑器只有vi/vim,你必须在五分钟内完成一次配置修改并保…

2026/9/25 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →