Skip to content

手写Promise.all实现原理详解

更新: 2024/7/15 字数: 0 字 时长: 0 分钟

🔈 前言:Promise.all是JavaScript中处理并发异步操作的重要工具,能够同时处理多个Promise并等待它们全部完成。本文将深入剖析其实现原理,并手写一个完整的Promise.all实现。

Promise.all的基本概念

Promise.all接收一个Promise数组(或可迭代对象)作为参数,返回一个新的Promise实例。当所有Promise都成功时,返回的Promise状态变为fulfilled,并将所有Promise结果按原顺序存入一个数组;如果有任何一个Promise失败,则立即返回该失败的Promise的结果。

javascript
// Promise.all的基本用法
const p1 = Promise.resolve(1);
const p2 = new Promise(resolve => setTimeout(() => resolve(2), 100));
const p3 = new Promise(resolve => setTimeout(() => resolve(3), 50));

Promise.all([p1, p2, p3])
  .then(values => console.log(values)) // [1, 2, 3]
  .catch(error => console.error(error));

实现原理分析

Promise.all的核心原理包括以下几点:

  1. 接收可迭代对象:将输入转换为数组处理
  2. 并发执行:同时处理多个Promise
  3. 保持顺序:结果数组与输入顺序一致
  4. 快速失败:任一Promise拒绝则整体拒绝
  5. 空数组处理:对空数组返回空数组结果

手写实现代码

下面是Promise.all的完整手写实现:

javascript
function myPromiseAll(promises) {
  // 判断输入是否可迭代
  if (promises == null || typeof promises[Symbol.iterator] !== 'function') {
    throw new TypeError(`${typeof promises} ${promises} is not iterable`);
  }
  
  // 将可迭代对象转为数组
  const promiseArr = Array.from(promises);
  
  return new Promise((resolve, reject) => {
    // 空数组直接返回空数组结果
    if (promiseArr.length === 0) {
      resolve([]);
      return;
    }
    
    const results = new Array(promiseArr.length); // 存储结果数组
    let completedCount = 0; // 已完成的Promise计数
    
    // 处理每个Promise
    promiseArr.forEach((promise, index) => {
      // 确保每个元素都是Promise
      Promise.resolve(promise)
        .then(value => {
          results[index] = value; // 保持结果顺序
          completedCount++; // 完成计数加1
          
          // 所有Promise都已完成,返回结果数组
          if (completedCount === promiseArr.length) {
            resolve(results);
          }
        })
        .catch(error => {
          // 任一Promise失败,整体失败
          reject(error);
        });
    });
  });
}

实现要点解析

  1. 输入验证

    • 检查输入是否为可迭代对象(具有Symbol.iterator方法)
    • 使用Array.from将可迭代对象转换为数组
  2. Promise包装

    • 返回一个新的Promise,封装整体结果
    • 使用Promise.resolve确保每个元素都是Promise实例
  3. 结果收集

    • 创建固定长度的结果数组,保证结果顺序与输入顺序一致
    • 使用计数器跟踪已完成的Promise数量
  4. 错误处理

    • 任何一个Promise拒绝时,立即拒绝整个Promise.all
    • 不再等待其他未完成的Promise
  5. 边界情况

    • 处理空数组输入,直接返回空数组结果

测试用例

javascript
// 基本测试
const test1 = myPromiseAll([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]);
test1.then(values => console.log('Test 1:', values)); // [1, 2, 3]

// 异步Promise测试
const test2 = myPromiseAll([
  Promise.resolve(1),
  new Promise(resolve => setTimeout(() => resolve(2), 100)),
  new Promise(resolve => setTimeout(() => resolve(3), 50))
]);
test2.then(values => console.log('Test 2:', values)); // [1, 2, 3]

// 错误处理测试
const test3 = myPromiseAll([
  Promise.resolve(1),
  Promise.reject('出错了'),
  Promise.resolve(3)
]);
test3.then(
  values => console.log('Test 3:', values),
  error => console.log('Test 3 Error:', error) // '出错了'
);

// 非Promise值测试
const test4 = myPromiseAll([1, 2, 3]);
test4.then(values => console.log('Test 4:', values)); // [1, 2, 3]

// 空数组测试
const test5 = myPromiseAll([]);
test5.then(values => console.log('Test 5:', values)); // []

实际应用场景

Promise.all在前端开发中有广泛的应用场景:

  1. 并行数据请求
    • 同时发起多个API请求并等待所有结果
    • 减少页面加载时间,提高用户体验
javascript
// 并行加载用户数据和文章列表
const loadUserAndArticles = async (userId) => {
  try {
    const [userData, articleList] = await Promise.all([
      fetch(`/api/users/${userId}`).then(res => res.json()),
      fetch(`/api/articles?author=${userId}`).then(res => res.json())
    ]);
    
    // 同时获得了用户信息和文章列表
    renderUserProfile(userData);
    renderArticles(articleList);
  } catch (error) {
    showError('加载数据失败');
  }
};
  1. 资源预加载
    • 同时加载多个图片、脚本或样式文件
    • 确保所有资源加载完成后再执行后续操作
javascript
// 预加载多张图片
const preloadImages = (imageUrls) => {
  const loadImage = (url) => {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
      img.src = url;
    });
  };
  
  return Promise.all(imageUrls.map(url => loadImage(url)));
};

// 使用示例
preloadImages(['img1.jpg', 'img2.jpg', 'img3.jpg'])
  .then(images => {
    console.log('所有图片加载完成');
    startSlideshow(images);
  })
  .catch(error => console.error('图片加载失败', error));
  1. 批量数据处理
    • 并行处理多个数据项
    • 等待所有处理完成后进行下一步操作
javascript
// 批量更新多条数据
const batchUpdate = async (items) => {
  const updatePromises = items.map(item => 
    fetch(`/api/items/${item.id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(item)
    }).then(res => res.json())
  );
  
  try {
    const results = await Promise.all(updatePromises);
    console.log('所有项目更新成功:', results);
    return results;
  } catch (error) {
    console.error('批量更新失败:', error);
    throw error;
  }
};
  1. 依赖多个异步操作的初始化
    • 应用启动时需要同时完成多个初始化任务
    • 所有任务完成后才能进入应用主流程
javascript
// 应用初始化
const initApp = async () => {
  try {
    await Promise.all([
      initDatabase(),
      loadUserSettings(),
      checkAuthentication(),
      loadInitialData()
    ]);
    
    // 所有初始化任务完成,启动应用
    renderApp();
  } catch (error) {
    showInitError(error);
  }
};

与其他Promise并发方法的比较

方法描述特点
Promise.all等待所有Promise完成全部成功才成功,一个失败则失败
Promise.allSettled等待所有Promise完成返回所有Promise的结果,无论成功失败
Promise.race返回最先完成的Promise结果只关注最快的结果,可能成功可能失败
Promise.any返回第一个成功的Promise只要有一个成功就成功,全部失败才失败

总结

Promise.all是处理并发异步操作的强大工具,通过深入理解其实现原理,我们不仅能更好地使用它,还能根据实际需求进行定制和扩展。手写实现Promise.all不仅是一个很好的练习,也帮助我们深入理解JavaScript的异步编程模型和Promise的工作机制。

在实际开发中,合理使用Promise.all可以显著提高应用性能和用户体验,特别是在需要并行处理多个异步操作的场景下。希望本文的详细解析能帮助你更好地掌握这一重要工具。