EastonJiang
首页博客简历关于

EastonJiang

热爱技术,持续学习,记录成长。

导航

  • 首页
  • 博客
  • 简历
  • 关于

友链

  • 🍔✌️ - God
  • 困醒 - 全栈神
  • acye - 全栈神

联系方式

GitHubjiangxu05@outlook.comAweme
© 2026 EastonJiang. All rights reserved.
返回文章列表

Promise手撕教程

2026年5月14日18 分钟
前端

Promise 手撕教程配图 想了很久,也试了很多种讲法,最后我还是决定用这种方式来讲。

我会先告诉你,哪些前置知识会更有助于理解(分成必须和可选);然后你先整体读一遍,再仔细读几遍。

这些方法都是我自己一路踩坑、一路总结出来的经验,所以不同经历的读者,理解速度可能会不一样。

如果前置知识里有你不懂的,建议先复制去问 AI 搞懂,或者你也可以边看代码边理解。

前置知识

必须

  • JavaScript 的 class 语法和结构
    • constructor 会在 class 被 new 的时候执行
  • JavaScript 传参机制
    • 不像 C++ 那种固定传参(函数定义时写几个形参,调用时通常就传几个);
    • JavaScript 函数传参不限制个数,少传的形参是 undefined,多传的可以用 arguments 接收;只按位置匹配,不按变量名匹配。
  • 队列特性
  • 回调函数
  • 形参和实参

可选

  • JavaScript 的内存结构
    • 栈空间存基础数据类型,在 Promise 中,存函数的引用地址(可以理解成快递地址)
    • 堆里存Promise 实例、回调函数、引用类型数据,栈里存地址引用。
  • this 指向弄懂,这里至少搞懂前两种
    • 普通函数的this
    • 箭头函数的this
    • apply / bind / call / new 的this

背景

假如我要睡觉

// ======================
// 👇 回调地狱 正式开始
// ======================
sleep(1000, (res1) => {
  console.log(res1); // 第1层:我睡一秒
  
  sleep(2000, (res2) => {
    console.log(res2); // 第2层:睡晚了一秒之后,我再睡2秒
    
    sleep(3000, (res3) => {
      console.log(res3); // 第3层:之后,再睡3秒
      
      sleep(4000, (res4) => {
        console.log(res4); // 第4层:睡完继续睡,继续嵌套
        // 想加逻辑只能继续往里缩进 → 金字塔灾难
      });
    });
  });
});

这就是回调地狱:

  • 最直观的地狱:缩进层级深,难维护,逻辑容易乱
  • 业务上的地狱:不能并行/等待/复用,错误难以捕获

使用promise后,差不多长这样了:

// ======================
// 👇 用了 Promise —— 链式调用,告别嵌套
// ======================
sleep(1000)
  .then(res1 => { console.log(res1); return sleep(2000); })
  .then(res2 => { console.log(res2); return sleep(3000); })
  .then(res3 => { console.log(res3); return sleep(4000); })
  .then(res4 => { console.log(res4); });

看完即可手撕

Promise 数据结构

你需要记住,Promise 的核心数据结构就是:状态机 + 回调队列

  • 状态有三种:等待中、已成功、已失败;状态只能单向地从等待中进入后两者。
  • 状态说白了本质就是一个变量;既然要修改这个变量,那就需要函数来改。既然有两种落定结果,那通常就要有对应的两个函数。
  • 回调队列,所谓回调,就是“回来再调用”,所以你可以猜到它不是在初始化的时候执行。队列一般可以用数组和shift方法模拟,但这里先不用急着纠结这一点,因为这跟回调真正执行的时机有关系,先记住,后面就会串起来。

实现思路

看完上面的阐述,你很容易就会想到:new Promise 的时候,到底要立即初始化哪些东西?答案其实就是:两个状态常量,两个数组,两个函数。

class MyPromise {
  constructor(executor) {
    // 状态:pending / fulfilled / rejected
    this.status = "pending";
    // 成功结果 / 失败原因
    this.result = undefined;
    // 成功回调队列
    this.onFulfilledcbs = [];
    // 失败回调队列
    this.onRejectedcbs = [];
 
    // 成功函数
    const resolve = (value) => {
    ...
    };
 
    // 失败函数
    const reject = (reason) => {
    ...
    };
 
    // 执行器异常直接 reject
    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
}

这里你可能会问:为什么要在 constructor 里面执行 executor? executor 到底是什么?

  • 这里我们通过一个例子来理解。
function sleep(ms) {
  return new Promise(【 (resolve) => {
    setTimeout(() => {
      resolve("2秒到了");
    }, ms);
  }) 】;
}
 
const promise1 = sleep(2000);
 
promise1.then((res) => {
  console.log(res);
});

注意看,我【】起来的这部分就是 executor,中文一般叫“执行器”。它会在 new Promise(...) 的时候立即执行,因为本质上就是在 constructor 里同步执行的。

也就是说,当 JavaScript 引擎执行到 new Promise(...) 这一行时,会立刻执行【】里的逻辑,比如这里的 setTimeout(...) 会立刻被注册出去。

你会发现 executor 本身是一个回调函数,并且它接收两个参数:resolve 和 reject。对于外部来说,这两个是形式参数;但在 Promise 内部,它们其实是你自己定义出来的函数,再通过闭包传给 executor 使用。

  • 所以说,这个 Promise 被 new 的时候,外部传入的那个箭头函数就是executor,它会随着初始化立即执行。
  • 这件事的关键,本质上就是你能不能理解形参和实参。
  • 那么问题来了:外部写 executor 的时候,为什么能直接拿到 resolve 和 reject?答案是:这两个函数本来就是 Promise 内部定义好的,然后再通过闭包传给 executor。

那回到业务代码,比如刚刚的 sleep。你会发现 resolve 也接受参数,那这个参数是做什么的?它最后会流向哪里?我们来看看 resolve 和 reject 的写法。

const resolve = (value) => {
  if (this.status !== "pending") return;
  this.status = "fulfilled";
  this.result = value;
  // 微任务执行所有成功回调
  queueMicrotask(() => {
	this.onFulfilledcbs.forEach(cb => cb(this.result));
  });
};

可以看到,resolve 一旦被调用,就会去修改前面定义好的 status 和 result。然后还有关键的一步:把当前 Promise 收集到的成功回调放进微任务里依次执行。为什么要这样做?你可以先思考下面几个问题:

  • 这个队列里的东西是从哪里来的?
  • 每个 Promise 的回调队列里都有值吗?
  • 回调队列的值大概几个?
  • 回调队列每次执行完都能干干净净吗,会不会继续把任务传到下一个 Promise?
  • 回调队列里的函数都要吃到当前 Promise 的 result,那如果 result 还没产生,也就是一直没 resolve,该怎么办?(透传)

实现链式调用 功能

我们知道,链式调用是 Promise 的一大特性。它的实现思路也不复杂:then 每次都返回一个新的 Promise,这样新的 Promise 还能继续调用 then,链子也就接起来了。

then 方法,顾名思义就是处理“然后”的逻辑。既然是“然后”,它就一定要依赖前一步的结果。放到业务代码里也很好理解:前面拿到接口数据,后面再在 then 里做一层加工处理。那我们继续看前面的 sleep 函数:

promise1.then(【】);
【(res) => {
  console.log(res);
}】

我把函数单独拎出来,是为了让你更明显地感知到:then 接收的其实就是一个**“结果处理函数”**(onFulfilled / onRejected)。

那这个函数里的 res 又是哪里来的呢?

这里就要把链式调用拆开来看了:前一个 Promise 先产出结果,后一个 Promise 再拿这个结果去加工。所以,then 接收的这个函数,本质上就是“处理前一个 Promise 结果的函数”,一般叫 onFulfilled 或 onRejected。

如果你愿意带着语义去记,onFulfilled 可以理解成:一旦 Promise1 成功,就执行这个函数。

那么这个加工函数加工的是谁?当然就是前一个 Promise 的结果。也就是说,如果当前 then 是挂在 Promise1 后面的,那么它内部大概率会有类似 const res = onFulfilled(this.result); 这样的逻辑,用来拿到 Promise1 的 result,再把处理后的结果交给 Promise2。

那细心的你可能又发现了:我们这里只传了处理成功结果的函数,那失败的情况怎么办?这里就需要写一个默认失败处理函数,也就是你常说的“结果透传”兜底逻辑:

onRejected = typeof onRejected === "function" ? onRejected : err => { throw err };

当用户没有传 onRejected 时,Promise 就会使用这个默认函数。它的作用不是“吞掉错误”,而是继续把错误往后抛,让后面的 Promise 还能接到。

同理,如果用户没传 onFulfilled,那默认就是 val => val,也就是把成功结果原样往后传。你可以把这种默认函数理解成一个不额外加工、只负责继续传递的中间层。

调用 then 方法会返回一个新 Promise。因为 Promise 在初始化时就会把【状态、结果、队列、函数】这些结构准备好,所以你完全可以把 Promise1.then(...) 看成一个新的整体,也就是 Promise2;继续往后链,Promise2.then(...) 又会得到下一个新的 Promise。

与直接 new Promise(...) 不同的是,调用 then 产生出来的 Promise2,必须感知前一个 Promise1 的状态。那它怎么感知?很简单,直接通过 this。因为 then 方法是由 Promise1 实例调用的,所以这里的 this 指向的就是 Promise1。

书写 then 方法时,我的建议是:先写分流逻辑,先根据 Promise1 当前的状态来决定怎么走。

      // 状态还在等待 → 推入队列
      if (this.status === "pending") {
        this.onFulfilledcbs.push(handleSuccess);
        this.onRejectedcbs.push(handleFail);
      }
      // 已成功 → 直接执行
      else if (this.status === "fulfilled") {
        queueMicrotask(handleSuccess);
      }
      // 已失败 → 直接执行
      else {
        queueMicrotask(handleFail);
      }
  • return new Promise 时,如果 Promise1 还没完成,也就是状态还是 pending,那 Promise2 对应的处理函数就暂时没法立刻执行。因为 Promise2 想加工的是 Promise1 的结果,可这时候 Promise1.result 还没出来。
    • 所以最自然的做法就是:先把这些处理函数打包好,先存进 Promise1 的“回调队列”里,等 Promise1 以后有结果了,再回过来执行。
    • 一旦 Promise1 之后 resolve / reject 了,就会把之前存起来的这些函数一个个取出来执行;而如果它一直没结束,那这些函数就只能继续留在队列里等。
  • 如果 Promise1 已经完成了,那就不用进队列了,直接把这个加工任务丢到微任务队列,等当前同步代码执行完后,再由 JavaScript 引擎统一执行。
    • 为什么是丢给微任务队列,可以看一下:https://www.jiangxu.net/blog/eventLoop
    • 简单来说,核心就一句:这里必须异步执行。
  • 失败分支也是同样的思路。

分流结束后,我们进入具体流程。前面说过,如果 Promise1 已经成功,那就可以直接拿它的结果来加工,代码大概就是这样:

const res = onFulfilled(this.result);
resolve(res);

但是,这里不能直接同步执行 onFulfilled。因为 Promise 规范要求:then 的回调必须异步执行。所以我们要先在外面包一层函数,再把这个函数丢进微任务队列。

你要意识到,handleSuccess 本身并不是“现在立刻执行”,它只是一个函数地址;真正什么时候执行,要等后面被调度。

 const handleSuccess = () => {
      ...
	  };

等后一个 Promise 去“查询”前一个 Promise 的状态时,如果前一个还没完成,那后一个 Promise 对应的处理函数就会先被挂起来。这里做一个极端假设比较好理解:如果 Promise1 的任务就是 sleep(一万年),那你不断地往后写 then,本质上就是不断把新的处理函数继续挂到前面的链路上。

等到某个时刻,Promise1 终于拿到结果了,怎么触发后续加工?很简单:业务侧一旦调用了 resolve(...),Promise1 就会拿着自己的 result 去触发前面收集好的回调,比如 Promise1.then(on1),本质上就是执行 on1(Promise1.result)。

以上就是我对 Promise 的理解。看到这里,如果你已经把前面的链路都想通了,那你基本上就可以手撕出完整 Promise 了。下面是完整代码,建议你自己再对着品一遍。

前面几个问题的答案

这些问题都是我在学习过程中自己逼自己想出来的。你如果能把下面这些问题都想明白,那你对 Promise 的理解就会非常深入。

看完前文,你应该能回答以下问题:

  • 这个队列里的东西从哪里来?【从后一个 Promise 推进来】
  • 每个 Promise 的回调队列里都有值吗?【不一定,第一个 Promise 刚创建时可能还没有】
  • 回调队列里的值大概几个?【可以有多个,这也是为什么一个 Promise 可以被多次 then】
promise1.then(fn1)
promise1.then(fn2)
promise1.then(fn3)
  • 回调队列每次执行完都能彻底结束吗,会不会继续把任务传到下一个 Promise?【会继续往后传,因为当前回调执行完,往往又会决定 Promise2 的结果】
  • Promise2 想加工 Promise1 的结果,但 Promise1 还没完成咋整?【先把处理函数存进 Promise1 的回调队列,等 Promise1 resolve 后再执行】

完整代码

class MyPromise {
  constructor(executor) {
    // 状态:pending / fulfilled / rejected
    this.status = "pending";
    // 成功结果 / 失败原因
    this.result = undefined;
    // 成功回调队列
    this.onFulfilledcbs = [];
    // 失败回调队列
    this.onRejectedcbs = [];
 
    // 成功函数
    const resolve = (value) => {
      if (this.status !== "pending") return;
      this.status = "fulfilled";
      this.result = value;
      // 微任务执行所有成功回调
      queueMicrotask(() => {
        this.onFulfilledcbs.forEach(cb => cb(this.result));
      });
    };
 
    // 失败函数
    const reject = (reason) => {
      if (this.status !== "pending") return;
      this.status = "rejected";
      this.result = reason;
      // 微任务执行所有失败回调
      queueMicrotask(() => {
        this.onRejectedcbs.forEach(cb => cb(this.result));
      });
    };
 
    // 执行器异常直接 reject
    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
 
  // then 方法
  then(onFulfilled, onRejected) {
    // Promise A+ 规范:不传则使用默认透传函数
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : val => val;
    onRejected = typeof onRejected === "function" ? onRejected : err => { throw err };
 
    // then 必须返回新 Promise
    return new MyPromise((resolve, reject) => {
      // 封装成功处理逻辑
      const handleSuccess = () => {
        try {
          const res = onFulfilled(this.result);
          resolve(res);
        } catch (err) {
          reject(err);
        }
      };
 
      // 封装失败处理逻辑
      const handleFail = () => {
        try {
          const res = onRejected(this.result);
          resolve(res);
        } catch (err) {
          reject(err);
        }
      };
 
      // 状态还在等待 → 推入队列
      if (this.status === "pending") {
        this.onFulfilledcbs.push(handleSuccess);
        this.onRejectedcbs.push(handleFail);
      }
      // 已成功 → 直接执行
      else if (this.status === "fulfilled") {
        queueMicrotask(handleSuccess);
      }
      // 已失败 → 直接执行
      else {
        queueMicrotask(handleFail);
      }
    });
  }
}
“The only true wisdom is in knowing you know nothing.”