每个前端和 Node.js 开发者每天都在用 Promise,但有多少人真正理解它内部是怎么运转的?今天,我们不复述 MDN 文档,而是从零开始,亲手实现一个符合 Promises/A+ 规范的迷你 Promise,把异步编程的本质看个通透。
为什么你要关心 Promise 的实现?
你可能觉得:"我会用 async/await,会用 .then(),够了。" 但你是否遇到过这些困惑:
- 为什么 new Promise 里的代码是立即执行的,但 .then() 的回调却不是?
- 为什么 Promise 的 .then() 能链式调用,而且能 "穿透" 返回值?
- 事件循环里,Promise 的微任务到底排在哪个位置?
这些问题的答案,全都藏在 Promise 的实现细节里。理解了它,你就不再是 API 的使用者,而是异步模型的掌控者。
第一步:Promise 的状态机模型
Promise 的本质是一个有限状态机,只有三种状态:
bash
PENDING ──resolve──→ FULFILLED
│
└──reject──→ REJECTED
一旦从 PENDING 变成 FULFILLED 或 REJECTED,就再也无法改变 ------ 这就是 "承诺" 的含义。
让我们先把骨架搭起来:
javascript
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) 是在构造函数中同步调用的。这就解释了下面这段代码的行为:
javascript
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。
javascript
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 对象。规范对此有严格定义:
javascript
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,我们只认第一个。
第四步:补全常用静态方法
有了核心骨架,静态方法就是水到渠成:
javascript
// 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 中 resultsi = value 这一行 ------ 它用索引 i 而非 push 来保证结果数组的顺序与输入一致,这是一个容易被忽略的正确性细节。
完整测试:看看我们的 Promise 能不能跑起来
javascript
// 测试 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 就不再神秘:
javascript
// 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 方法) |
读源码最好的方式,不是去看别人的分析文章,而是自己写一遍。 当你亲手处理了每一个边界情况,那些曾经让你困惑的异步行为,都会变成理所当然。