深入剖析 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 的工作原理,如果有任何疑问或建议,欢迎在评论区讨论!


参考资源:

相关推荐
前端端2 小时前
claude code 原理分析
前端
GalaxyMeteor2 小时前
Elpis 开发框架搭建第二期 - Webpack5 实现工程化建设
前端
Spider_Man2 小时前
从 “不会迭代” 到 “面试加分”:JS 迭代器现场教学
前端·javascript·面试
我的写法有点潮2 小时前
最全Scss语法,赶紧收藏起来吧
前端·css
小高0072 小时前
🧙‍♂️ 老司机私藏清单:从“记事本”到“旗舰 IDE”,我只装了这 12 个插件
前端·javascript·vue.js
Mo_jon3 小时前
css 遮盖滚动条,鼠标移上显示
前端·css
EveryPossible3 小时前
终止异步操作
前端·javascript·vue.js
Stringzhua3 小时前
setup函数相关【3】
前端·javascript·vue.js
neon12043 小时前
解决Vue Canvas组件在高DPR屏幕上的绘制偏移和区域缩放问题
前端·javascript·vue.js·canva可画