Promise 并发控制

限制异步操作的并发数量,实现图片资源的批量加载

问题

有 8 个图片资源的 URL 存储在数组中,需要通过 loadImg 函数下载这些图片。loadImg 接收一个 URL,返回一个 Promise,图片下载完成时 resolve,失败时 reject。

要求:任何时刻同时下载的图片数量不超过 3 个,并尽可能快地完成所有下载。

解答

核心思路是维护一个固定大小的”并发池”,当有任务完成时,立即补充新任务进来。

function limitLoad(urls, handler, limit) {
  let sequence = [].concat(urls); // 复制 urls 数组
  
  // 初始化并发池,先启动 limit 个任务
  let promises = sequence.splice(0, limit).map((url, index) => {
    return handler(url).then(() => {
      return index; // 返回下标,用于标识哪个任务完成了
    });
  });
  
  // 使用 reduce 遍历剩余的 urls
  return sequence
    .reduce((pCollect, url) => {
      return pCollect
        .then(() => {
          return Promise.race(promises); // 等待最快完成的任务
        })
        .then(fastestIndex => {
          // 用新任务替换已完成的任务
          promises[fastestIndex] = handler(url).then(() => {
            return fastestIndex; // 继续返回下标供下次使用
          });
        });
    }, Promise.resolve())
    .then(() => {
      // 等待最后剩余的任务全部完成
      return Promise.all(promises);
    });
}

// 使用示例
const urls = [
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png",
  "https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png",
  // ... 更多 URL
];

function loadImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(url);
    img.onerror = reject;
    img.src = url;
  });
}

limitLoad(urls, loadImg, 3).then(() => {
  console.log('所有图片加载完成');
});

关键点

  • 使用 Promise.race 找出最快完成的任务,立即用新任务替换它,保持并发数恒定
  • 通过返回任务下标来定位并发池中哪个位置的任务已完成
  • reduce 串行处理剩余任务的补充逻辑,最后用 Promise.all 等待所有任务完成
  • 初始化时先启动 limit 个任务填满并发池,后续任务采用”完成一个补充一个”的策略