实现一个最简单的Promise

中文: 实现一个最基础的 Promise,只支持:

English: Implement a minimal Promise that supports:


分析:

要结合Promise的定义、作用、用法,还有class的用法来理解。参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

1、定义:Promise是JS中用于处理异步操作的一种对象。可以使用prototype的方式来实现,但语法太琐碎,所以最好使用class来实现,它语法直观、可读性强。

2、Promise的构造函数怎么写?还是要参考文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise

image-20260410104106045

这是最难理解的部分,要根据Promise的用法来做。

仔细看executor的定义。①它是一个函数。②它接收两个参数,resolveFunc和rejectFunc。③它返回的是一个promise对象,这个现在不用管,只需要返回一个函数或者reject(err)即可。

疑问:

① 既然 new Promise(executor)executor 函数接收 (resolve, reject) 参数,为什么resolveFunc和rejectFunc需要在constructor里面来直接实现?为什么不是 executor 函数接收 (resolve, reject) 参数传递过来的?

主要原因有两点:

1、它们是 Promise 内部状态的“钥匙”

resolvereject 的唯一任务就是:修改这个 Promise 实例的内部状态(从 pending 变为 fulfilledrejected)。

2、控制权的安全移交(Privilege Design)

Promise 设计遵循一种“特权模式”:只有创建 Promise 的人,才有权决定它什么时候成功或失败。

如果你尝试从外部传入这两个函数,代码会变得逻辑混乱。比如:

你会发现 myResolve 根本不知道它要“解决”哪一个 Promise 实例。

总结

constructor 内部实现这两个函数,本质上是 Promise 在向你发出邀请:

“嘿,我为你这个实例专门定制了两个改状态的工具,我现在通过 executor 的参数交给你。只有你能用它们来告诉我任务结束了,别人都改不了我。”

这就是为什么我们在实现 MyPromise 时,必须在 constructor 内部手动写好 const resolve = ...,然后把这个定义好的函数引用传给 executor

理解了这个设计,你也就理解了 Promise 是如何保证状态安全和不可逆的。

最难理解的就是:

一般我们在new一个实例的时候,会传实参进去,比如说new Animal("Sheep"),传递的是实参。如果需要传递一个函数进去呢?像这样:new MyPromise(() => { ... }),我们也不难理解。

难就难在,new Promise传递的参数executor,要写成这样:

这里的两个形参resolve和reject,让人很难理解。关键是这两个形参也是函数,而且是不需要我们实现的函数(在Promise类中就已经实现了)。

按理说,传入函数的函数参数,我们应该知道是起什么作用的,才比较好理解,但是这里确实很抽象。

 

② contructor里面很抽象,用到了哪些js技术?

1、闭包 (Closure) —— 最关键的基石

哪里用了:constructor 内部定义的 const resolveconst reject 为什么难懂: 这两个函数在 constructor 执行完之后,依然能“记住”并操作 this.statethis.value 等变量。 通俗解释: 这就像你在家里(constructor)留了两个遥控器给客人(executor)。即便你离开了家,客人按下遥控器,依然能控制你家里的电视机。

2、箭头函数 (Arrow Functions) 与 this 绑定

哪里用了: const resolve = (value) => { ... } 为什么难懂: 如果你用普通的 function 定义,当它在 executor 里被调用时,this 可能会指向全局对象或 undefined 通俗解释: 箭头函数能自动捕捉定义时所在的 this。它确保了无论 resolve 被丢到哪里执行,它改的一定是当前这个 Promise 实例的状态。

3、控制权反转 (Inversion of Control)

哪里用了: executor(resolve, reject) 为什么难懂: 通常是我们调用系统的函数,但这里是 Promise 调用你传入的函数 通俗解释: 这叫“不要打电话给我们,我们会给你打电话”。Promise 准备好了工具(resolve/reject),然后通过回调把工具递到你手里,让你在自己的逻辑里决定什么时候用。

4、状态机模型 (State Machine)

哪里用了: this.state = "pending" 以及改变状态时的 if 判断。 为什么难懂: 因为它要求逻辑是“原子化”的。 通俗解释: Promise 像是一个有三种颜色的灯。你的逻辑确保了灯只能从“灰色(pending)”变到“绿色(fulfilled)”或“红色(rejected)”,且一旦变色,就再也按不动开关了。

5、异常处理 (Try...Catch)

哪里用了: try { executor(...) } catch (err) { reject(err) } 为什么难懂: 它捕捉的是外部传入的代码。 通俗解释: 这是一个“安全气囊”。万一用户在 executor 里的代码写错了(比如变量名写错),MyPromise 不会直接让整个程序崩溃,而是优雅地自动切换到“失败状态”。

 

③ executor的作用是什么?(这个要重点理解)

可以结合实际使用来理解,最经典的就是获取api接口数据之后做下一步的动作。

这个例子可以很好的说明,Promise能够让你不用关心任务什么时候结束(因为用到Promise的地方,可能都会有异步操作),在new的时候告诉它执行什么任务,之后只需要通过 then 告诉它“之前的任务结束之后该干嘛”。

 

代码如下:

下面是对代码的解释:

  1. Promise 的定义:它是一个“状态机”

在你的代码里,class MyPromise 就是在定义这个状态机。

  1. 什么是 Executor(执行器)?

executor 是连接“Promise 内部逻辑”和“外部业务逻辑”的桥梁。

 

  1. 用法:如何使用这个最简版?

假设我们要模拟一个同步的业务逻辑(因为你这个版本暂不支持异步):

  1. 结合你的代码看流程

  2. 初始化:当你 new MyPromise 时,构造函数运行,状态设为 pending

  3. 执行任务:构造函数里的 executor 立即执行。

  4. 状态变更

    • executor 里你调用了 resolve("数据")
    • 这会触发你代码里定义的 const resolve = (value) => { ... }
    • 关键点:此时 this.statepending 变成了 fulfilledthis.value 存下了 "数据"
  5. 处理结果

    • 当你调用 .then() 时,then 内部会判断 if (this.state === "fulfilled")
    • 因为状态已经变了,它会立刻执行你传给 then 的第一个参数(函数),并把 this.value 传进去。

总结

3、.then()方法最多接收两个参数。第一个参数是 Promise 兑现时的回调函数,第二个参数是 Promise 拒绝时的回调函数。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/then

image-20260410104549746

image-20260410104614907

但是在这个例子中,我们返回的不是一个Promise对象,而是直接执行then,所以里面只需要根据state状态,来判断是执行onFulfilled还是onRejected函数,并传递相应的参数即可。


测试:

针对你实现的那个最简版 MyPromise,我们可以通过以下测试案例来验证它的核心逻辑(同步执行、状态锁定、异常捕获)。

你可以将这些代码直接粘贴到你的 MyPromise 类下方运行:

  1. 测试案例:正常同步调用(验证基本流程)

这是最基础的场景,验证 resolve 是否能正常改变状态并触发 then

image-20260410140251551

  1. 测试案例:状态不可逆性(验证状态锁定)

Promise 的规范要求状态一旦改变就不能再变。这个案例验证你的 if (this.state === "pending") 判断是否生效。

image-20260410140341155

  1. 测试案例:异常捕获(验证 try...catch)

验证当 executor 内部发生代码错误时,Promise 是否能自动转为 rejected 状态。

image-20260410140403640

  1. 异步测试

为了让你看清这个版本的局限性,你可以运行下面这个案例。你会发现控制台什么都不会输出

总结

你的最简版代码已经成功实现了:

 

在上面的Promise基础上实现异步

从上面的测试4可以看出,如果任务是异步的,then执行的时候状态还是pending,现有的then函数里面没有对pending进行处理。该怎么处理呢?

Question(核心)就是:如果 state 是 pending,then 应该做什么?

中文: 如果 state 是 pending,then 应该把回调保存起来,等待之后执行

English: If the state is pending, then should store the callbacks and execute them later.

中文: Promise 本质是一个“发布-订阅模型”,then 是订阅,resolve/reject 是发布。

English: Promise is essentially a publish-subscribe model: then subscribes, and resolve/reject publish.


新增两个队列:

在then中,处理state为“pending”时的逻辑,也就是store the callbacks。

修改resolve和reject,当异步任务完成后,执行resolve或者reject的时候,将队列里面的callbacks取出来执行。

完整代码:


测试:

image-20260410145034988

注意执行流程:

executor里面先执行setTimeout,state没有变化;然后执行then,走then里面的state === 'pending'这段代码,然后将data => console.log(data)这段代码放到onFulfilledCallbacks中;等到1s之后,执行resolve()函数,从onFulfilledCallbacks中取出全部函数来执行。

 

实现 then 链式调用 + 返回新 Promise

要求:


then 必须返回一个新的 Promise,是为了实现:

  1. 链式调用(chainability)

每次 then 都返回一个新的 Promise,才能继续调用 then

  1. 值的传递(value propagation)

上一个 then 的返回值,会作为下一个 then 的输入

  1. 状态隔离(state isolation)

每个 then 都是一个独立的 Promise,互不影响

  1. 错误传播(error propagation)

错误可以沿着链一直传递到 catch

then 返回新的 Promise,是为了把“当前回调的执行结果”封装成一个新的异步任务,从而实现链式调用和状态传递。

 

Standard Answer (English)

then must return a new Promise to enable:

  1. Chainability
  2. Value propagation
  3. State isolation
  4. Error propagation

then returns a new Promise to wrap the result of the current callback into a new asynchronous task, enabling chaining and value propagation.


1、then的返回值必须是一个promise

2、需要处理onFulfilled、onRejected函数执行后的返回值。

接收返回值:

返回值可能有三种类型:

3、使用instanceof来判断某个对象是否属于Promise的实例

4、如果new Promise()里面的任务是异步任务,当then执行之后,不能简单的将onFulfilled、onRejected方法推送到队列中去,应该对它们进行同样的处理,然后这个then后面的.then().then()能够得到链式调用。

5、疑问:

为什么this.state === 'rejected'了,不直接onRejected(this.reason)执行呢?还要像fulfilled那样继续进行处理呢?

我被rejected的拒绝,这个翻译误导了。这个rejected并不等于是错误,而是一种状态。比如说一个任务,如果做成了接下来就执行一种方案,如果没有做成接下来就执行另外的方案,都是可以继续下去的,而且后面还会有两种方案等着,所以肯定是要继续处理的。 这和错误是完全不同的,要理清楚。

为了避免错误时程序停止执行,那么就需要使用try...catch来捕获错误,供接下来处理。

 

代码如下:


上面的代码实现有重复的部分,可以将try...catch部分封装为一个函数,然后调用即可:


测试:

  1. 测试同步链式调用(值传递)

验证 .then() 返回普通值时,能否正确传给下一个 .then()

image-20260410180116758

  1. 测试异步链式调用(Promise 嵌套拍平)

这是 Promise 最强大的地方:如果第一个 then 返回一个 新的 Promise,第二个 then 是否会等待它完成?

image-20260410180129276

这里不是同步、异步的问题,而是状态确定了之后,才能进行下一步。

  1. 测试错误冒泡(Error Propagation)

验证中间步骤报错,是否能被最后的逻辑捕获,或者传给下一个 reject

image-20260410180252955

  1. 测试同一个 Promise 挂载多个 then

验证你的 onFulfilledCallbacks 数组是否起作用(多个订阅者是否都能收到通知)。

image-20260410180331613

 

提问:为什么 Promise 要设计成:微任务(microtask)执行 then 回调?

这个问题本质在考:

👉 你能不能解释“代码执行顺序”

 

答案:

中文(面试版本)

Promise 的 then 回调被设计成微任务,是为了保证:

  1. 执行顺序更可预测
  2. 优先级高于宏任务(如 setTimeout)
  3. 保证异步操作尽快执行
  4. 避免回调被延迟到下一轮事件循环

English

then callbacks are executed as microtasks to ensure:

  1. Predictable execution order
  2. Higher priority than macrotasks
  3. Faster async execution
  4. No unnecessary delay to the next event loop

更加深入一点:

假设promise是宏任务:

promise的执行顺序就不稳定了。

 

Event Loop 执行顺序:

  1. 执行同步代码
  2. 执行所有微任务(eg: Promise),即使微任务里再产生微任务,也要继续执行
  3. 执行一个宏任务(eg: setTimeout)
  4. 再执行所有微任务
  5. 再依据1-4的规则循环

 

实现“微任务”异步调用

 

面试官要求:


分析:

现状问题 (The Issue): 在你目前的代码中,当状态是 fulfilled 时,onFulfilled(this.value)立即同步执行的。

 

改进思路 (The Solution): 我们需要把这行代码包在一个“异步容器”里。

  1. 方式 A (现代标准): 使用 queueMicrotask(() => { ... })。这是原生 Promise 使用的微任务 API。
  2. 方式 B (兼容方案,这个可以作为练习使用,但基本上不要考虑): 使用 setTimeout(() => { ... }, 0)。虽然它是宏任务,但在手写练习中经常作为降级处理方案。

思路其实很简单了,就是then里面的执行语句,也就是handle函数,要使用queueMicrotask这个api来包裹,就可以创建微任务了。

其余代码都不用改变。


测试用例 (Test Cases)

 

测试 A:验证执行顺序(面试核心)

目的:验证 Promise 的回调是否在同步代码之后运行。

image-20260418191243835

如果没有实现微任务调用,那么执行顺序就是这样的:

image-20260512162314808

测试 B:验证微任务优先级

目的:验证 Promise (微任务) 是否比 setTimeout (宏任务) 先执行。

image-20260418191330416

测试 C:验证异步链式调用

目的:确保加入微任务逻辑后,异步的 then 链条依然工作正常。

image-20260418191446721

 

值穿透处理(Value Propagation)

面试官的要求 (Interviewer's Requirements)

面试官会考察你的代码在“非正常传参”情况下的健壮性:


分析:

then方法里面有两个参数,一个是onFulfilled,一个是onRejected,都必须是函数才行。

我们应该对两个参数进行判断。then()或者then(1, "failed to post")这类方式的调用,就是非正常传参。then(resolve)或者then(resolve, "hello")就表示onRejected参数非正常的情况;then(null, onRejected)或者then(1, onRejected)就表示onFulfilled参数非正常的情况。这些情况都有可能遇到,那么应该怎么处理呢?

then 方法的最开始,检查 onFulfilledonRejected 是否为函数。如果不是,给它们赋值一个“默认转发”函数。


分析明白了,其实代码就很好改了,也就是从最开始就判断 onFulfilledonRejected 是否为函数,如果不是,就做相应的处理。其余部分都不用改。


测试 1:成功值的连续穿透

目的:验证当连续多个 .then() 都不传参数时,初始的 resolve 值能否安全到达最后一个 .then()

image-20260418193324522

测试 2:失败原因的连续穿透(错误冒泡)

目的:验证错误是否能跳过中间没有定义 onRejectedthen,直到被捕获。

image-20260418193358573

测试 3:穿透后的链式返回

目的:验证穿透发生后,后续的 then 是否依然能正常通过 return 改变 Promise 的值。

image-20260418193427440

测试 4:异步情况下的穿透

目的:验证在 pending 状态下(异步),值穿透逻辑是否依然有效。

image-20260418193456120

 

 

解决循环引用与 Thenable 兼容:Handle Self-Reference and Thenables(了解即可)

面试官的要求 (Interviewer's Requirements)

面试官会观察你如何处理返回值,特别是边界情况:


分析:

问题所在 (The Issue): 在你之前的代码里,我们直接判断了 result instanceof MyPromise。但有两个缺陷:

  1. 它没法判断 result 是否就是当前正在创建的那个 promise2
  2. 它没法识别 axios 或原生 Promise 返回的对象(因为它们不是 MyPromise 的实例)。

解决办法 (The Solution): 我们需要抽象出一个名为 resolvePromise 的辅助函数,专门负责“拆解”返回值。


要求1很好理解,如果没有办法理解要求2,就暂时不看吧,不过要求2仔细看一下,其实也不难理解。

然后在then方法里面,先拿到new Promise返回的对象,然后使用resolvePromise方法来进行处理。

resolvePromise里面递归的作用:

  1. 用户体验:保证 .then(res => ...) 里的 res 永远是我们要的业务数据,而不是另一个 Promise 实例。如果不递归:当你解析完第一层,发现结果还是一个 Promise,你就直接把它传给下一个 then 了。用户拿到的就是一堆 Promise 对象,还得手动再写一个 .then() 才能拿到值。resolvePromise递归就是为了帮助我们拿到的是值。
  2. 状态同步:让外层 Promise 的状态“追踪”内层 Promise 的状态。内层不成功,外层就不结算。

测试用例 (Test Cases)

测试 1:自引用检测

image-20260418203610527

测试 2:兼容原生 Promise(Thenable)

image-20260418203642672

 

99%的Promise代码,供参考