手写 Promise.all:从原理到实现,彻底搞懂并发控制

0 阅读8分钟

前言

Promise 是 ES6 提供的异步解决方案,而 Promise.all 是处理多个并发异步任务的核心方法。

面试中经常被问到:

"请手写一个 Promise.all,并说明它和 Promise.race 的区别"

今天我们从 Promise 的基础出发,结合原型链知识,彻底搞懂 Promise.all 的原理和实现。


一、Promise 基础回顾

1.1 Promise 的三种状态

// 1.html
const p = new Promise((resolve, reject) => {
  console.log(111);  // 同步执行
  
  setTimeout(() => {
    console.log(333);
    reject('失败1');  // 状态变为 rejected
  }, 1000);
});

console.log(222);
console.log(p);

p.then((data) => {
  console.log(data);
}).catch(err => {
  console.log(err);
}).finally(() => {
  console.log('finally');
});

逐行解析:

const p = new Promise((resolve, reject) => {
  • new Promise 创建 Promise 实例
  • 构造函数接收一个 executor 函数,立即执行
  • executor 接收两个参数:resolve(成功回调)和 reject(失败回调)
console.log(111);
  • executor 是同步执行
  • 所以 111 会立即输出
setTimeout(() => {
  console.log(333);
  reject('失败1');
}, 1000);
  • setTimeout 是异步任务,1 秒后执行
  • reject('失败1') 将 Promise 状态从 pending 改为 rejected
  • 传递的参数会传给 .catch 的回调
console.log(222);
console.log(p);
  • 同步代码继续执行,输出 222
  • p 是 Promise 实例,此时状态是 pending

执行顺序:

111222 → [Promise 对象] → (1秒后) 333 → 失败1finally

Promise 状态流转:

状态说明可改变为
pending初始状态,进行中fulfilled 或 rejected
fulfilled操作成功不可改变
rejected操作失败不可改变

1.2 Promise 的原型链结构

console.log(p.__proto__ == Promise.prototype);  // true
console.log(p.__proto__);

逐行解析:

p.__proto__ == Promise.prototype
  • Promise 实例的 __proto__ 指向 Promise.prototype
  • 这是 JS 原型链的核心机制
console.log(p.__proto__);
  • 输出 Promise 原型对象,包含:
    • then 方法
    • catch 方法
    • finally 方法
    • constructor 指向 Promise 构造函数

原型链关系:

Promise 实例 (p)
    ↓ __proto__
Promise.prototype
    ↓ __proto__
Object.prototype
    ↓ __proto__
null

实例方法 vs 静态方法:

类型方法挂载位置调用方式
实例方法thencatchfinallyPromise.prototypep.then()
静态方法allraceresolverejectPromise 构造函数Promise.all()

二、Promise.all 的使用

2.1 基本用法

const p1 = new Promise((resolve) => {
  setTimeout(() => resolve('结果1'), 1000);
});

const p2 = new Promise((resolve) => {
  setTimeout(() => resolve('结果2'), 2000);
});

const p3 = new Promise((resolve) => {
  setTimeout(() => resolve('结果3'), 3000);
});

Promise.all([p1, p2, p3]).then((results) => {
  console.log(results);  // ['结果1', '结果2', '结果3']
});

特点:

  • 接收一个可迭代对象(数组、Set 等)
  • 所有 Promise 都成功时,返回结果数组
  • 结果顺序与入参顺序一致
  • 任一 Promise 失败,立即 reject

2.2 实际应用场景

// 并发请求多个接口
const getUser = () => fetch('/api/user').then(res => res.json());
const getPosts = () => fetch('/api/posts').then(res => res.json());
const getComments = () => fetch('/api/comments').then(res => res.json());

Promise.all([getUser(), getPosts(), getComments()])
  .then(([user, posts, comments]) => {
    console.log('用户信息:', user);
    console.log('文章列表:', posts);
    console.log('评论列表:', comments);
  })
  .catch(err => {
    console.error('请求失败:', err);
  });

优势:

  • 三个请求并发执行,不是串行
  • 总耗时取决于最慢的那个请求
  • 代码简洁,易于维护

三、手写 Promise.all

3.1 基础实现

function myPromiseAll(promises) {
  // 1. 返回一个新的 Promise
  return new Promise((resolve, reject) => {
    // 2. 参数校验
    if (!Array.isArray(promises)) {
      return reject(new TypeError('promises must be an array'));
    }
    
    // 3. 空数组处理
    if (promises.length === 0) {
      return resolve([]);
    }
    
    // 4. 结果数组和计数器
    const results = [];
    let completedCount = 0;
    
    // 5. 遍历每个 Promise
    promises.forEach((promise, index) => {
      // 6. 包装为 Promise(处理非 Promise 值)
      Promise.resolve(promise).then(
        (value) => {
          // 7. 成功时保存结果
          results[index] = value;
          completedCount++;
          
          // 8. 所有 Promise 都完成
          if (completedCount === promises.length) {
            resolve(results);
          }
        },
        (error) => {
          // 9. 任一失败,立即 reject
          reject(error);
        }
      );
    });
  });
}

// 测试
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);

myPromiseAll([p1, p2, p3]).then(console.log);  // [1, 2, 3]

逐行解析:

return new Promise((resolve, reject) => {
  • Promise.all 返回一个新的 Promise
  • 这个 Promise 的状态取决于所有传入的 Promise
if (!Array.isArray(promises)) {
  return reject(new TypeError('promises must be an array'));
}
  • 参数必须是数组(或可迭代对象)
  • 原生 Promise.all 支持所有可迭代对象,这里简化为数组
if (promises.length === 0) {
  return resolve([]);
}
  • 空数组直接返回空数组
  • 这是边界情况处理
const results = [];
let completedCount = 0;
  • results:保存每个 Promise 的结果
  • completedCount:记录已完成的 Promise 数量
  • 为什么用计数器? 因为 Promise 是异步的,需要等待所有完成
promises.forEach((promise, index) => {
  • 遍历每个 Promise
  • index 用于保持结果顺序
Promise.resolve(promise).then(
  • Promise.resolve 将值包装为 Promise
  • 如果已经是 Promise,直接返回
  • 如果是普通值,包装成 resolved 状态的 Promise
(value) => {
  results[index] = value;
  completedCount++;
  
  if (completedCount === promises.length) {
    resolve(results);
  }
}
  • 成功回调:保存结果到对应位置
  • 计数器加 1
  • 当所有 Promise 都完成时,resolve 结果数组
  • 为什么用 results[index] = value 保证结果顺序与入参一致
(error) => {
  reject(error);
}
  • 失败回调:任一 Promise 失败,立即 reject
  • 传递第一个失败的错误信息

3.2 完整实现(支持可迭代对象)

function myPromiseAll(iterable) {
  return new Promise((resolve, reject) => {
    // 1. 转换为数组
    const promises = Array.from(iterable);
    
    // 2. 空数组处理
    if (promises.length === 0) {
      return resolve([]);
    }
    
    // 3. 结果数组和计数器
    const results = [];
    let completedCount = 0;
    
    // 4. 遍历每个 Promise
    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(
        (value) => {
          results[index] = value;
          completedCount++;
          
          if (completedCount === promises.length) {
            resolve(results);
          }
        },
        (error) => {
          reject(error);
        }
      );
    });
  });
}

// 测试 Set
const set = new Set([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]);

myPromiseAll(set).then(console.log);  // [1, 2, 3]

改进点:

const promises = Array.from(iterable);
  • 使用 Array.from 将可迭代对象转换为数组
  • 支持 Set、Map、字符串等可迭代对象

四、Promise 原型链与静态方法

4.1 原型链关系

// 2.js
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.speci = '人类';
let zhen = new Person('郑总', 18);
console.log(zhen.speci);  // '人类'

const kong = {
  name: '孔子',
  hobbies: ['读书', '喝酒']
};

zhen.__proto__ = kong;
console.log(zhen.hobbies, zhen.speci);
// ['读书', '喝酒'] undefined

逐行解析:

Person.prototype.speci = '人类';
  • Person.prototype 上添加属性
  • 所有实例都可以访问
let zhen = new Person('郑总', 18);
console.log(zhen.speci);
  • zhen 自身没有 speci 属性
  • 沿着原型链查找到 Person.prototype.speci
  • 输出 '人类'
zhen.__proto__ = kong;
  • 修改 zhen 的原型指向 kong 对象
  • 注意: 这会破坏原型链,不推荐
console.log(zhen.hobbies, zhen.speci);
  • zhen.hobbies:从 kong 对象获取
  • zhen.speci:原型已改变,找不到,输出 undefined

原型链对比:

对象原生原型链修改后
zhenzhen → Person.prototype → Object.prototype → nullzhen → kong → Object.prototype → null

4.2 Promise 的静态方法

// Promise.all 是静态方法
console.log(typeof Promise.all);  // 'function'
console.log(typeof Promise.prototype.all);  // 'undefined'

// Promise 实例不能直接调用 all
const p = Promise.resolve(1);
console.log(typeof p.all);  // 'undefined'

为什么 Promise.all 是静态方法?

  • Promise.all 不需要访问实例的状态
  • 它接收多个 Promise,返回一个新的 Promise
  • 与具体实例无关,所以挂在构造函数上

静态方法 vs 实例方法:

// 静态方法:挂在构造函数上
Promise.all([p1, p2, p3]);
Promise.race([p1, p2]);
Promise.resolve(value);

// 实例方法:挂在原型上
p.then(onFulfilled, onRejected);
p.catch(onRejected);
p.finally(onFinally);

五、扩展:Promise.race 和 Promise.allSettled

5.1 Promise.race

function myPromiseRace(promises) {
  return new Promise((resolve, reject) => {
    promises.forEach((promise) => {
      Promise.resolve(promise).then(
        (value) => resolve(value),  // 第一个成功的
        (error) => reject(error)    // 第一个失败的
      );
    });
  });
}

// 测试
const p1 = new Promise((resolve) => {
  setTimeout(() => resolve('结果1'), 1000);
});

const p2 = new Promise((resolve) => {
  setTimeout(() => resolve('结果2'), 500);
});

myPromiseRace([p1, p2]).then(console.log);  // '结果2'(先完成)

特点:

  • 返回第一个完成的 Promise 结果
  • 无论成功还是失败
  • 常用于超时控制

Promise.all vs Promise.race:

方法成功条件失败条件返回值
Promise.all所有都成功任一失败结果数组
Promise.race第一个成功第一个失败单个结果

5.2 Promise.allSettled

function myPromiseAllSettled(promises) {
  return new Promise((resolve) => {
    const results = [];
    let completedCount = 0;
    
    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(
        (value) => {
          results[index] = { status: 'fulfilled', value };
          completedCount++;
          if (completedCount === promises.length) {
            resolve(results);
          }
        },
        (error) => {
          results[index] = { status: 'rejected', reason: error };
          completedCount++;
          if (completedCount === promises.length) {
            resolve(results);
          }
        }
      );
    });
  });
}

// 测试
const p1 = Promise.resolve('成功1');
const p2 = Promise.reject('失败2');
const p3 = Promise.resolve('成功3');

myPromiseAllSettled([p1, p2, p3]).then(console.log);
// [
//   { status: 'fulfilled', value: '成功1' },
//   { status: 'rejected', reason: '失败2' },
//   { status: 'fulfilled', value: '成功3' }
// ]

特点:

  • 等待所有 Promise 完成(无论成功失败)
  • 返回每个 Promise 的状态和结果
  • 不会因某个失败而中断

Promise 方法对比:

方法等待条件成功返回失败返回
Promise.all所有成功结果数组第一个错误
Promise.race第一个完成第一个结果第一个错误
Promise.allSettled所有完成状态数组不会失败
Promise.any第一个成功第一个结果所有失败(AggregateError)

5.3 Promise.any

function myPromiseAny(promises) {
  return new Promise((resolve, reject) => {
    const errors = [];
    let rejectedCount = 0;
    
    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(
        (value) => resolve(value),  // 第一个成功就 resolve
        (error) => {
          errors[index] = error;
          rejectedCount++;
          if (rejectedCount === promises.length) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        }
      );
    });
  });
}

// 测试
const p1 = Promise.reject('失败1');
const p2 = Promise.reject('失败2');
const p3 = Promise.resolve('成功3');

myPromiseAny([p1, p2, p3]).then(console.log);  // '成功3'

特点:

  • 返回第一个成功的 Promise
  • 所有都失败时,抛出 AggregateError
  • Promise.race 相反:race 返回第一个完成的,any 返回第一个成功的

六、经典面试题

面试题 1:Promise.all 处理空数组

Promise.all([]).then(console.log);  // []

为什么返回空数组?

  • 空数组没有 Promise 需要等待
  • 直接 resolve 空数组
  • 这是合理的边界情况

面试题 2:Promise.all 中的非 Promise 值

Promise.all([1, Promise.resolve(2), 3]).then(console.log);
// [1, 2, 3]

为什么能处理普通值?

  • Promise.resolve 将普通值包装为 Promise
  • 1 和 3 被包装为 Promise.resolve(1)Promise.resolve(3)
  • 所有值都被统一处理

面试题 3:Promise.all 的错误处理

const p1 = Promise.resolve(1);
const p2 = Promise.reject('错误2');
const p3 = Promise.resolve(3);

Promise.all([p1, p2, p3])
  .then(console.log)
  .catch(console.error);  // '错误2'

为什么只输出错误 2?

  • p2 失败,Promise.all 立即 reject
  • p3 虽然成功,但结果被忽略
  • 注意: p3 仍然会执行,只是结果不会被处理

面试题 4:如何实现并发限制?

function asyncPool(limit, promises, iteratorFn) {
  const results = [];
  const executing = [];
  
  for (const item of promises) {
    const p = Promise.resolve().then(() => iteratorFn(item));
    results.push(p);
    
    if (limit <= promises.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      
      if (executing.length >= limit) {
        Promise.race(executing).then(() => {});
      }
    }
  }
  
  return Promise.all(results);
}

// 使用:最多同时执行 2 个任务
asyncPool(2, [1, 2, 3, 4, 5], (item) => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(item), 1000);
  });
});

核心思路:

  • 维护一个执行队列
  • 当队列长度达到限制时,等待某个任务完成
  • 使用 Promise.race 实现"等待最快完成的任务"

七、总结

核心要点

  1. Promise.all 接收可迭代对象,返回新的 Promise
  2. 所有成功时 resolve 结果数组,任一失败时 reject
  3. 结果顺序与入参顺序一致
  4. 使用计数器跟踪已完成的 Promise 数量
  5. Promise.all静态方法,挂在构造函数上

实现步骤

1. 返回新的 Promise
2. 参数校验(数组/可迭代对象)
3. 空数组处理
4. 遍历每个 Promise
5. 使用 Promise.resolve 包装
6. 成功时保存结果,计数器加 1
7. 所有完成时 resolve
8. 任一失败时 reject

方法对比

方法用途成功条件失败条件
Promise.all并发请求所有成功任一失败
Promise.race超时控制第一个完成第一个失败
Promise.allSettled批量处理所有完成不会失败
Promise.any竞速成功第一个成功所有失败