阅读视图

发现新文章,点击刷新页面。

手写Promise,从测试用例的角度理解

最近在补基础,发现Promise里面有挺多东西需要理解的,函数绕来绕去的

先来一个都可看懂的代码框架,剩余的慢慢补充

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class MyPromise {
  constructor(executor) {
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;

    const resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onFulfilledCallbacks.forEach((fn) => fn());
      }
    };
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach((fn) => fn());
      }
    };
    executor(resolve, reject);
  }
 }

首先补充then方法,由于需要链式调用,所以返回的同样是Promise对象

then(onFulfilled, onRejected) {

    const promise2 = new MyPromise((resolve, reject) => {
      const handleCallback = (callback, value, resolve, reject) => {
        queueMicrotask(() => {
          try {
            const x = callback(value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      };

      if (this.status === FULFILLED) {
        handleCallback(onFulfilled, this.value, resolve, reject);
      } else if (this.status === REJECTED) {
        handleCallback(onRejected, this.reason, resolve, reject);
      } else if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(() =>
          handleCallback(onFulfilled, this.value, resolve, reject),
        );
        this.onRejectedCallbacks.push(() =>
          handleCallback(onRejected, this.reason, resolve, reject),
        );
      }
    });

    return promise2;
  }

handleCallback是一个工具函数,用于将用于传进来的函数进行包装,这里在then中的回调加了queueMicrotask包装了下,使它变成一个异步的任务,为什么呢 考虑两种情况

情况1: new MyPromise中立刻resolve,也就是同步的情况

const myPromise = new MyPromise((resolve, reject) => {
  console.log("状态pending")
  resolve('成功调用resolve')
})

myPromise.then(res => {
  console.log(res);
}, err => {
  console.log(err);
})

对于以上例子,resolve中会立刻执行回调队列中的函数,但是实例对象的then方法这个时候还没调用呢,里面是空的。

然后执行then,这个时候状态已经确定,立即执行handleCallback。

情况2:

const myPromise = new MyPromise((resolve, reject) => {
  console.log("状态pending")
  setTimeout(() => { resolve("成功"); // 【异步执行】主线程空闲后才调用 resolve }, 0)
})

myPromise.then(res => {
  console.log(res);
}, err => {
  console.log(err);
})

这个时候,resolve没有立即调用,因此会先调用then,将任务放到队列里,等待resolve后执行handleCallback。

queueMicrotask保证了用户的回调不会阻塞同步代码的执行。

then中还有一个情况,就是then中什么也没有传,最后值还需要默认传递

const promise = new MyPromise((resolve, reject) => {
  resolve("success");
});
promise
  .then()
  .then()
  .then()
  .then(
    (value) => console.log(value),
    (err) => console.log(err)
  );

只需要加一个默认回调即可

onFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (value) => value;
onRejected =
  typeof onRejected === "function"
    ? onRejected
    : (reason) => {
        throw reason;
      };

分析一下调用,一旦顶部的promise的resolve调用,不管是resolve同步还是异步的被调用了,都会导致handleCallback被调用,最终onFulfilled被调用,并将值返回。

对于上面的例子, 一共有p p1 p2 p3 p4

P的resolve后,p1是一个新的promise,内部调用(value) => value,resolve后返回值为value; p2进来后发现p1是resolve(value)的敲定状态,也调用(value) => value,将值传递下去。

还剩下一个核心函数resolvePromise,用来处理返回值不是普通值的情况。

用例1:

// 使用 thenable 对象
const myThenable = {
  then: function (resolve, reject) {
    setTimeout(() => {
      resolve("success myThenable");
      reject("fail myThenable");
    }, 1000);
  },
};
new MyPromise((resolve, reject) => {
  resolve("success");
})
  .then((value) => {
    console.log(value);
    return myThenable;
  })
  .then((value) => console.log(value));

这个用例中返回了一个对象,该对象有then方法,

用例2:

// 使用 thenable 对象
new MyPromise((resolve, reject) => {
  resolve("success");
})
  .then((value) => {
    console.log(value);
    return new MyPromise((resolve) => setTimeout(() => resolve(num + 5), 1000));;
  })
  .then((value) => console.log(value));

这个用例返回了一个Promise对象。

当然还有更复杂的,返回嵌套的情况,可以使用递归解决,直到遇到一个普通值再结束。

上面两个用例有点难以理解,先来看普通的Promise对象:

new Promise((resolve, reject) => {
  // 这个函数就是 executor,它会被立即同步执行
  setTimeout(() => {
    resolve('done'); // 调用 resolve 改变 Promise 状态
  }, 1000);
});
  • 作用executor 负责启动异步操作,并在操作完成时调用 resolve 或 reject 来改变 Promise 的状态。
  • 特点executor 是立即执行的,并且由 Promise 构造函数传入 resolve 和 reject 两个函数。

再来看then方法:

promise.then(
  value => console.log(value), // 成功回调
  error => console.error(error) // 失败回调
);
  • 作用:注册当 Promise 状态变为 fulfilled 或 rejected 时执行的回调。
  • 特点then 方法不会主动调用 resolve 或 reject,它只是注册监听。它返回一个新的 Promise,用于链式调用。

Thenable 对象中的 then 方法

const myThenable = {
  then: function (resolve, reject) {
    // 这个 then 方法类似于 executor
    setTimeout(() => {
      resolve("success myThenable"); // 主动调用 resolve
      reject("fail myThenable");      // 也可以调用 reject
    }, 1000);
  },
};
  • 作用:当 Promise 机制(例如 Promise.resolve(myThenable))遇到 thenable 对象时,会自动调用其 then 方法,并传入两个回调(resolvePromise 和 rejectPromise 的包装函数)。thenable 内部的 then 方法可以像 executor 一样启动异步操作,并在适当时候调用传入的 resolve 或 reject 来通知结果。

  • 特点

    • 这个 then 方法承担了启动异步操作并触发状态改变的责任,与 Promise 构造函数的 executor 角色一致。
    • 它和 Promise 的 then 方法名称相同,但语义完全不同:前者是操作发起者,后者是结果监听者

其实写法上也可以看出来,自定义对象的then方法,相当于构造方法了,也是立即执行的,因为两者都叫 then,而且在 ES6 之前,许多 Promise 库(如 Q、Bluebird)的 thenable 对象确实用 then 方法来包装异步操作。但在原生 Promise 中,这两个角色被清晰地分开:

  • 构造函数中的 executor:启动操作 + 触发完成。
  • 原型上的 then 方法:注册回调 + 返回新 Promise。

而 thenable 将“启动操作”和“接收回调”合并到了同一个 then 方法中。当 Promise 处理 thenable 时,它相当于把 thenable 的 then 方法当作一个 executor 来使用,传入的 resolve 和 reject 就是用来改变最终 Promise 状态的函数。

原生 Promise 流程

  1. new Promise(executor) → executor 立即执行,启动异步任务。
  2. 异步任务完成 → 调用 resolve(或 reject)→ Promise 状态改变。
  3. 后续调用 then 注册回调 → 回调会在状态改变后被调用。

Thenable 被 Promise 处理时的流程

  1. Promise.resolve(myThenable) → 检测到 thenable。
  2. Promise 内部调用 myThenable.then(onFulfilled, onRejected),其中 onFulfilled 和 onRejected 是 Promise 提供的包装函数。
  3. myThenable.then 方法内部可以启动异步操作,并在适当时调用 onFulfilled(即传入的 resolve 函数)或 onRejected(即传入的 reject 函数)。
  4. 调用 onFulfilled 或 onRejected 会最终改变由 Promise.resolve 返回的那个 Promise 的状态。

所以,myThenable.then 相当于 Promise 构造函数的 executor,而 Promise.resolve(myThenable) 相当于 new Promise(executor)

根据上面的分析,可以来写一下resolvePromise这个函数了,首先是MYPromise 实例:

if (x instanceof MYPromise) {
    // 根据 x 的状态调用 resolve 或 reject
    x.then(
      y => {
        resolvePromise(promise2, y, resolve, reject);
      },
      reason => {
        reject(reason);
      }
    );
  }

递归调用返回值的then方法,直到返回值是一个普通值,我们再resolve掉。

对于myThenable

    // 获取 x 的 then 方法
      const then = x.then;
      if (typeof then === 'function') { // 如果 then 是函数
        // 使用 x 作为上下文调用 then 方法
        then.call(
          x,
          y => { // 成功回调
            if (called) return; // 如果已经调用过,直接返回
            called = true;
            // 递归处理 y
            resolvePromise(promise2, y, resolve, reject);
          },
          reason => { // 失败回调
            if (called) return; // 如果已经调用过,直接返回
            called = true;
            reject(reason);
          }
        );
      }

有几个需要注意的点,第一这里也是调用了then方法,并且写法上和原生的有点类似,都是传入了一个回调。为什么呢,上面说了myThenable的then有点像构造方法,接收的是resolve,Promise的then接收了onFulfilled回调,对于这俩回调,resolvePromise 在处理的时候都调用了resolvePromise。

resolve 与 onFulfilled 的区别

角色 来源 作用 被谁调用
resolve Promise 构造函数(executor 的第一个参数) 将 Promise 状态从 pending 变为 fulfilled,并设置内部 value。 由用户(或异步任务完成时)主动调用。
onFulfilled then 方法的第一个参数 当 Promise 变为 fulfilled 时被自动调用,接收该 Promise 的 value 作为参数,用于处理结果。 由 Promise 内部机制在状态变更后调用。

简言之,resolve 是“写”操作(触发状态变更),onFulfilled 是“读”操作(响应状态变更)

当 Promise 引擎遇到一个 thenable 对象(如 myThenable)时,它会调用该对象的 then 方法,并传入两个包装函数:

then.call(x,
  (y) => { /* 类似 resolve 的角色 */ },
  (r) => { /* 类似 reject 的角色 */ }
);

这个第一个包装函数(通常记为 resolvePromise 的包装)确实在语义上类似于 executor 中的 resolve——它被 thenable 内部的异步操作调用,用来传递成功值。但区别在于:

  • 并不直接改变最终 Promise 的状态,而是先经过 resolvePromise 的递归解析,最终才可能调用最外层的 resolve
  • 它的任务是接收 thenable 产生的成功值 y,然后启动递归解析过程。

所以,在 thenable 处理中,我们传入的成功回调模拟了 resolve 的行为,但实际上是解析流程的起点

resolve拿到值,处理then中回调,该回调返回不是普通值,递归处理该值。

变成了,resolve拿到值,该值不是普通值,递归处理该值。

❌