【若川视野 x 源码共读】第31期 | p-limit 限制并发数

362 阅读2分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

使用

p-limit@4.0.0 使用时,需要用module方式引入,当前引入的js后缀名,需要改为mjs,才能在node中使用es module 模块化方案引入包。 image.png

// p-limit的pageage.json 设置了type:"module"
// 所以需要把js变成mjs 然后用es module 形式引入
import pLimit from "p-limit";  

// 模拟请求
const testFetch = (params) => {
    return new Promise((resolve) => {
        setTimeout(resolve, 1000, params);
    })
}
const limit = pLimit(3);  // 限制并发数为3个
const input = [
    limit(() => testFetch(1)),
    limit(() => testFetch(2)),
    limit(() => testFetch(3)),
    limit(() => testFetch(4)),
    limit(() => testFetch(5)),
    limit(() => testFetch(6)),
];

(async () => {
    const result = await Promise.all(input);
    console.log(result);  // 每次并发3个,所以两秒后返回 [1,2,3,4,5,6]
})()

源码

p-limit 用于限制node的并发数。

// https://github1s.com/sindresorhus/yocto-queue/blob/HEAD/index.js
import Queue from 'yocto-queue';  // 这个库的队列用双向链表写的,push shift 都是O(1),如果用数组 shift 是O(n) 

export default function pLimit(concurrency) {
    // 如果 不是整数 不是无穷数 不是大于0,就报类型错误
    if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
        throw new TypeError('Expected `concurrency` to be a number from 1 and up');
    }

    const queue = new Queue();  // 创建一个队列
    let activeCount = 0;  // 活动数量0

    const next = () => {
        activeCount--;  // 活动数量 - 1

        // 下一个队列里面有值,就取出调用
        if (queue.size > 0) {
            queue.dequeue()();  // 出队 并执行
        }
    };
    // 包装
    const run = async (fn, resolve, args) => {
        activeCount++;  // 入队,活动数量+1

        const result = (async () => fn(...args))();  // 执行

        resolve(result);  // 结果返回

        try {
            await result; // 为保证 next 的顺序,采用了 await result
        } catch {}

        next(); // 调用next
    };

    const enqueue = (fn, resolve, args) => {
        queue.enqueue(run.bind(undefined, fn, resolve, args));  // 将传入的函数放到队列中,放之前需要用 run 函数包一下

        (async () => {
            // This function needs to wait until the next microtask before comparing
            // `activeCount` to `concurrency`, because `activeCount` is updated asynchronously
            // when the run function is dequeued and called. The comparison in the if-statement
            // needs to happen asynchronously as well to get an up-to-date value for `activeCount`.
            // 大概意思:activeCount 这个值,需要在一次微任务之后,才会更新为最新的。然后用最新的和 concurrency 比较
            await Promise.resolve();

            // 如果当前活动数 小于 并发数 并且 队列中还有值
            if (activeCount < concurrency && queue.size > 0) {
                    queue.dequeue()();  // 取出队列中的值,并调用
            }
        })();
    };

    // 返回一个 generator 函数, 参数会接收一个函数,函数可以传参数
    const generator = (fn, ...args) => new Promise(resolve => {
        enqueue(fn, resolve, args);  // push到队列中
    });

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

    return generator;
}

总结

Number.isInteger()

Number.POSITIVE_INFINITY

Infinity === Number.POSITIVE_INFINITY
// true

yocto-queue

  • 双向链表写的队列
  • 源码

自执行函数

  • 在入队的时候,放入了一个自执行异步函数。来控制代码发生的时间。
const enqueue = (fn, resolve, args) => {
    queue.enqueue(run.bind(undefined, fn, resolve, args));  // 将传入的函数放到队列中,放之前需要用 run 函数包一下
    (async () => {
        // 以保证执行顺序
        await Promise.resolve();
        if (activeCount < concurrency && queue.size > 0) {
            queue.dequeue()();
        }
    })();
};