多层异步之最优解

前言

好久没写博客了,转眼间这是2022年的第一篇博客,相信作为前端开发者的大家肯定对promise一定不会陌生,那么这期我再分享一个promise相关的进阶问题。

问题描述

首先,我们需要进行一组异步请求,然后当每个请求回来后会对当前的结果再进行一组异步操作,当上一次异步操作的结果返回后最后再进行一次异步操作得到最终的结果。这也就是多层异步嵌套。
是不是感觉有点绕,那接下来,我就用结合代码示例来进行具体问题描述。

function main() {
     const outers = [
        new Promise((resolve, reject) => {
            setTimeout(resolve, 700, 'a');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 800, 'b');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 900, 'c');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 1000, 'd');
        })];
}

function getInners(value, length) {
    const res = [];
    for (let i = 1; i <= length; i++) {
        res.push(new Promise((resolve, reject) => {
            setTimeout(resolve, parseInt(Math.random() * (200 - 100) + 100), value + i);
        }));
    }
    return res;
}

function getFin() {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, parseInt(Math.random() * (60 - 20) + 20));
    })
}

首先,在main函数里面有一组待执行的异步请求outers,当每个outer的结果返回后会调用getInners再次生成一组新的待执行的异步请求inners,当每个inner的结果返回后会再调用一次getFin,最后fin执行完的结果也就是一个异步请求分支的最终结果。
那么,现在的问题设计一种既能保证结果的有序性又能耗时更少的方案。

问题分析

那么是否真的能够找到一种即有序又省时的方案呢?
我也不知道,毕竟鱼和熊掌都想要,是不是有点太贪心了。

有序性

我们先着手看看保证有序性,毕竟有序性的实现思路还是比较简单的。
一种非常简单直接的思路是:outers外部并发处理,outers内部串行处理。
Talk is cheap,show you my code:

let count = 1;
function main() {
    const outers = [
        new Promise((resolve, reject) => {
            setTimeout(resolve, 700, 'a');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 800, 'b');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 900, 'c');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 1000, 'd');
        })];
    handlerOuters(outers);
}

function handlerOuters(outers) {
    Promise.all(outers).then(async values => {
        for (let i = 0; i < values.length; i++) {
            const inners = getInners(values[i], values.length - i);
            await handlerInners(inners);
        }
    })
}

async function handlerInners(inners) {
    const nums = await Promise.all(inners);
    for (let i = 0; i < nums.length; i++) {
        await new Promise((resolve, reject) => {
            setTimeout(resolve, parseInt(Math.random() * (60 - 20) + 20));
        })
        console.log("结束:", count++, nums[i]);
    }
}

function getInners(value, length) {
    const res = [];
    for (let i = 1; i <= length; i++) {
        res.push(new Promise((resolve, reject) => {
            setTimeout(resolve, parseInt(Math.random() * (200 - 100) + 100), value + i);
        }));
    }
    return res;
}

main();

运行此代码,和我们预期的结果一样是按照a1,a2…顺序打印。
结下来,我们尝试分析此代码:
首先使用Promise.all并行的发送a、b、c、d四个请求,一方面保证了返回结果的有序性另一方面使用并发技术大幅缩短请求时间。
然后当我们拿到各自的value时会立刻生成inners,但是我们再处理每组inners时是同步进行的(await)。同样,使用Promise.all并发发送inners请求,但是在处理最后的每个返回值的时候也是同步进行。
那么最终我们所需要的时间是多少呢?
time=maxOuter+allHandlerInners,handlerInner = maxInner+allHandlerFin。
显然,这个用时还是比较长的,完全可以进行进一步优化。
下面是加入时间戳的代码,可以更清晰的看到中间过程的用时:

let count = 1;
let start = 0;
let sum = 0;
const res = [];
function main() {
    const outers = [
        new Promise((resolve, reject) => {
            setTimeout(resolve, 700, 'a');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 800, 'b');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 900, 'c');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 1000, 'd');
        })];
    start = Date.now();
    handlerOuters(outers);
}

function handlerOuters(outers) {
    sum = 10;
    for (let i = 0; i < outers.length; i++) {
        res.push([]);
    }
    Promise.all(outers).then(async values => {
        for (let i = 0; i < values.length; i++) {
            const inners = getInners(values[i], values.length - i);
            await handlerInners(inners);
            console.log("中间耗时:", Date.now() - start);
        }
    })
}

async function handlerInners(inners) {
    const nums = await Promise.all(inners);
    for (let i = 0; i < nums.length; i++) {
        await new Promise((resolve, reject) => {
            setTimeout(resolve, parseInt(Math.random() * (60 - 20) + 20));
        })
        console.log("中间耗时:", Date.now() - start);
        res[0].push(nums[i]);
        console.log("结束:", count++, nums[i]);
        if (count === sum + 1) {
            console.log("总耗时:", Date.now() - start);
            console.log('res:', res.flat());
        }
    }
}

function getInners(value, length) {
    const res = [];
    for (let i = 1; i <= length; i++) {
        res.push(new Promise((resolve, reject) => {
            setTimeout(resolve, parseInt(Math.random() * (200 - 100) + 100), value + i);
        }));
    }
    return res;
}

main();

耗时更少

首先我们需要找到时间优化的方向:Promise.all,异步并发请求,无需优化;await,同步请求,需求优化。
知道了优化方向之后(去掉await),但是去掉await之后又如何保证结果的有序性呢。
没错,只能我们自己手动维护结果的有序性,毕竟真所谓,鱼和熊掌不可兼得,便捷好用的await和时间更短二者只能取其一。
Talk is cheap,show you my code:

let count = 1;
let start = 0;
let sum = 0;
const res = [];
function main() {
    const outers = [
        new Promise((resolve, reject) => {
            setTimeout(resolve, 700, 'a');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 800, 'b');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 900, 'c');
        }), new Promise((resolve, reject) => {
            setTimeout(resolve, 1000, 'd');
        })];
    start = Date.now();
    handlerOuters(outers);
}

function handlerOuters(outers) {
    sum = 10;
    for (let i = 0; i < outers.length; i++) {
        res.push([]);
    }
    Promise.all(outers).then(async values => {
        for (let i = 0; i < values.length; i++) {
            const inners = getInners(values[i], values.length - i);
            handlerInners(inners, i);
            console.log("中间耗时:", Date.now() - start);
        }
    })
}

function handlerInners(inners, index) {
    Promise.all(inners).then(nums => {
        for (let i = 0; i < nums.length; i++) {
            new Promise((resolve, reject) => {
                setTimeout(resolve, parseInt(Math.random() * (60 - 20) + 20));
            }).then(() => {
                console.log("中间耗时:", Date.now() - start);
                res[index][i] = nums[i];
                console.log("结束:", count++, nums[i]);
                if (count === sum + 1) {
                    console.log("总耗时:", Date.now() - start);
                    console.log('res:', res.flat());
                }
            });
        }
    })
}

function getInners(value, length) {
    const res = [];
    for (let i = 1; i <= length; i++) {
        res.push(new Promise((resolve, reject) => {
            setTimeout(resolve, parseInt(Math.random() * (200 - 100) + 100), value + i);
        }));
    }
    return res;
}

main();

我们通过在handlerInners函数里传入index参数来确保结果的有序性,从而真正实现了所有请求的异步并发。
运行代码,我们不难发现虽然中间结果是乱序的,但是最终的结果依旧是有序的。
并且此时的时间复杂度就是单次请求的最长时间,即time=maxOuter+maxInner+maxFin。

小节

那么是不是第二种方案就一定比第一种方案更好呢?其实并不一定是这样。
毕竟async/await的出现是有它们自己的意义已经自身的优缺点,具体细节可见mdn官方文档:async/await
我个人认为,方案一(使用async/await)的好处在于代码更简洁可读性更高并且维护成本低,方案二也仅仅只是性能更好这一个优点。
并且,在有些具体场景中也无法使用方案二,比如强制需要走完一个完成的请求过程才能进行下一个请求过程的处理。

结语

这篇文章主要围绕多层异步请求的性能问题进行讲述,顺便扩展到async/await的优缺点。这边文章的内容有讲述不当之处欢迎大家指出,同时如果对文章内容有任何疑问的朋友,也欢迎评论区留言或者私信我。下期再会!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值