实现一个并发数可变的 Promise.all 静态方法

实现一个并发数可变的 Promise.all 静态方法

Promise.all (iterable): The all function returns a new promise which is fulfilled with an array of fulfillment values for the passed promises, or rejects with the reason of the first passed promise that rejects. It resolves all elements of the passed iterable to promises as it runs this algorithm.

Promise.all 静态方法具有如下特性:

  1. 接收一个 Promise 实例的数组或具有 Iterator 接口的对象
  2. 如果元素不是 Promise 对象则使用 Promise.resolve 转成 Promise 对象
  3. 如果全部成功状态变为 resolved,返回值将有序组成一个数组传给回调
  4. 只要有一个失败状态就变为 rejected,返回值将直接传递给回调
  5. 该方法的返回值是一个新的 Promise 对象

下面是 Promise.all 的简单用法:

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);

Promise.all([p1, p2, p3])
    .then((results) => {
        console.log(results); // [1, 2, 3]
    });
const p1 = Promise.resolve(1);
const p2 = Promise.reject(2);
const p3 = Promise.resolve(3);

Promise.all([p1, p2, p3])
    .then((results) => {
        console.log(results);
    }).catch((e) => {
        console.log(e); // 2
    });

如何实现 Promise.all 静态方法

Promise.all = Promise.all || function(promises) {
    // 如果实参不是数组则报错返回
    if (!isArray(promises)) {
        throw new TypeError('You must pass an array to all.');
    }

    // 结果返回一个新的 Promise 实例
    return new Promise(function(resolve, reject) {
        var i = 0,
            result = [],
            len = promises.length,
            count = len

        // 使用闭包记录数组执行顺序
        function resolver(index) {
            return function(value) {
                resolveAll(index, value);
            };
        }

        // 只要有一个失败状态就变为 rejected
        function rejecter(reason) {
            reject(reason);
        }

        // 如果全部成功状态变为 resolved
        function resolveAll(index, value) {
            result[index] = value;
            if (--count == 0) {
                resolve(result)
            }
        }

        // 遍历数组并发执行异步代码
        for (; i < len; i++) {
            promises[i].then(resolver(i), rejecter);
        }
    });
}

实现一个调度粒度可变的 Promise.all 静态方法

那么回到题目的问题:如何实现一个调度粒度可变的 Promise.all 静态方法呢?这里首先可能会产生一个疑问就是什么叫调度粒度可变,实际上很简单:就是给 all 方法增加一个正整数类型的参数,用来标识传入的 Promise 实例数组中可以并发执行的最大个数。

举例如下,声明三个不同异步时间的 Promise 实例并调用 all 方法,正常情况下(我们粗暴的认为)其执行时间应该为200ms(当然这是一个错误的答案,在这个例子当中我们仅考虑并发逻辑),而在调度粒度为1的 all 方法中其执行时间应该为450ms,在调度粒度为2的 all 方法中其执行时间应该为250ms,以此类推。

const p1 = new Promise((resolve) => setTimeout(() => resolve(1), 150));
const p2 = new Promise((resolve) => setTimeout(() => resolve(2), 200));
const p3 = new Promise((resolve) => setTimeout(() => resolve(3), 100));
Promise.all([p1, p2, p3]).then((r) => console.log(r));

在这种情况下,马上可以想到的一个方法是:我们可以使用队列的数据结构来实现调度粒度为1的 all 方法

Promise.all1 = (promises) => {
    if (!isArray(promises)) {
        throw new TypeError('You must pass an array to all.');
    }

    return new Promise((resolve, reject) => {
        const _q = [...promises];
        const result = [];

        function resolver(value) {
            result.push(value);
            next();
        }

        function rejecter(reason) {
            reject(reason);
        }

        function next() {
            if (_q.length) {
                _q.shift().then(resolver, rejecter);
            } else {
                resolve(result);
            }
        }

        next();
    });
}

写到这儿不难发现,不同调度粒度实际上是对队列每次推出的 Promise 实例数量最大值的约束,以及对返回结果的顺序索引作出缓存,那么把代码进行简单的修改即可实现预期的功能。

Promise.all2 = (promises, concurrent = promises.length) => {
    if (!Array.isArray(promises)) {
        throw new TypeError('You must pass an array to all.');
    }

    if (concurrent < 1) {
        return Promise.reject();
    }

    return new Promise((resolve, reject) => {
        const queue = [...promises];
        const result = [];
        let total = promises.length;
        let count = concurrent;
        let index = 0;

        function resolver(index) {
            return function(value) {
                resolveAll(index, value);
            };
        }

        function resolveAll(index, value) {
            result[index] = value;
            count++;
            total--;
            next();
            
            if (!total) {
                resolve(result);
            }
        }

        function rejecter(reason) {
            reject(reason);
        }

        function next() {
            while (queue.length && count > 0) {
                count--;
                (queue.shift())().then(resolver(index++), rejecter);
            }
        }

        next();
    });
};
当然这里有一个比较吊诡的地方!如果我们按照上面的代码进行 Promise 实例的声明,那么在执行到 all 方法之前它们就已经在并发执行,也就不会有“不同调度粒度”之说,所以为了实现我们预期的功能需要在 all 方法内部把这些 Promise 实例初始化出来。
function promiseFactory(fn) {
    return function() {
        return new Promise(fn);
    };
}

const p1 = promiseFactory((resolve) => setTimeout(() => resolve(1), 1500));
const p2 = promiseFactory((resolve) => setTimeout(() => resolve(2), 2000));
const p3 = promiseFactory((resolve) => setTimeout(() => resolve(3), 1000));
console.time('hello world!');

Promise.all2([p1, p2, p3], 1).then((r) => {
    console.log(r)
    console.timeEnd('hello world!'); // 4500+ms打印结果
});

Promise.all2([p1, p2, p3], 2).then((r) => {
    console.log(r)
    console.timeEnd('hello world!'); // 2500+ms打印结果
});

Promise.all2([p1, p2, p3]).then((r) => {
    console.log(r)
    console.timeEnd('hello world!'); // 2000+ms打印结果
});

为了能看出明显的区别我们把定时器的时间延长到秒,执行代码后完美验证。

至此结束。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值