【JavaScript面试题-异步编程】手写一个符合 Promise/A+ 规范的 Promise,或实现一个带并发限制的请求调度器

0 阅读7分钟

什么是Promise 原理

Promise 原理 可以概括为以下几个核心要点:

  1. 状态机
    Promise 是一个拥有三种状态的对象:pending(进行中)、fulfilled(已成功)、rejected(已失败)。

    • 状态只能由 pending 变为 fulfilled 或 rejected,且一旦改变就不可逆(保证结果稳定)。
  2. 回调注册与异步执行
    通过 .then() 方法注册成功和失败的回调函数。

    • 如果 Promise 已经处于最终状态,回调会被异步执行(微任务)。
    • 如果还在 pending,回调会被存入队列,待状态变更后依次调用。
  3. 链式调用
    .then() 总是返回一个新的 Promise,从而支持链式操作。

    • 上一个回调的返回值会传递给下一个 .then()
    • 如果返回值是一个 Promise(或 thenable 对象),则会等待它解析完毕再继续。
  4. 值的穿透与错误处理

    • 如果 .then() 缺少对应回调,值会直接穿透到下一个链节。
    • 错误可以通过 .catch() 统一捕获,类似于同步代码的 try/catch
  5. 符合 Promise/A+ 规范
    规范定义了 then 方法的行为细节,包括如何解析 thenable 对象、防止循环引用、保证异步调用等,确保所有 Promise 实现可以互操作。

简而言之,Promise 是一种异步编程的解决方案,它通过状态机管理异步操作的结果,并利用回调队列和链式调用来组织异步代码,使其更接近同步编程的写法,避免了“回调地狱”。

什么是并发限制

并发限制是指在同一时间内,允许同时执行的任务或操作的最大数量。

在异步编程中,如果不加限制地同时发起大量请求(如网络请求、文件读写、数据库操作),可能会耗尽系统资源(如内存、网络连接)、导致服务响应变慢甚至崩溃。因此,通过设置并发限制,可以控制同时运行的任务数,确保系统平稳运行。

举例

  • 浏览器通常限制同一域名下的并发请求数为 6 个,这就是一种并发限制。
  • 在 Node.js 中,如果需要读取 100 个文件,可以设置一次最多读 5 个,等某个读完再读下一个,避免文件描述符耗尽。

并发限制通常通过 任务队列 实现:将待执行的任务放入队列,维护当前正在执行的任务计数;每当一个任务完成,就从队列中取出下一个任务执行,确保同时执行的任务数不超过设定的上限。

一个形象比喻

用一个更精简的  “奶茶店”  比喻:

  • Promise 就像一张 取餐小票,它代表一个未来的结果(奶茶),状态只有“待制作”、“已完成”或“已取消”,一旦确定就不会再变。
  • 异步控制 好比奶茶店只有 2 个制作员(并发限制),同时最多做两杯奶茶。
  • 队列思想 就是 排队系统:多出来的订单先进先出地等待,每当一个制作员空闲,就从队首取出下一个订单开始做。

这个比喻概括了 Promise 的状态管理、并发控制和任务排队三个核心概念,简洁而形象。

手写符合 Promise/A+ 规范的 Promise

javascript

function MyPromise(executor) {
  let self = this;
  self.status = 'pending';
  self.value = undefined;
  self.reason = undefined;
  self.onFulfilledCallbacks = [];
  self.onRejectedCallbacks = [];

  function resolve(value) {
    // 处理 thenable 对象(包括原生 Promise 或其他符合 then 方法的对象)
    if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
      let then;
      try {
        then = value.then;
      } catch (err) {
        reject(err);
        return;
      }
      if (typeof then === 'function') {
        // 递归解析 thenable,直到得到一个非 thenable 的值或最终状态
        then.call(value, resolve, reject);
        return;
      }
    }
    // 确保状态改变是异步的(使用 setTimeout 模拟微任务,实际应为微任务)
    setTimeout(() => {
      if (self.status === 'pending') {
        self.status = 'fulfilled';
        self.value = value;
        self.onFulfilledCallbacks.forEach(cb => cb(value));
      }
    });
  }

  function reject(reason) {
    setTimeout(() => {
      if (self.status === 'pending') {
        self.status = 'rejected';
        self.reason = reason;
        self.onRejectedCallbacks.forEach(cb => cb(reason));
      }
    });
  }

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

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  let self = this;
  let promise2;

  // 值穿透:如果回调不是函数,则忽略并传递当前值
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
  onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };

  if (self.status === 'fulfilled') {
    promise2 = new MyPromise((resolve, reject) => {
      setTimeout(() => {
        try {
          let x = onFulfilled(self.value);
          resolvePromise(promise2, x, resolve, reject);
        } catch (err) {
          reject(err);
        }
      });
    });
  }

  if (self.status === 'rejected') {
    promise2 = new MyPromise((resolve, reject) => {
      setTimeout(() => {
        try {
          let x = onRejected(self.reason);
          resolvePromise(promise2, x, resolve, reject);
        } catch (err) {
          reject(err);
        }
      });
    });
  }

  if (self.status === 'pending') {
    promise2 = new MyPromise((resolve, reject) => {
      self.onFulfilledCallbacks.push(value => {
        try {
          let x = onFulfilled(value);
          resolvePromise(promise2, x, resolve, reject);
        } catch (err) {
          reject(err);
        }
      });
      self.onRejectedCallbacks.push(reason => {
        try {
          let x = onRejected(reason);
          resolvePromise(promise2, x, resolve, reject);
        } catch (err) {
          reject(err);
        }
      });
    });
  }

  return promise2;
};

// 核心的解析函数:处理 then 回调返回的值 x,决定 promise2 的状态
function resolvePromise(promise2, x, resolve, reject) {
  // 防止循环引用
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }

  let called = false; // 确保只能调用一次

  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let then = x.then;
      if (typeof then === 'function') {
        // x 是一个 thenable,将其作为 promise 处理
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 递归解析,直到 y 不是 thenable
            resolvePromise(promise2, y, resolve, reject);
          },
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        // x 是一个普通对象
        resolve(x);
      }
    } catch (err) {
      if (called) return;
      called = true;
      reject(err);
    }
  } else {
    // x 是基本类型值
    resolve(x);
  }
}

// 添加 catch 方法
MyPromise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected);
};

// 静态 resolve 方法
MyPromise.resolve = function(value) {
  return new MyPromise(resolve => resolve(value));
};

// 静态 reject 方法
MyPromise.reject = function(reason) {
  return new MyPromise((_, reject) => reject(reason));
};

说明

  1. 处理 thenable 对象(包括原生 Promise)

    • 在 resolve 方法中,如果传入的值是对象或函数,并且拥有 then 方法,则将其视为 thenable 并递归解析,直到得到最终值或最终状态。这符合 Promise/A+ 规范对 resolve 的处理要求。
    • 之前版本只检查了 value instanceof MyPromise,现在通过检查 then 方法的存在来兼容任何符合 Promise/A+ 的 thenable(如原生 Promise、其他库的 Promise)。
  2. 状态变更的异步性

    • 使用 setTimeout 模拟异步,确保 then 中的回调总是在当前代码执行完毕后调用(实际应为微任务,这里简化)。关键是在状态变更时,需要异步执行回调,保证 then 总是异步的。
  3. resolvePromise 函数的健壮性

    • 严格处理循环引用:如果 promise2 和 x 是同一个对象,抛出 TypeError
    • 使用 called 标志防止多次调用(例如,当 thenable 同时调用 resolve 和 reject,或多次调用同一个回调)。
    • 递归解析:当 x 是 thenable 时,继续调用 resolvePromise 解析其返回值,直到得到非 thenable 值。
  4. 值穿透

    • 在 then 方法中,如果传入的 onFulfilled 或 onRejected 不是函数,则用默认函数代替,使得值能够穿透到下一个 then 或 catch
  5. 错误捕获

    • 在 executor 执行时和回调执行时都使用 try/catch 捕获同步错误,并调用 reject

并发请求调度器

javascript

class Scheduler {
  constructor(limit) {
    this.limit = limit;          // 最大并发数
    this.queue = [];             // 等待队列,存放 { task, resolve, reject }
    this.runningCount = 0;       // 当前正在执行的任务数
  }

  // 添加一个任务,返回一个 Promise,该 Promise 在任务实际完成时 resolve/reject
  add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.next(); // 尝试执行下一个任务
    });
  }

  // 尝试从队列中取出任务执行(如果并发未满)
  next() {
    if (this.runningCount < this.limit && this.queue.length) {
      const { task, resolve, reject } = this.queue.shift();
      this.runningCount++;
      // 使用 Promise.resolve().then 确保 task() 的同步错误能被捕获
      Promise.resolve()
        .then(() => task())
        .then(result => resolve(result))
        .catch(error => reject(error))
        .finally(() => {
          this.runningCount--;
          this.next(); // 任务完成,继续尝试下一个
        });
    }
  }
}

说明

  1. 使用 Promise.resolve().then() 包裹任务执行

    • 原代码直接调用 task(),如果 task 同步抛出异常(例如非函数调用错误),会导致程序崩溃。现在通过 Promise.resolve().then(() => task()) 将同步错误转化为 Promise 的 rejected 状态,能够被 catch 捕获并传递给外层的 reject
  2. 任务队列的封装

    • 每个添加的任务被包装成一个包含 task 函数和外部 resolve/reject 的对象。当任务实际完成时,调用对应的 resolve/reject,使外部能通过 add 返回的 Promise 获取结果。
  3. 并发控制逻辑

    • runningCount 记录当前执行数,小于限制时从队列头部取出任务执行。
    • 任务执行完毕(无论成功或失败)后,runningCount 减一,并调用 next() 尝试启动下一个任务。
    • 使用 finally 保证无论任务成功还是失败,计数都会减少,队列继续推进。
  4. 支持任意返回 Promise 的函数

    • task 可以是任意返回 Promise 的函数,调度器不关心任务内部实现,只负责并发控制和结果传递。

总结

  • 手写 Promise 实现了 Promise/A+ 规范的核心:状态机、then 链式调用、thenable 解析、值穿透、错误处理。
  • 并发调度器展示了异步队列管理的经典模式:维护等待队列和并发计数,通过递归调用 next 推动任务执行,并安全捕获同步错误。

#前端、#前端面试、#干货

如果这篇这篇文章对您有帮助?需要你的三连 关注、点赞、收藏,谢谢。
有疑问或想法?评论区见