Promise 实现 Ajax 并发请求控制

一道字节跳动的面试题:

实现一个批量请求函数 multiRequest(urls, maxNum),要求如下:
• 要求最大并发数 maxNum
• 每当有一个请求返回,就留下一个空位,可以增加新的请求
• 所有请求完成后,结果按照 urls 里面的顺序依次打出

比较简单,情景应该是:

  1. 第一次入队 maxNum(urls 长度更小时为 urls.length)个请求任务
  2. 入队同时立刻执行
  3. 执行完毕,在 result 的对应位置记录结果,并标记状态,将该任务出队,继续取下一个任务入队
  4. 此时产生循环:一次任务的执行完毕,对应下一次任务的开始
const multiRequest = (urls, maxNum) => {
  // 记录url的起始顺序
  const urlsCopy = [...urls];
  // 每个请求的状态,初始 0
  const state = new Array(urls.length).fill(0);
  // 结果按顺序存储
  const result = new Array(urls.length);
  const taskQueue = [];
  const queueLimit = Math.min(maxNum, urls.length);
  while (enqueue(taskQueue, urls.shift()) < queueLimit) {}
  function request(queue, url) {
    let i = urlsCopy.indexOf(url);
    console.log("开始请求任务" + i);
    // 模拟一下fetch/axios请求,默认是resolve
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        // 模拟一个异常
        if (i === 5) {
          reject(new Error(i + "出错了"));
        }
        resolve(url);
      }, 10000 * Math.random());
    }).then(
      (value) => {
        result[i] = value;
        state[i] = 1;
        console.log(i + "任务成功");
        // 任务完成,执行出队,开启下一次任务
        dequeue(queue, url);
      },
      (error) => {
        state[i] = 1;
        console.log(i + "任务失败");
        // 任务完成,执行出队,开启下一次任务
        dequeue(queue, url);
        throw error;
      }
    );
  }
  // 将任务入队,并自动触发请求
  function enqueue(queue = [], url) {
    // 记录下队列长度
    let len = queue.push(url);
    let i = urlsCopy.indexOf(url);
    console.log(i + "任务入队");
    request(queue, url);
    return len;
  }
  // 将任务出队,并入队下一个任务
  function dequeue(queue = [], url) {
    queue.splice(queue.indexOf(url), 1);
    if (urls.length) {
      enqueue(queue, urls.shift());
    } else {
      // 所有任务结束,终止promise并返回
      if (state.indexOf(0) === -1) {
        promise.resolve(result);
      }
    }
  }
  let promise = {
    resolve: "",
    reject: "",
  };
  return new Promise((resolve, reject) => {
    promise.resolve = resolve;
    promise.reject = reject;
  });
};

Async/Await 结合 Promise 实现并发请求控制

替换 Promise 部分即可,和 then 链的写法相比神清气爽。

const multiRequest = (urls, maxNum) => {
  // 记录url的起始顺序
  const urlsCopy = [...urls];
  // 每个请求的状态,初始 0
  const state = new Array(urls.length).fill(0);
  // 结果按顺序存储
  const result = new Array(urls.length);
  const taskQueue = [];
  const queueLimit = Math.min(maxNum, urls.length);
  while (enqueue(taskQueue, urls.shift()) < queueLimit) {}
  function request(queue, url) {
    let i = urlsCopy.indexOf(url);
    console.log("开始请求任务" + i);
    if (i === 5) {
      // 模拟异步
      setTimeout(async () => {
        // async/await 中用try、catch处理异常
        try {
          await Promise.reject(i + "出错了");
        } catch (e) {
          state[i] = 1;
          console.log(i + "任务失败");
          // 任务完成,执行出队,开启下一次任务
          dequeue(queue, url);
          throw e;
        }
      }, 10000 * Math.random());
    } else {
      setTimeout(async () => {
        result[i] = await Promise.resolve(url);
        state[i] = 1;
        console.log(i + "任务成功");
        // 任务完成,执行出队,开启下一次任务
        dequeue(queue, url);
      }, 10000 * Math.random());
    }
  }
  // 将任务入队,并触发请求
  function enqueue(queue = [], url) {
    let len = queue.push(url);
    let i = urlsCopy.indexOf(url);
    console.log(i + "任务入队");
    request(queue, url);
    return len;
  }
  // 将任务出队,并入队下一个任务
  function dequeue(queue = [], url) {
    queue.splice(queue.indexOf(url), 1);
    if (urls.length) {
      enqueue(queue, urls.shift());
    } else {
      // 所有任务结束,终止promise并返回
      if (state.indexOf(0) === -1) {
        promise.resolve(result);
      }
    }
  }
  let promise = {
    resolve: "",
    reject: "",
  };
  return new Promise((resolve, reject) => {
    promise.resolve = resolve;
    promise.reject = reject;
  });
};

至于为什么是这样写?

function request(queue, url) {
  let i = urlsCopy.indexOf(url);
  console.log("开始请求任务" + i);
  if (i === 5) {
    setTimeout(async () => {
      // async/await 中用try、catch处理异常
      try {
        await Promise.reject(i + "出错了");
      } catch (e) {
        state[i] = 1;
        console.log(i + "任务失败");
        // 任务完成,执行出队,开启下一次任务
        dequeue(queue, url);
        throw e;
      }
    }, 10000 * Math.random());
  } else {
    setTimeout(async () => {
      result[i] = await Promise.resolve(url);
      state[i] = 1;
      console.log(i + "任务成功");
      // 任务完成,执行出队,开启下一次任务
      dequeue(queue, url);
    }, 10000 * Math.random());
  }
}

只是因为这样比较人性化,当然也可以这样写:

async function request(queue, url) {
  let i = urlsCopy.indexOf(url);
  console.log("开始请求任务" + i);
  if (i === 5) {
    await setTimeout(() => {
      // 模拟 async/await 返回一个promise
      // 既然是 promise 当然可以用 then/catch 捕获 
      Promise.reject(new Error(i + "出错了")).catch((error) => {
        state[i] = 1;
        console.log(i + "任务失败");
        // 任务完成,执行出队,开启下一次任务
        dequeue(queue, url);
      });
    }, 10000 * Math.random());
  } else {
    await setTimeout(() => {
      Promise.resolve(url).then((value) => {
        result[i] = value;
        state[i] = 1;
        console.log(i + "任务成功");
        // 任务完成,执行出队,开启下一次任务
        dequeue(queue, url);
      });
    }, 10000 * Math.random());
  }
}