04月24, 2023

从实现Promise.all()到通过Promise递归实现并发效果

最近有同学面试时被问到,如果并行执行多个异步操作,该如何操作。

这个问题很简单,当然可以使用Promise.all()的静态方法。Promise.all() 会并行执行多个异步操作时等待所有操作完成,并在所有操作完成后返回结果。

基本操作如下(该代码在React环境下运行)

import axios from 'axios';
import { useEffect } from 'react';

export default function TestPromise() {
  let executePromiseAll = async () => {
    let pending = [];
    for (let i = 1; i <= 10; i++) { 
      //pending数组放入10个都是pending状态的promise
      //这里读取的远程数据
      pending.push(axios.get("/api/users/get/" + i));
    }

    //promise的三个状态 pending fulfilled rejected

    //Promise.all静态方法
    Promise.all(pending).then(res => {
      console.log(res);
    })

  }

  useEffect(() => { 
    executePromiseAll();
  },[])

  return (
    <div>
      <h2>
        测试promise.all
      </h2>
    </div>
  )
}

2023-04-24 21.56.07

但是面试官继续发问,如何自己去实现一个Promise.all()函数呢?这个时候同学就卡壳了...一问到稍微底层的就有点虚,加上面试紧张,很容易不知道该从何说起。

其实这个问题要实现很简单,

首先你要知道Promise无非就三种状态,pending,fulfilled和rejected

Promise.all()需要的参数是一个全部是pending状态promise数组,最后Promise.all()执行,将所有结果返回成一个数组。如果有一个失败,则全部失败。

那问题就很简单了啊,如果我们自己实现Promise.all()的话,只需要写一个函数,传一个全部pending状态的数组,迭代数组中的每个元素并执行,将返回结果放入到数组中即可。

let promiseAll = (promises) => {
    return new Promise((resolve, reject) => {
      if (promises.length === 0) { 
        resolve([]);
        return;
      }
      let result = [];
      let count = 0;

      promises.forEach((promise, index) => {
        Promise.resolve(promise).then((value) => {
          result[index] = value;
          count++;

          if (count === promises.length) {
            resolve(result);
          }
        }).catch((error) => {
          reject(error);
        });
      });
    });
}

这个自定义的函数用起来,和Promise.all()就是一模一样的

//自定义promise.all函数
promiseAll(pending).then(res => {
  console.log(res);
})

这个实现实际上是很简单的,但是由此我想到了另外一个比较复杂的面试题的:

要求写一个函数,传入一个url地址的数组和number值,能够根据number值实现并发执行的效果,并将执行的结果放入到一个数组中

首先你要读懂题意,简单来说,比如有10个同时执行的异步操作,执行这个函数,传入的number值如果是3,那么就3个一组,3个一组的执行异步操作。类似于下面的效果:

2023-04-24 23.12.43

要完成这个题目,首先要知道,明白一个道理,比如_request()是一个异步执行函数,如果我们连续执行三次_request(),是一个什么结果

_request();
_request();
_request();

如图,3个异步操作,调用的是一个接口,只是传递的值一样,这样,你看到的效果其实接近于同步效果。那也就是说,如果我们像这样同时调用异步函数,无论你怎么调用,都达不到上面的那种效果。

但是,如果在异步调用函数里面,当执行完结果后,再次执行自身递归,那就可以达到先执行完一个异步,再执行下一个的目的。伪代码如下:

async function _request(){
    //...其他相关代码省略
    let resp = await axios.get('xxx');
    //...
    _request();
}

大家都知道await是语法糖,所以,放在await后面的,肯定是当前异步函数执行之后的操作,因此,如果_request()是像上面的递归函数,那么执行的效果,就如下图:

了解这个重点之后,就可以直接上代码了

/**
* 并发请求
* @param {Array} urls 请求的url数组
* @param {Number} max 同时并发的数量,默认值3
* @returns {Array} result 并发请求后,返回所有请求的结果
*/
let concurrency = (urls, max=3) => { 
    return new Promise((resolve, reject) => { 
      if (urls.length === 0) { 
        resolve([]);
        return;
      }
      let result = []; //记录最终的结果
      let count = 0;
      let index = 0;

      async function _request() { 
        if (index === urls.length) { 
          return;
        }
        //设定临时变量i
        //因为是异步调用,最后把结果放入到result数组中
        //所以就会存在一个问题,放入到数组中的顺序不能确定先后
        //因此临时变量i就是用来确定放入到数组中的顺序
        let i = index; 
        let url = urls[index];
        index++;
        console.log(url);
        try {
          let res = await axios.get(url);
          // 不能使用push,要保证异步执行后,放入到数组中的顺序是正确的
          // result.push(res); 
          result[i] = res;
        } catch (e) {
          result[i] = e;
        } finally {
          count++;
          if (count === urls.length) {
            console.log('请求全部执行完毕');
            resolve(result);
          }
          _request(); //回调执行_request()函数
        }
      }

      //同时执行的promise的数量不能大于数组的数量
      let times = Math.min(max, urls.length);

      for (let i = 0; i < times; i++) {
        _request();
      }
    })
}

调用并发:

let urls = [];
for (let i = 1; i <= 10; i++) { 
  urls.push("/api/users/get/" + i);
}
concurrency(urls, 3).then(resp=>console.log(resp));

完整案例:

import axios from 'axios';
import { useEffect } from 'react';

export default function TestPromise() {
  let executePromiseAll = async () => {
    // 放入pending状态数组
    // let pending = [];
    // for (let i = 1; i <= 10; i++) { 
    //   //pending数组放入10个都是pending状态的promise
    //   pending.push(axios.get("/api/users/get/" + i));
    // }

    // Promise.all静态方法
    // Promise.all(pending).then(res => {
    //   console.log(res);
    // })

    // 自定义promise.all函数
    // promiseAll(pending).then(res => {
    //   console.log(res);
    // })

    //并发请求
    let urls = [];
    for (let i = 1; i <= 10; i++) { 
      urls.push("/api/users/get/" + i);
    }
    concurrency(urls, 3).then(resp=>console.log(resp));
  }

  //自定义promise.all函数

  //promise.all函数的关键点在于,当所有的promise都执行完毕后,才会执行resolve
  //并将所有执行结果返回到一个数组中

  //所以我们需要一个计数器,当计数器的值等于promises的长度时,说明所有的promise都执行完毕了
  //这时候就可以执行resolve了
  //同时,我们还需要一个数组,用来存放每个promise的执行结果
  //当每个promise执行完毕后,就将执行结果存放到数组中
  let promiseAll = (promises) => {
    return new Promise((resolve, reject) => {
      if (promises.length === 0) { 
        resolve([]);
        return;
      }
      let result = [];
      let count = 0;

      promises.forEach((promise, index) => {
        Promise.resolve(promise).then((value) => {
          result[index] = value;
          count++;

          if (count === promises.length) {
            resolve(result);
          }
        }).catch((error) => {
          reject(error);
        });
      });
    });
  }

  /**
   * 并发请求
   * @param {Array} urls 请求的url数组
   * @param {Number} max 同时并发的数量,默认值3
   * @returns {Array} result 并发请求后,返回所有请求的结果
   */
  let concurrency = (urls, max=3) => { 
    return new Promise((resolve, reject) => { 
      if (urls.length === 0) { 
        resolve([]);
        return;
      }
      let result = []; //记录最终的结果
      let count = 0;
      let index = 0;

      async function _request() { 
        if (index === urls.length) { 
          return;
        }
        //设定临时变量i
        //因为是异步调用,最后把结果放入到result数组中
        //所以就会存在一个问题,放入到数组中的顺序不能确定先后
        //因此临时变量i就是用来确定放入到数组中的顺序
        let i = index; 
        let url = urls[index];
        index++;
        console.log(url);
        try {
          let res = await axios.get(url);
          // 不能使用push,要保证异步执行后,放入到数组中的顺序是正确的
          // result.push(res); 
          result[i] = res;
        } catch (e) {
          result[i] = e;
        } finally {
          count++;
          if (count === urls.length) {
            console.log('请求全部执行完毕');
            resolve(result);
          }
          _request(); //回调执行_request()函数
        }
      }

      //同时执行的promise的数量不能大于数组的数量
      let times = Math.min(max, urls.length);

      for (let i = 0; i < times; i++) {
        _request();
      }
    })
  }

  useEffect(() => { 
    executePromiseAll();
  },[])
  return (
    <div>
      <h2>
        测试promise.all
      </h2>
    </div>
  )
}

本文链接:http://www.yanhongzhi.com/post/promise_all.html

-- EOF --

Comments