手写 Promise:从“我会用”到“我会造”

用了这么久Promise,你知道它内部是怎么工作的吗?今天我们就来手写一个符合Promise/A+规范的Promise,从零开始造轮子。别怕,这不是为了炫技,而是为了让你真正理解异步编程的底层逻辑。造完这个轮子,你会发现:原来Promise也就这么回事!

前言

你有没有想过,Promise这个"承诺"到底是怎么实现的?为什么它能链式调用?为什么错误会冒泡?为什么then里的回调可以返回普通值,也可以返回Promise?

就像开惯了自动挡的人,突然想知道发动机是怎么工作的。今天我们就来拆解Promise,然后亲手组装一个。放心,我们不会从零写一个工业级的库,而是聚焦核心逻辑,让你一眼看穿它的本质。

一、Promise/A+规范:行业标准

Promise有很多实现,但它们都遵循一个共同的标准------Promise/A+。这个规范定义了Promise的行为,比如状态、then方法等。我们手写的Promise只要符合这个规范,就能和原生Promise无缝衔接。

核心要点:

  • Promise有三种状态:pending(等待)、fulfilled(成功)、rejected(失败)
  • 状态一旦改变,就不能再变
  • 必须有一个then方法,接受onFulfilledonRejected两个回调
  • then必须返回一个新的Promise,实现链式调用
  • 支持值的穿透(比如.then().then(value => ...)

二、搭架子:定义状态和构造函数

先搭一个最简单的架子:

js 复制代码
class MyPromise {
  constructor(executor) {
    this.state = 'pending';    // 初始状态
    this.value = undefined;    // 成功时的值
    this.reason = undefined;   // 失败时的原因
    this.onFulfilledCallbacks = []; // 存储成功的回调(因为可能有多个then)
    this.onRejectedCallbacks = [];   // 存储失败的回调

    // resolve 和 reject 函数
    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    // 执行executor,传入resolve和reject
    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
}

这里的关键:当异步操作还没完成时,可能有多个then被调用,所以要用数组把回调存起来,等状态改变时再依次执行。

三、实现then方法:核心中的核心

then是Promise的心脏,它的逻辑比较复杂:

js 复制代码
then(onFulfilled, onRejected) {
  // 参数可选,如果不是函数就忽略(值穿透)
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
  onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

  const promise2 = new MyPromise((resolve, reject) => {
    if (this.state === 'fulfilled') {
      // 用微任务包装,保证异步执行
      queueMicrotask(() => {
        try {
          const x = onFulfilled(this.value);
          resolvePromise(promise2, x, resolve, reject);
        } catch (err) {
          reject(err);
        }
      });
    } else if (this.state === 'rejected') {
      queueMicrotask(() => {
        try {
          const x = onRejected(this.reason);
          resolvePromise(promise2, x, resolve, reject);
        } catch (err) {
          reject(err);
        }
      });
    } else if (this.state === 'pending') {
      // 异步等待,先把回调存起来
      this.onFulfilledCallbacks.push(() => {
        queueMicrotask(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        });
      });
      this.onRejectedCallbacks.push(() => {
        queueMicrotask(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        });
      });
    }
  });

  return promise2;
}

注意:

  • then返回的是一个新的Promise(promise2)
  • 回调的执行要用微任务(queueMicrotask),符合规范
  • 回调可能返回普通值,也可能返回Promise,需要统一处理

四、核心辅助函数:resolvePromise

这个函数处理then回调的返回值,决定promise2的状态:

js 复制代码
function resolvePromise(promise2, x, resolve, reject) {
  // 防止循环引用
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected'));
  }

  // 如果x是Promise,就让它决定promise2的状态
  if (x instanceof MyPromise) {
    x.then(resolve, reject);
    return;
  }

  // 如果x是对象或函数,可能是thenable
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    let then;
    try {
      then = x.then;
    } catch (err) {
      return reject(err);
    }

    // 如果是thenable,就当作Promise处理
    if (typeof then === 'function') {
      let called = false;
      try {
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject);
          },
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } catch (err) {
        if (called) return;
        reject(err);
      }
    } else {
      resolve(x);
    }
  } else {
    // 普通值直接resolve
    resolve(x);
  }
}

这个函数主要做了三件事:

  1. 处理循环引用(防止自己等自己)
  2. 处理返回的Promise(直接then)
  3. 处理thenable对象(比如其他Promise库的实例)

五、静态方法:resolve、reject、all、race

有了核心,再添几个静态方法:

js 复制代码
static resolve(value) {
  if (value instanceof MyPromise) return value;
  return new MyPromise(resolve => resolve(value));
}

static reject(reason) {
  return new MyPromise((_, reject) => reject(reason));
}

static all(promises) {
  return new MyPromise((resolve, reject) => {
    const results = [];
    let count = 0;
    if (promises.length === 0) return resolve(results);
    promises.forEach((p, idx) => {
      MyPromise.resolve(p).then(
        val => {
          results[idx] = val;
          count++;
          if (count === promises.length) resolve(results);
        },
        reject
      );
    });
  });
}

static race(promises) {
  return new MyPromise((resolve, reject) => {
    promises.forEach(p => {
      MyPromise.resolve(p).then(resolve, reject);
    });
  });
}

六、完整代码与测试

把上面的片段拼起来,就是一个可用的Promise了。我们用官方的Promise/A+测试套件跑一下(需要额外安装),基本都能通过。

七、手写Promise的收获

造完这个轮子,你会发现:

  • 状态管理:pending → fulfilled/rejected,不可逆
  • 异步存储:pending时回调存在数组里,等状态改变再执行
  • 链式调用 :核心就是then返回新Promise,并且通过resolvePromise处理返回值
  • 微任务:回调必须异步执行,规范要求用微任务(原生用微任务,我们这里用queueMicrotask)
  • 错误处理:try-catch捕获同步异常,reject处理异步异常

以后再看到Promise的链式调用,你脑子里就会浮现出这些逻辑:噢,原来它只是把回调存起来,然后按顺序执行而已。

八、总结

手写Promise不是为了替代原生Promise,而是为了让你:

  • 彻底理解异步编程的核心机制
  • 不再畏惧源码阅读
  • 面试时能从容应对"手写Promise"的考题

现在,你对Promise的认知已经超越了"会用"的层面,真正进入了"懂原理"的阶段。恭喜你,又升了一级!

明天我们将继续深入异步世界,聊聊Generator函数与协程,看看它是怎么为async/await铺路的。如果你喜欢今天的手写轮子系列,点个赞让更多人看到。有疑问评论区见,我们明天见!

相关推荐
এ慕ོ冬℘゜17 小时前
手写一款高兼容、零BUG图片预览组件|前端
前端·bug
铁链鞭策大师17 小时前
javaEE之多线程(2)
java·前端·java-ee
KaMeidebaby17 小时前
卡梅德生物技术快报|生信实操:ChIP 染色质免疫共沉淀技术流程、短板与替代方案详解
前端·人工智能·物联网·百度·新浪微博
weixin1997010801617 小时前
[特殊字符] 【性能提升300%】仿1688首页的Webpack优化全记录(附构建分析Python脚本)
前端·python·webpack
Komorebi_999917 小时前
Day6:微调 vs RAG 场景区分(面试高频)
面试·职场和发展
英俊潇洒美少年17 小时前
Vue2 $set 深度解析 + 批量更新全套优化方案(原理+实战+踩坑+面试)
面试·职场和发展·wps
Xzh042317 小时前
Redis黑马点评 实战复盘与面试高频考点详解
java·数据库·redis·面试
海兰17 小时前
【文字三国志:第五篇】天命重构,游戏前端UI设计
前端·人工智能·游戏·语言模型
时寒的笔记17 小时前
LF11期_day19~20 补环境(二)入门案例
javascript
海鸥-w17 小时前
前端学习python第二天手敲笔记整理
前端·python·学习