代码题:Promise 请求并发限制

816 阅读3分钟

前言

之前面试一家公司就遇到这道代码题,当时只是知道应该要用Promise.all() 来处理,但是不清楚应该怎么写,当然那次面试也就挂了,今天刚好看到 前端一个月面试小记,字节、蚂蚁、美团、滴滴 有这道题目。

题目

第一种形式

实现 Scheduler.add() 函数

const timeout = (time) => new Promise(resolve => {
  setTimeout(resolve, time)
})

const scheduler = new Scheduler()
const addTask = (time, order) => {
  scheduler.add(() => timeout(time))
    .then(() => console.log(order))
}

// 限制同一时刻只能执行2个task
addTask(4000'1')
addTask(3500'2')
addTask(4000'3')
addTask(3000'4')
.....

//Scheduler ?
//4秒后打印1
//3.5秒打印2
//3进入队列,到7.5秒打印3 
//...

第二种形式

function asyncPool () {
  ...
}

const timeout = (i) => {
  console.log('开始', i);
  return new Promise((resolve) => setTimeout(() => {
    resolve(i);
    console.log('结束', i);
  }, i));
};


asyncPool(2, [5000, 4000, 3000, 2000], timeout).then(res => {
  console.log(res);
});

答案

第一种

class Scheduler {
  constructor() {
    this.awatiArr = [];
    this.count = 0;
  }
  async add(promiseCreator) {
    if (this.count >= 2) {
     // 函数执行停顿, 3 4 堵塞 为什么是使用 new Promise 进行堵塞呢? 其实就是 把 3 4 的resolve 给1 2通过的人来使用 resolve 表示结束这个等待。
      await new Promise((resolve) => {
        this.awatiArr.push(resolve);
      });
    }
    this.count++;
    const res = await promiseCreator();
    this.count--;
    if (this.awatiArr.length) {
      // 前面promise的resolve, 只要1 2 有一个执行完,就把 3 的停顿终止
      this.awatiArr.shift()();
    }
    return res;
  }
}

// 这里做了一个验证
const timeout = (time) => {
  console.log('开始', time);
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
      console.log('结束', time);
    }, time);
  });
};

const scheduler = new Scheduler();
const addTask = (time, order) => {
  scheduler.add(() => timeout(time)).then(() => console.log(order));
};

// 限制同一时刻只能执行2个task
addTask(4000, '1');
addTask(3500, '2');
addTask(4000, '3');
addTask(3000, '4');

总结: 请求使用 count 来计算以及正在执行的请求,然后使用新的 promise 来进堵塞后续的请求,把 promise 的 resolve 函数暴露出去,然后执行完的请求来结束还在等待中的请求。

第二种

async function asyncPool(poolLimit, array, iteratorFn) {
  const ret = []; // 用于存放所有的promise实例
  const executing = []; // 用于存放目前正在执行的promise
  for (const item of array) {
    const p = Promise.resolve(iteratorFn(item)); 
    ret.push(p);
    // 当请求数量大于 限制数
    if (poolLimit <= array.length) {
      // then回调中,当这个promise状态变为fulfilled后,将其从executing列表中删除
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      // 正在请求的数量已经大于 限制数
      if (executing.length >= poolLimit) {
        // 一旦正在执行的promise列表数量等于限制数,就使用Promise.race等待某一个promise状态发生变更,
        // 状态变更后,就会执行上面then的回调,将该promise从executing中删除,
        // 然后再进入到下一次for循环,生成新的promise进行补充
        await Promise.race(executing);
      }
    }
  }
  return Promise.all(ret);
}

const timeout = (i) => {
  console.log('开始', i);
  return new Promise((resolve) =>
    setTimeout(() => {
      resolve(i);
      console.log('结束', i);
    }, i),
  );
};

asyncPool(2, [5000, 4000, 3000, 2000], timeout).then((res) => {
  console.log(res);
});

总结: 这里有两个数组 一个是 ret 给 promise.all 使用的,executing 是提供给 Promise.race 执行。使用 Promise.race 堵塞住 for 循环。

Promise.resolve 和 new Promise

Promise.resolve()与new Promise(r => r(v))

总结

两道题差不多都是异曲同工。 都是在前面请求到达请求限制,然后暂停。当前面的请求 Promise 状态变成 fulfilled, 就取消前面的暂停。 第一个暂停是函数执行的停端,第二种是 for循环的停端。