前端必刷手写题系列 [22]

424 阅读6分钟

这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战

这个系列也没啥花头,就是来整平时面试的一些手写函数,考这些简单实现的好处是能看出基本编码水平,且占用时间不长,更全面地看出你的代码实力如何。一般不会出有很多边界条件的问题,那样面试时间不够用,考察不全面。

平时被考到的 api 如果不知道或不清楚,直接问面试官就行, api 怎么用这些 Google 下谁都能马上了解的知识也看不出水平。关键是在实现过程,和你的编码状态习惯思路清晰程度等。

注意是简单实现,不是完整实现,重要的是概念清晰实现思路清晰,建议先解释清楚概念 => 写用例 => 写伪代码 => 再实现具体功能,再优化,一步步来。

32. 并发控制

问题是什么

我们在做复杂网站时,往往需要加载数量众多大小不一的资源

比如一个页面启动时需要加载几十张图(甚至更多),这些图片请求几乎是并发的,而在 Chrome 浏览器,最多支持的并发请求次数是有限的,其他的请求会推入到队列中等待,直到上轮请求完成后新的请求才会发出。

这样会导致 - 加载时间过长 - 首屏体验差

我们如何优化 - 懒加载 (核心就是只加载可视区域内的资源) - 限制并发数量

懒加载 Lazyloading 不是本文重点,今天重点是 限制文件并发加载的数量。

先介绍一些基本概念

并发和并行

那么我们再来介绍下什么是并发,什么是并行

我取了知乎高赞的回答,很形象

  • 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行
  • 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发
  • 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行

所以我们理解下

  • 并发的关键是你有处理多个任务的能力,不一定要同时

  • 并行的关键是你有同时处理多个任务的能力。

所以我认为它们最关键的点就是:是否是『同时』(同一时刻) 也就是耳朵在听同时嘴巴在吃的这种同时。

耳朵和嘴巴可以看成 CPU 的双核,可以同一时刻处于计算。

再看下国外人画的图 并发和并行

image.png

浏览器为什么要请求并发数限制?

在浏览器同域名并发请求都产生并发数限制,并发限制通常是4~8以内。

  1. 对操作系统端口资源考虑, 一个TCP(http也是tcp)链接就占用一个端口, 这为了端口数量不被迅速消耗殆尽

  2. 过多并发导致频繁切换产生性能问题

  3. 避免同一客服端并发大量请求超过服务端的并发阈值,在服务端通常都对同一个客户端来源设置并发阀值避免恶意攻击

  4. 客户端良知机制(两个应用抢占资源时候导致强势一方无限制的获取资源导致弱势一方永远阻塞状态)

好了,基本概念已经了解,下面是代码

代码实现

首先准备一些资源,里面有每个资源的 id, 名字 (name), 耗时 (timeConsuming)

const resources = [{
    id: 1,
    name: 'img1',
    timeConsuming: 3000
}, {
    id: 1,
    name: 'img2',
    timeConsuming: 2500
},{
    id: 1,
    name: 'img3',
    timeConsuming: 1000
},{
    id: 1,
    name: 'img4',
    timeConsuming: 1000
},{
    id: 1,
    name: 'img5',
    timeConsuming: 2000
},{
    id: 1,
    name: 'img6',
    timeConsuming: 1500
}];

我们需要一个方法把这些资源加载进来,我们用 Promise 模拟异步加载

function loadSrc(resource) {
    return new Promise((resolve, reject) => {
        console.log(resource.name + 'is loading ...');
        // 模拟下载文件资源的异步事件
        setTimeout(() => {
            console.log(resource.name + 'is loaded successfully !');
            resolve();
        }, resource.timeConsuming)
    })
};

那么重点来了, 怎么限制并发数呢,想到昨天实现的 Promise.race

这个方法设计入参有3个

  • 资源列表
  • 处理方式(下载,埋点,通知等)
  • 最大并发限制数

!注意,在并发过程有任务执行完毕,后面的任务及时补位,保证最大数量的任务并发数。

所以不能n个, n个循环, 然后 Promise.all来限制。

const limitLoader = (srcs, handler, limit) => {
    // 复制下形参,以免对外界传入引用类型的变量造成影响
    let resources = JSON.parse(JSON.stringify(srcs))
    // 这个数组来存放并发执行的异步任务,长度就是限制数
    let promises = new Array(limit)

    // 首先截取前面的limit 数量的资源放入 promises
    // 并且原数组被改变剩下还没被执行的任务 
    // 以 limit = 3 为例 当前 promises: [p1, p2, p3], 资源数组剩下 [r4, r5, r6]
    promises = resources.splice(0, limit).map((item, index) => {
        // 对每个 promise 去执行处理函数,并且返回 index
        // 为什么返回 index, 因为我们如果 p1 执行完毕后,promises 当前 index 会空出来给后面的任务去 补位执行
        return handler(item).then(() => {
            return index
        })
    })
    // 获取第一个 结束的 Promise,注意 race 虽然只返回第一个结束的结果,但是其余的 promise 仍然是执行的
    let endProRes = Promise.race(promises);
    // 接下来就是遍历剩余的资源,一个个链式调用补位执行
    for (let i = 0; i < resources.length; i++) {
        endProRes = endProRes.then((res) => {
            // 这个 res 就是上面返回的 index, 下一个补位到 promises 中
            promises[res] = handler(resources[i]).then(() => {
                // 结束还是返回 index
                return res
            })
            // 接下来下一轮竞速, 到下一个.then
            return Promise.race(promises)
        })
    }
    // 实际上这个循环就可以看成 endProRes.then().then().then()......
}

limitLoader(resources, loadSrc, 3)

输出结果

img1is loading ...
img2is loading ...
img3is loading ...
img3is loaded successfully !   // 第三个最先执行完
img4is loading ...             // 第四个立刻在第三个 index 空上补位, 保证同时3个并发
img4is loaded successfully !
img5is loading ...
img2is loaded successfully !
img6is loading ...
img1is loaded successfully !
img5is loaded successfully !
img6is loaded successfully !

这样就实现了控制加载资源的并发数量

看注释不理解请留言评论,我99%概率会解答(除非人太多,题的问题又不同)。

另外向大家着重推荐下另一个系列的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列 记得点赞哈

今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 点击此处交个朋友 Or 搜索我的微信号infinity_9368,可以聊天说地 加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我 presious tower shock the rever monster,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考