为了搞懂 Promise 源码,我重写了 MiniPromise

0 阅读3分钟

前言

Promise 源码看了一百遍,不如自己写一遍。

相信很多前端同学都有过这样的经历:面试问手写 Promise,网上搜一搜 "Promise A+ 规范实现",然后对着代码 Copy 一遍,写完还是云里雾里 —— 那些 .thenPromise.resolvePromise.all 到底是怎么串起来的?

这篇文章不打算贴完整代码(GitHub 上已经够多了),而是换个方式:用一个最小、最简、最裸的 MiniPromise,带你从零理解 Promise 的设计思路


1. 先想清楚:Promise 解决什么问题?

在 Promise 出现之前,我们用回调函数来处理异步:

fetchData(function(result) {
  processResult(result, function(processed) {
    saveData(processed, function() {
      // ... 回调地狱
    });
  });
});

这叫 回调地狱(Callback Hell),问题不仅仅是嵌套难读,更重要的是 错误处理分散、状态不可控

Promise 的核心思路就两点:

  1. 状态机:pending → fulfilled / rejected,只能变一次
  2. 链式调用.then() 返回一个新的 Promise,实现"异步组合"

理解这两点,你就能自己动手写一个简化版 Promise。


2. MiniPromise 的核心结构

class MiniPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => this._resolve(value);
    const reject = (reason) => this._reject(reason);

    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
}

就这?一个类,三个属性,两个数组?

对,就是这么裸。Promise 本质就是一个状态容器,所有的魔法都在 _resolve 方法里。


3. 核心逻辑:状态流转 + 异步执行

_resolve(value) {
  // 只能从 pending 变一次
  if (this.state !== 'pending') return;

  // 如果 value 是 Promise,需要"展开"
  if (value instanceof MiniPromise) {
    return value.then(this._resolve.bind(this), this._reject.bind(this));
  }

  this.state = 'fulfilled';
  this.value = value;

  // 异步执行回调 —— 这就是 then 可以链式调用的关键
  queueMicrotask(() => {
    this.onFulfilledCallbacks.forEach(cb => cb(this.value));
  });
}

等等,这里有个关键点:为什么要用 queueMicrotask?

因为 Promise 的设计原则是:then 的回调必须异步执行。这保证了执行顺序的可预测性,也是 Promise/A+ 规范的要求。


4. then 是怎么实现的?

then(onFulfilled, onRejected) {
  // 返回一个新的 Promise,这就是链式调用的秘密
  return new MiniPromise((resolve, reject) => {
    const handleCallback = (callback, value) => {
      try {
        // 如果没有传回调,直接透传 value
        const result = callback ? callback(value) : value;
        resolve(result); // 关键:resolve 的是回调的返回值
      } catch (err) {
        reject(err);
      }
    };

    if (this.state === 'fulfilled') {
      // 异步执行,保持一致性
      queueMicrotask(() => handleCallback(onFulfilled, this.value));
    } else if (this.state === 'rejected') {
      queueMicrotask(() => handleCallback(onRejected, this.value));
    } else {
      // pending 状态,先把回调存起来
      this.onFulfilledCallbacks.push(() => handleCallback(onFulfilled, this.value));
      this.onRejectedCallbacks.push(() => handleCallback(onRejected, this.value));
    }
  });
}

看到没?then 返回的是一个全新的 Promise,而不是直接返回结果。这个新 Promise 的 resolve 取决于回调函数的返回值——这,就是链式调用的本质。


5. 静态方法:Promise.resolve / Promise.reject

static resolve(value) {
  if (value instanceof MiniPromise) return value;
  return new MiniPromise(resolve => resolve(value));
}

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

简单到不用解释。


6. Promise.all 怎么写?

static all(promises) {
  return new MiniPromise((resolve, reject) => {
    const results = [];
    let completed = 0;

    if (promises.length === 0) return resolve([]);

    promises.forEach((p, i) => {
      MiniPromise.resolve(p).then(val => {
        results[i] = val;
        completed++;
        if (completed === promises.length) resolve(results);
      }, reject);
    });
  });
}

核心就一个:遍历 + 计数 + 全部成功才 resolve


7. 写完 MiniPromise,我学到了什么?

  1. Promise 不是什么魔法:就是一个有状态管理的异步容器,外加一套回调收集 + 异步调度机制

  2. 链式调用的本质:每个 .then() 返回一个新 Promise,上一个 then 的返回值成为下一个 then 的输入

  3. queueMicrotask 的作用:确保 then 的回调总是异步执行,这是 Promise 行为一致性的根基

  4. Promise.resolve 的"递归展开":这是 Promise 最难理解的部分——如果 resolve 的是一个 Promise,需要等它完成后再 fulfill 当前 Promise


结语

手写一遍之后,再看 Promise.allPromise.raceasync/await,你会发现它们都是建立在同一套机制上的延伸。

源码不是魔法,原理才是。


完整代码我已经整理到 GitHub,有兴趣的同学可以跑跑测试:

GitHub 地址(可替换为你的仓库)