从零手写一个 Promise:深入理解 JavaScript 异步编程的本质
2026/7/21 21:21:34 网站建设 项目流程

每个前端和 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 最核心的方法,它需要处理三种情况:

  1. 调用 .then() 时 Promise 已经 fulfilled → 直接执行 onFulfilled
  2. 调用 .then() 时 Promise 已经 rejected → 直接执行 onRejected
  3. 调用 .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; }

这里有两个精妙的设计:

  1. 每次 .then() 都返回一个新 Promise —— 这就是链式调用的秘密。
  2. 参数穿透 —— 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)); // "捕获: 出错了!" // 测试 3:Promise.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 的功劳。

Q3:Promise 微任务在事件循环中的位置?

微任务队列在当前宏任务执行完毕后、下一个宏任务开始前清空。所以 .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 方法)

读源码最好的方式,不是去看别人的分析文章,而是自己写一遍。 当你亲手处理了每一个边界情况,那些曾经让你困惑的异步行为,都会变成理所当然。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询