p-limit,优雅地实现并发

745 阅读3分钟

什么是并发?

并发,顾名思义,就是一起发生。而在前端中,并发大多是与请求挂钩的。

并发的场景

在前端开发中,难免要遇到并发请求的场景,比如说对大文件进行分片上传,文件有100M,而每个分片只有1M,因此要发送100次请求。

大家都知道浏览器是有并发限制的,一次性只允许发送6个请求,如果一次性发送100个请求,肯定会造成卡顿,此外,如果这时候除了文件上传的请求,还有其他请求,也会造成不好的体验,因此我们有必要手动对请求进行并发限制。

在github上有很多实现并发请求的库。p-limit就是一个非常优秀的实现并发请求的库,接下来,让我们来深入了解一下这个库的源码,学习一下如何优雅地实现并发。

源码

p-limit主要是通过队列的形式将所有任务推入到队列中,只有当前执行数量少于并发数且队列不为空时才会从队列中取出任务执行。

生成Promise实例

export default function pLimit(concurrency) {
    const queue = new Queue();
    let activeCount = 0;
    const generator = (fn, ...args) => new Promise(resolve => {
        enqueue(fn, resolve, args);
    });
        .....
 }

首先p-limit的队列用的是yocto-queue这个库,主要用到了入队和出队的方法。 concurrency就是并发数,activeCount则是当前执行的任务数。 函数generator用于生成一个新的Promise实例,将请求任务Promise的状态改变函数resolve以及剩余参数传入到enqueue函数中。

enqueue入队

const enqueue = (fn, resolve, args) => {
    queue.enqueue(run.bind(undefined, fn, resolve, args));
    (async () => {
        await Promise.resolve();
        if (activeCount < concurrency && queue.size > 0) {
            queue.dequeue()();
        }
    })();
};

enqueue函数中,调用yocto-queue的enqueue方法,将经过run调用bind方法创建新函数推入到队列中。

下面的async/await立即执行函数则是为了确保比较是在队列中的请求任务执行之后进行获取到最新的任务执行数,如果少于并发数的话且队列不为空,则从队列中取出任务执行。

run执行

const run = async (fn, resolve, args) => {
    activeCount++;
    const result = (async () => fn(...args))();
    resolve(result);
    try {
        await result;
    } catch {}
    next();
};

run函数就是用来执行请求任务的,activeCount++,当前执行的任务数加一。获取到请求的结果,通过generator函数传递过来的resolve改变它生成的Promise的状态。

这里async/await的作用是等待当前任务执行完再执行下个任务,next函数也就是用于执行下一个任务的。

next执行下一个任务

const next = () => {
    activeCount--;
    if (queue.size > 0) {
        queue.dequeue()();
    }
};

当执行next的时候,说明上一个任务已经执行完毕了,因此将activeCount减一,同时如果队列不为空,取出一个任务来执行。

其他属性

至此p-limit的核心代码已经讲解完毕,除此之外,p-limit还额外提供了三个属性,分别是activeCountpendingCountclearQueue

Object.defineProperties(generator, {
    activeCount: {
            get: () => activeCount,
    },
    pendingCount: {
            get: () => queue.size,
    },
    clearQueue: {
            value: () => {
                    queue.clear();
            },
    },
});

其中activeCount表示当前执行的任务数、pendingCount表示等待执行的任务数,他们都是只读的,不允许被修改,而clearQueue这个属性则可以用来清除队列中未执行的请求,这三个属性拓展了p-limit的功能,让它变得更为强大。

除此之外,为了提升一定的容错,p-limit还对传入的并发数进行的判断,需要满足如下条件:是数字、不能为Infinity、且必须大于0,如果不满足的话,则会抛出错误。

if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
    throw new TypeError('Expected `concurrency` to be a number from 1 and up');
}

总结

以上就是关于p-limit解读的全部内容啦,这仅是我在学习p-limit源码时的理解,如果哪里有误欢迎大佬指出,以后我也会分享更多关于源码的解读!