使用Promise实现并发控制

1,239 阅读2分钟

最近在做一个图片懒加载的需求,因为使用公司组件库的限制,没办法直接使用 data-src 属性加 Intersection Observer 来实现,也没法使用 getBoundingClientRect 方法判断图片的位置来实现,便采取了通过 requestAnimationFrame 的方式限制每秒钟加载图片的个数来实现。因为是内部工具,仅在公司内使用,因此可以不考虑网速等因素。基于此,想到了另外一个场景,即批量上传,同样需要限制每次上传的数量。

思路

使用 Promise.race 判断是否已达到并发的最大数量。依次遍历数据,将异步请求放入并发池中,当并发池中的任务数量达到最大值时,通过 Promise.race 等待并发池中的任务完成,每完成一个任务,就将该任务从并发池中移除,然后将下一个数据的请求任务放入并发池中,直到所有任务都执行完成。

代码实现

/**
 * 异步任务执行最大并发为max的函数
 * @param {*} max 最大并发数
 * @param {*} data 要处理的数据
 * @returns
 */
const concurrency = (max, data = []) => {
  // 并发池
  let pool = [];
  return async function (fn) {
    if (typeof fn !== "function") {
      throw Error(`param ${fn} is expect a function.`);
    }
    for (let i = 0; i < data.length; i++) {
      // 异步任务都采用Promise的形式,加convertToPromise包裹是处理执行的是非异步任务的情况
      const task = convertToPromise(fn.call(this, data[i]));
      pool.push(task);

      task.then(() => {
        const finishedTaskIndex = pool.findIndex((item) => item === task);
        pool.splice(finishedTaskIndex, 1);
      });

      // 这一步判断是关键,当并发池占满时,要等待前面的任务执行完才能再往里面添加任务
      if (pool.length === max) {
        await Promise.race(pool);
      }
    }
    // 等待并发池中所有的任务都执行完成
    // 不加这行,会出现最后几个任务都加入并发池了,但是并没有执行完成,就被判断为已经全部执行完了
    await Promise.all(pool);
    console.log("任务执行完毕!");
  };
};

function convertToPromise(task) {
  if (typeof task?.then === "function") {
    return task;
  }
  return new Promise((resolve) => {
    resolve(task);
  });
}

// 测试
const data = [1, 2, 3, 4, 5, 6, 7];
const executer = concurrency(5, data);
// 执行异步任务
const fn = function (param) {
  return new Promise((resolve, reject) => {
    // 随机延迟定时任务的执行
    const time = Math.floor(Math.random() * 10);
    setTimeout(() => {
      console.log(param, "延迟: ", time);
      resolve();
    }, time * 1000);
  });
};

// 执行同步任务
const f2 = function (params) {
  console.log(params);
};