算法 - 手写简易 Promise

1. 题目

实现一个满足以下核心特性的迷你 MyPromise

  • 三种状态:pendingfulfilledrejected,状态一经改变不再变化
  • 构造器接收 executor(resolve, reject)立即执行;executor 内抛错则转为 rejected。
  • resolve/reject 可接收值;若 resolve 一个 Promise(thenable),要等它 settle 后再决定本 Promise 状态。
  • then(onFulfilled, onRejected) 支持链式调用(返回新 Promise),且回调异步在微任务里执行;支持值穿透(回调不是函数时透传)。
  • catch 复用 then

2. 解题思路

四个关键点:

  1. 状态机:用一个 status 字段 + 只允许一次变更(用 changeStatus 守卫),保证不可逆。
  2. 异步执行回调then 的回调不能立即调用(可能在 resolve 之前就被 then),统一丢进 queueMicrotask(模拟微任务,等价 Promise.resolve().then())。
  3. 链式 & 值穿透then 永远返回一个新的 Promise;若传入的不是函数,用默认的 v => v(成功透传)和 err => throw err(失败下抛),从而支持 .then().then() 跳过。
  4. resolve thenableresolve 收到的若是带 then 方法的对象/ Promise,需递归地采用它的最终状态,不能直接 fulfilled。
  • 状态变更 O(1);链式每个 then O(1)。

3. TypeScript 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
type Status = "pending" | "fulfilled" | "rejected";

class MyPromise {
private status: Status = "pending";
private value: any = undefined;
private reason: any = undefined;

constructor(executor: (resolve: (v?: any) => void, reject: (r?: any) => void) => void) {
try {
executor(this.resolve.bind(this), this.reject.bind(this));
} catch (e) {
this.reject(e);
}
}

private resolve(value: any): void {
if (this.status !== "pending") return; // 只允许一次
// resolve 了一个 thenable -> 采用其状态
if (value && (typeof value === "object" || typeof value === "function")
&& typeof value.then === "function") {
value.then(this.resolve.bind(this), this.reject.bind(this));
return;
}
this.status = "fulfilled";
this.value = value;
}

private reject(reason: any): void {
if (this.status !== "pending") return;
this.status = "rejected";
this.reason = reason;
}

then(onFulfilled?: any, onRejected?: any): MyPromise {
// 值穿透:非函数用默认处理器
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v: any) => v;
onRejected = typeof onRejected === "function" ? onRejected : (e: any) => { throw e; };

return new MyPromise((resolve, reject) => {
const runFulfilled = () =>
queueMicrotask(() => {
try { resolve(onFulfilled(this.value)); }
catch (e) { reject(e); }
});
const runRejected = () =>
queueMicrotask(() => {
try { resolve(onRejected(this.reason)); }
catch (e) { reject(e); }
});

if (this.status === "fulfilled") {
runFulfilled();
} else if (this.status === "rejected") {
runRejected();
}
// 注意:简易版未处理 pending 时回调的收集;
// 完整版应把 runFulfilled/runRejected 存入回调数组,resolve/reject 时再依次触发。
});
}

catch(onRejected: any): MyPromise {
return this.then(undefined, onRejected);
}

static resolve(v: any): MyPromise {
if (v instanceof MyPromise) return v;
return new MyPromise((resolve) => resolve(v));
}
}

// 测试:链式 + 值穿透
new MyPromise<number>((resolve) => setTimeout(() => resolve(1), 50))
.then((v) => { console.log(v); return v + 1; }) // 1
.then(null) // 值穿透
.then((v) => console.log(v)); // 2

说明:为了讲解聚焦,上面 then 省略了「pending 状态下回调收集」的实现。完整版本必须在 Promise 尚未 settle 时把回调 push 进 onFulfilledCbs[] / onRejectedCbs[],并在 resolve/reject 里遍历触发——这是异步 executor 场景正确性的关键。

4. 面试延伸

  • 为什么用微任务而不是 setTimeout:Promise 回调属微任务,在同步代码之后、下一轮事件循环前统一执行;setTimeout 是宏任务,时序不同。能讲清宏/微任务队列即是加分。
  • then 返回新 Promise 且 resolve 回调返回值:正是这个「返回值再交给新 Promise 的 resolve」实现了链式传递和 thenable 展开。
  • resolve(new MyPromise(...)) 不会立刻定状态:因为 thenable 递归采用逻辑,能答出这点说明真懂。
  • 进阶补全:加上 pending 回调队列、静态 all/race/any/allSettled(见 [Promise.all]、[Promise.race] 两篇),即接近 A+ 规范。
  • 异常处理:回调内 throw 会被新 Promise reject 捕获,向下游 catch 冒泡,与原生一致。

难度:困难 | 手写 Promise 核心 | 异步机制试金石