深入剖析 Promise 实现:从原理到手写完整实现

前言

在现代 JavaScript 开发中,Promise 已成为处理异步操作的基石。它优雅地解决了回调地狱问题,让异步代码拥有了同步代码般的可读性和可维护性。但你是否真正理解 Promise 的内部工作机制?本文将带你从零开始,逐步构建一个符合 Promises/A+ 规范的 Promise 实现,深入剖析其核心机制。


一、Promise 的核心概念

1.1 状态机:Promise 的三大状态

Promise 本质上是一个状态机,它有三种状态:

  • pending:初始状态,既不是成功也不是失败
  • fulfilled:操作成功完成
  • rejected:操作失败

状态转换是不可逆的,只能从 pending 变为 fulfilled 或从 pending 变为 rejected。这种设计保证了 Promise 行为的确定性。

1.2 Thenable 接口:链式调用的基础

Promise 的核心是 then 方法,它注册了当 Promise 完成或拒绝时的回调函数。then 方法返回一个新的 Promise,这使得链式调用成为可能。


二、基础架构设计

让我们从构造函数开始,逐步构建我们的 Promise 实现。

javascript 复制代码
class MyPromise {
  constructor(executor) {
    // 初始状态
    this.status = 'pending';
    // 成功时传递的值
    this.value = undefined;
    // 失败时传递的原因
    this.reason = undefined;
    
    // 存储成功回调队列
    this.onFulfilledCallbacks = [];
    // 存储失败回调队列
    this.onRejectedCallbacks = [];
    
    // 定义resolve函数
    const resolve = (value) => {
      // 只有pending状态可以转换
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        // 执行所有成功回调
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };
    
    // 定义reject函数
    const reject = (reason) => {
      // 只有pending状态可以转换
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
        // 执行所有失败回调
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };
    
    // 立即执行executor函数
    try {
      executor(resolve, reject);
    } catch (error) {
      // 如果executor执行抛出异常,直接reject
      reject(error);
    }
  }
}

三、 Then 方法的深度解析

then 方法是 Promise 最复杂也最核心的部分,它需要处理多种情况。

javascript 复制代码
then(onFulfilled, onRejected) {
  // 处理参数不是函数的情况 - 值穿透
  onFulfilled = typeof onFulfilled === 'function' 
    ? onFulfilled 
    : value => value;
  
  onRejected = typeof onRejected === 'function' 
    ? onRejected 
    : reason => { throw reason };
  
  // 返回新的Promise,实现链式调用
  const newPromise = new MyPromise((resolve, reject) => {
    // 处理已完成状态
    if (this.status === 'fulfilled') {
      // 使用setTimeout确保异步执行
      setTimeout(() => {
        try {
          // 执行成功回调
          const result = onFulfilled(this.value);
          // 解析返回值
          this.resolvePromise(newPromise, result, resolve, reject);
        } catch (error) {
          // 捕获回调执行中的错误
          reject(error);
        }
      }, 0);
    }
    
    // 处理已拒绝状态
    if (this.status === 'rejected') {
      setTimeout(() => {
        try {
          const result = onRejected(this.reason);
          this.resolvePromise(newPromise, result, resolve, reject);
        } catch (error) {
          reject(error);
        }
      }, 0);
    }
    
    // 处理pending状态
    if (this.status === 'pending') {
      // 将回调函数存入队列
      this.onFulfilledCallbacks.push(() => {
        setTimeout(() => {
          try {
            const result = onFulfilled(this.value);
            this.resolvePromise(newPromise, result, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      });
      
      this.onRejectedCallbacks.push(() => {
        setTimeout(() => {
          try {
            const result = onRejected(this.reason);
            this.resolvePromise(newPromise, result, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      });
    }
  });
  
  return newPromise;
}

1. 值穿透机制

当 then 方法的参数不是函数时,我们需要提供默认实现确保值能正确传递到链中的下一个 Promise。

javascript 复制代码
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };

这保证了以下代码能正常工作。

javascript 复制代码
promise.then().then(value => console.log(value));

2. 异步执行保证

Promises/A+ 规范明确要求 then 方法的回调必须异步执行。我们使用 setTimeout 来模拟微任务。

javascript 复制代码
setTimeout(() => {
  // 回调逻辑
}, 0);

虽然原生 Promise 使用微任务队列而非宏任务,但 setTimeout 帮助我们满足了"异步执行"的基本要求。

3. 错误处理

使用 try...catch 包装回调执行过程,确保任何异常都能被捕获并传递给下一个 Promise。

javascript 复制代码
try {
  const result = onFulfilled(this.value);
  this.resolvePromise(newPromise, result, resolve, reject);
} catch (error) {
  reject(error);
}

四、Promise 解析过程:resolvePromise 方法

resolvePromise 方法是实现符合 Promises/A+ 规范的关键,它处理了 then 方法返回值的多种情况。

javascript 复制代码
resolvePromise(promise, x, resolve, reject) {
  // 防止循环引用
  if (promise === x) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  
  // 防止多次调用
  let called = false;
  
  // 如果x是对象或函数
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      // 获取x的then方法
      const then = x.then;
      
      // 如果then是函数,假定x为Promise或thenable对象
      if (typeof then === 'function') {
        then.call(
          x,
          // resolve回调
          y => {
            if (called) return;
            called = true;
            // 递归解析,直到返回值不是Promise
            this.resolvePromise(promise, y, resolve, reject);
          },
          // reject回调
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        // 如果x是普通对象或函数,但没有then方法
        resolve(x);
      }
    } catch (error) {
      if (called) return;
      called = true;
      reject(error);
    }
  } else {
    // 如果x是基本类型值
    resolve(x);
  }
}

1. 循环引用检测

防止 Promise 与自身循环引用。

javascript 复制代码
if (promise === x) {
  return reject(new TypeError('Chaining cycle detected for promise'));
}

2. Thenable 对象处理

处理具有 then 方法的对象(thenable),这是 Promise interoperability 的基础。

javascript 复制代码
if (typeof then === 'function') {
  then.call(
    x,
    y => {
      // 递归解析
      this.resolvePromise(promise, y, resolve, reject);
    },
    r => {
      reject(r);
    }
  );
}

3. 确保只执行一次

使用 called 标志位确保 resolve 或 reject 只执行一次。

javascript 复制代码
let called = false;

// 在回调中检查
if (called) return;
called = true;

五、 Catch 方法与错误处理

catch 方法是 then 方法的语法糖,专门用于错误处理。

javascript 复制代码
catch(onRejected) {
  return this.then(null, onRejected);
}

这种设计保持了 API 的简洁性,同时充分利用了 then 方法已有的功能。

六、完整实现代码

将以上各部分组合起来,我们得到完整的 Promise 实现。

javascript 复制代码
class MyPromise {
  constructor(executor) {
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    
    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());
      }
    };
    
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
  
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
    
    const newPromise = new MyPromise((resolve, reject) => {
      if (this.status === 'fulfilled') {
        setTimeout(() => {
          try {
            const result = onFulfilled(this.value);
            this.resolvePromise(newPromise, result, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }
      
      if (this.status === 'rejected') {
        setTimeout(() => {
          try {
            const result = onRejected(this.reason);
            this.resolvePromise(newPromise, result, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }
      
      if (this.status === 'pending') {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const result = onFulfilled(this.value);
              this.resolvePromise(newPromise, result, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });
        
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const result = onRejected(this.reason);
              this.resolvePromise(newPromise, result, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });
      }
    });
    
    return newPromise;
  }
  
  catch(onRejected) {
    return this.then(null, onRejected);
  }
  
  resolvePromise(promise, x, resolve, reject) {
    if (promise === x) {
      return reject(new TypeError('Chaining cycle detected for promise'));
    }
    
    let called = false;
    
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
      try {
        const then = x.then;
        
        if (typeof then === 'function') {
          then.call(
            x,
            y => {
              if (called) return;
              called = true;
              this.resolvePromise(promise, y, resolve, reject);
            },
            r => {
              if (called) return;
              called = true;
              reject(r);
            }
          );
        } else {
          resolve(x);
        }
      } catch (error) {
        if (called) return;
        called = true;
        reject(error);
      }
    } else {
      resolve(x);
    }
  }
}

总结

虽然我们的实现基本符合 Promises/A+ 规范,但与原生 Promise 仍存在一些重要差异,如原生 Promise 使用微任务队列,而我们使用 setTimeout(宏任务),我们未实现 Promise.all、Promise.race、Promise.finally 等静态方法。但通过手动实现 Promise,我们深入理解其内部工作机制:

  1. 状态管理:Promise 是一个状态机,具有明确的状态转换规则
  2. 回调队列:使用数组存储回调函数,支持多个 then 调用
  3. 链式调用:then 方法返回新 Promise 是实现链式调用的基础
  4. 值穿透:处理非函数参数确保值正确传递
  5. 异步保证:确保回调总是异步执行,符合 Promises/A+ 规范
  6. Promise 解析:递归解析 thenable 对象,实现 interoperability

理解 Promise 的内部实现不仅有助于我们更好地使用它,也能帮助我们在面对复杂异步场景时做出更合理的设计决策。希望本文能帮助你深入理解 Promise 的工作原理,如果有任何疑问或建议,欢迎在评论区讨论!


参考资源:

相关推荐
竹林81837 分钟前
用 wagmi v2 + viem 监听链上事件,我踩了三天坑终于搞懂了实时日志与历史补全
javascript
Momo__41 分钟前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
只一44 分钟前
😭从回调地狱到 async/await:一文打通 Ajax 与 JS 异步编程
javascript
程序员小富1 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇1 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇1 小时前
React中的forwardRef
前端·react.js·面试
槑有老呆1 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端
风骏时光牛马1 小时前
Verilog开发常见问题汇总解析
前端
子兮曰1 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端