从零手写 Promise:状态机、链式调用与 then 的三大规范化难点
# 从零手写 Promise:状态机、链式调用与 then 的三大规范化难点
> 摘要:先讲清 Promise 的三状态不可逆模型,再逐步写出最小可用版本,深入 then 链式调用的三层返回逻辑、thenable 吸收、循环引用检测与只能决议一次三个规范化难点,最后补齐 all/race/allSettled/any 四个静态方法。
> 标签:javascript、前端
## 一、三个状态与一条铁律
Promise 只有三种状态:
- `pending`:初始态,既没成功也没失败
- `fulfilled`:成功,携带 value
- `rejected`:失败,携带 reason
铁律只有一条:**状态只能从 pending 单向流转到 fulfilled 或 rejected,且不可逆、不可二次变更**。后续所有实现细节都是为了守住这条规则。另外 Promise 构造函数里的执行器(executor)是同步执行的,只有 then 回调才是微任务——这是新手最常搞混的一点。
## 二、最小可用版本
```js
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
this.state = PENDING;
this.value = undefined;
this.handlers = []; // 暂存 pending 期间的 then 回调
const resolve = (value) => {
if (this.state !== PENDING) return;
this.state = FULFILLED;
this.value = value;
this.handlers.forEach((h) => h.onFulfilled(value));
};
const reject = (reason) => {
if (this.state !== PENDING) return;
this.state = REJECTED;
this.value = reason;
this.handlers.forEach((h) => h.onRejected(reason));
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const handle = (cb, fallback) => {
// 微任务:保证 then 回调异步执行
queueMicrotask(() => {
try {
if (typeof cb !== 'function') return fallback(this.value);
resolve(cb(this.value)); // 关键:把回调结果作为新 Promise 的值
} catch (e) {
reject(e);
}
});
};
if (this.state === FULFILLED) {
handle(onFulfilled, resolve);
} else if (this.state === REJECTED) {
handle(onRejected, reject);
} else {
this.handlers.push({
onFulfilled: () => handle(onFulfilled, resolve),
onRejected: () => handle(onRejected, reject),
});
}
});
}
}
```
两个关键设计:**then 返回新 Promise**(不是 this),所以能链式调用;回调里抛错要 `reject` 而不是冒泡出去。
## 三、then 链式调用的三种返回值
`then` 的返回值决定下一个 Promise 的状态,这是规范里最绕的部分:
1. **返回普通值**:下一个 Promise 直接 fulfilled,value 就是该值
2. **返回 Promise/thenable**:下一个 Promise 等它决议,状态和值都跟它走(称为 thenable 吸收)
3. **抛错**:下一个 Promise rejected,reason 是错误对象
另外两个必须处理的细节:
- **onFulfilled 不是函数就透传**:`.then(null)` 应该把 value 原样带下去,否则链就断了
- **回调必须异步执行**:用 queueMicrotask 或 Promise.resolve().then 包一层,否则同步执行会导致 `.then` 里的代码在 resolve 之前跑完
## 四、三个规范化难点
### 1. thenable 吸收
判断"返回的是不是 thenable"不能只 `instanceof MyPromise`,原生 Promise、第三方库的 Promise 都要能吸收:
```js
if (result && (typeof result === 'object' || typeof result === 'function')) {
const then = result.then;
if (typeof then === 'function') {
then.call(result, resolve, reject); // 用它的 then 接管
}
}
```
### 2. 循环引用检测
```js
const p = MyPromise.resolve().then(() => p); // 自己等自己,必须 reject
```
规则:如果 `then` 回调返回的就是新 Promise 本身,直接 reject 一个 TypeError。实现里保存 `promise2` 的引用,比较 `result === promise2`。
### 3. 决议只能一次
thenable 是恶意/异常对象时可能同时调 resolve 和 reject 两次,必须加锁:
```js
let called = false;
const onceResolve = (v) => { if (!called) { called = true; resolve(v); } };
const onceReject = (r) => { if (!called) { called = true; reject(r); } };
```
这三条是手写 Promise 从"能跑"到"符合规范"的分水岭,面试官基本都在这里追问。
## 五、四个静态方法
```js
// all:全部成功才成功,一个失败立即失败
MyPromise.all = (list) => new MyPromise((resolve, reject) => {
const results = new Array(list.length);
let count = 0;
if (list.length === 0) return resolve([]);
list.forEach((item, i) => {
MyPromise.resolve(item).then((v) => {
results[i] = v;
if (++count === list.length) resolve(results);
}, reject);
});
});
// race:谁先决议就用谁
MyPromise.race = (list) => new MyPromise((resolve, reject) => {
list.forEach((item) => MyPromise.resolve(item).then(resolve, reject));
});
// allSettled:等全部结束,永不 reject
// any:第一个成功就成功,全失败才 reject(AggregateError)
```
注意 all 里必须用 `MyPromise.resolve(item)` 包装,保证非 Promise 值也能处理,且结果顺序按传入顺序而非完成顺序。
## 六、实践中的三条建议
1. **优先用原生 Promise**:手写版没有 V8 的性能优化,也没有 async_hooks 支持,只适合学习
2. **别在 then 里忘 return**:`.then(() => fetchA())` 不 return,外层拿不到结果,链会提前结束
3. **finally 不收值不改变状态**:它只用于清理,返回值被忽略(除非抛错覆盖)
## 七、总结
手写 Promise 的核心就三件事:状态机守住单向不可逆、then 返回新 Promise 支撑链式、决议函数做 thenable 吸收与循环引用防护。把这三块写对,其余静态方法都是组合。
- 点赞
- 收藏
- 关注作者
评论(0)