源码阅读:p-limit

源码阅读:p-limit

简介

p-limit是一个用于限制并发操作的包,它可以控制同时执行的异步操作数量。它提供了一种简单的方式来管理并发操作,以避免系统资源过度占用和性能下降。

p-limit的工作原理是使用一个计数器来跟踪当前正在执行的操作数量。当有新的操作需要执行时,它会检查当前的计数器值,如果小于设定的并发限制数,则立即执行操作并将计数器加一。如果计数器已达到并发限制数,则将操作加入等待队列,直到有空闲的位置。

p-limit提供了以下功能和特点:

  1. 简单易用:使用p-limit非常简单,只需创建一个限制对象,并将需要执行的异步操作包装成一个函数进行调用即可。
  2. 并发限制:通过设置并发限制数,可以控制同时执行的操作数量,以避免过度占用系统资源。
  3. 异步操作支持:p-limit适用于异步操作,可以是Promise对象、回调函数或任何需要异步执行的操作。
  4. 队列管理:当并发限制数已满时,p-limit会自动将操作加入等待队列,并在有空闲位置时按照先进先出的顺序执行等待的操作。
  5. 可以清空队列:p-limit还提供了清空队列的方法,可以在需要时立即取消所有等待执行的操作。

使用p-limit可以很方便地控制并发操作的数量,特别适用于需要限制资源消耗或避免性能问题的场景,例如网络请求、文件操作、数据库查询等。

使用p-limit非常简单,以下是p-limit的基本用法:

创建一个限制对象,指定并发限制数:

const limit = pLimit(2); // 限制同时执行的操作数量为3

将需要执行的异步操作包装成一个函数,并调用限制对象的函数来执行操作:

const asyncTask = (id) => {
    
    
  return limit(() => {
    
    
    return new Promise((resolve) => {
    
    
      console.log(`Start task ${
      
      id}`);
      setTimeout(() => {
    
    
        console.log(`End task ${
      
      id}`);
        resolve();
      }, 1000);
    });
  });
};

asyncTask(1);
asyncTask(2);
asyncTask(3);

在上面的示例中,我们创建了一个 p-limit 实例,并将并发限制设置为 2。然后,我们定义了一个异步任务 asyncTask,通过 limit 方法包装这个任务,确保最多同时执行 2 个任务。最后,我们按顺序调用 asyncTask,可以看到只有同时运行的任务数不超过 2

此外,p-limit还提供了其他一些功能和方法,例如:

  • 获取当前正在执行的操作数量:limit.activeCount可以获取当前正在执行的操作数量。
  • 获取等待执行的操作数量:limit.pendingCount可以获取等待执行的操作数量。
  • 清空等待队列:limit.clearQueue()可以清空等待队列,取消所有等待执行的操作。

这些功能可以帮助我们更好地管理并发操作的执行和控制。

源码解读

import Queue from 'yocto-queue';

export default function pLimit(concurrency) {
    
    
	// 检查并发数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;
  
  return generator;
}

这段代码实现了一个 pLimit 函数,用于限制并发执行异步操作的数量。首先,代码导入了一个名为 yocto-queue 的链表队列库(详细请看:源码阅读:yocto-queue),并将其命名为 Queue,然后导出了一个名为 pLimit 的函数。pLimit 函数接受一个参数 concurrency,表示允许同时执行的异步操作数量。接下来,代码进行了一系列的校验。它判断 concurrency 是否为一个大于零的整数,或者是否为正无穷大。如果不满足这些条件,将抛出一个类型错误。然后,代码创建了一个队列实例 queue,用于存储需要执行的异步操作。同时,定义了一个变量 activeCount,用于记录当前正在执行的异步操作数量。最后,返回了 generator 函数作为 pLimit 函数的结果。

接下来我们来看看 generator 函数:

// 生成器函数,用于创建一个Promise,将任务入队
const generator = (fn, ...args) => new Promise(resolve => {
    
    
  enqueue(fn, resolve, args);
});

生成器函数 generator用于创建一个新的 Promise,并在其内部调用 enqueue 函数,并将异步操作的结果通过 resolve 函数传递出去。

紧接着我们来看一下 enqueue 函数:

// 入队任务的函数
const enqueue = (fn, resolve, args) => {
    
    
  // 将任务函数fn和resolve函数作为参数传递给run函数,并将run函数入队
  queue.enqueue(run.bind(undefined, fn, resolve, args));

  // 异步执行以下代码块
  (async () => {
    
    
    // 这个函数需要在下一个微任务中执行,以便在比较`activeCount`和`concurrency`之前等待
    // `activeCount`在任务函数出队并调用时会异步更新。if语句中的比较也需要异步执行,以获取`activeCount`的最新值。
    await Promise.resolve();

    // 如果当前正在执行的任务数量小于并发数并且队列中还有任务,则出队并执行
    if (activeCount < concurrency && queue.size > 0) {
    
    
      queue.dequeue()();
    }
  })();
};

函数 enqueue用于将异步函数包装成一个新的异步函数,并将其加入队列中。在加入队列之后,通过一个立即执行的异步函数,检查是否可以从队列中取出并执行下一个操作。这里需要通过 Promise.resolve() 来等待下一个微任务,以确保 activeCountconcurrency 的比较是在队列中的异步操作被执行之后进行的。

接下来我们来看一下入队的 run 函数:

// 执行任务的函数
const run = async (fn, resolve, args) => {
    
    
  activeCount++;

  // 执行任务函数fn,并将结果保存到result中
  const result = (async () => fn(...args))();

  // 将结果resolve出去
  resolve(result);

  try {
    
    
    await result;
  } catch {
    
    }

  // 执行完任务后调用next函数,继续执行下一个任务
  next();
};

异步函数 run用于执行传入的异步函数,并在异步操作完成后执行 next 函数。在执行异步函数之前,将 activeCount 加一,并将异步函数的返回结果通过 resolve 函数传递出去。在 try...catch 语句中,捕获异步函数可能抛出的错误,并在最后调用 next 函数。

接下来看看 next 函数:

// 下一个任务的处理函数
const next = () => {
    
    
  activeCount--;

  // 如果队列中还有任务,则出队并执行
  if (queue.size > 0) {
    
    
    queue.dequeue()();
  }
};

函数 next,用于执行队列中的下一个异步操作。它将 activeCount 减一,并检查队列中是否还有待执行的操作。如果有,就从队列中取出一个操作并执行。

// 使用Object.defineProperties定义generator对象的属性
Object.defineProperties(generator, {
    
    
  activeCount: {
    
    
    // activeCount属性的getter函数,返回当前正在执行的任务数量
    get: () => activeCount,
  },
  pendingCount: {
    
    
    // pendingCount属性的getter函数,返回队列中等待执行的任务数量
    get: () => queue.size,
  },
  clearQueue: {
    
    
    // clearQueue方法,用于清空队列
    value: () => {
    
    
      queue.clear();
    },
  },
});

最后,通过 Object.defineProperties 方法,给 generator 函数添加了一些属性。activeCount 属性返回当前正在执行的异步操作数量,pendingCount 属性返回队列中待执行的操作数量,clearQueue 方法用于清空队列。

该函数的核心思想是使用一个队列来管理并发执行的异步操作,通过控制队列中的操作数和异步操作的完成情况,实现了对并发数量的限制。

/* eslint-disable @typescript-eslint/member-ordering */

export interface LimitFunction {
    
    
	/**
	The number of promises that are currently running.
	*/
	readonly activeCount: number;

	/**
	The number of promises that are waiting to run (i.e. their internal `fn` was not called yet).
	*/
	readonly pendingCount: number;

	/**
	Discard pending promises that are waiting to run.

	This might be useful if you want to teardown the queue at the end of your program's lifecycle or discard any function calls referencing an intermediary state of your app.

	Note: This does not cancel promises that are already running.
	*/
	clearQueue: () => void;

	/**
	@param fn - Promise-returning/async function.
	@param arguments - Any arguments to pass through to `fn`. Support for passing arguments on to the `fn` is provided in order to be able to avoid creating unnecessary closures. You probably don't need this optimization unless you're pushing a lot of functions.
	@returns The promise returned by calling `fn(...arguments)`.
	*/
	<Arguments extends unknown[], ReturnType>(
		fn: (...arguments: Arguments) => PromiseLike<ReturnType> | ReturnType,
		...arguments: Arguments
	): Promise<ReturnType>;
}

/**
Run multiple promise-returning & async functions with limited concurrency.

@param concurrency - Concurrency limit. Minimum: `1`.
@returns A `limit` function.
*/
export default function pLimit(concurrency: number): LimitFunction;

export interface LimitFunction 是一个接口定义,它描述了一个具有特定属性和方法的对象。

  • activeCount 是一个只读属性,表示当前正在运行的Promise的数量。
  • pendingCount 是一个只读属性,表示等待运行的Promise的数量(其内部的fn尚未被调用的数量)。
  • clearQueue 是一个方法,用于丢弃等待运行的Promise。这在程序生命周期的末尾或丢弃引用应用程序的中间状态的函数调用时可能会有用。注意:这不会取消已经在运行的Promise。
  • pLimit 是一个默认导出的函数,它接受一个表示并发限制的concurrency参数,并返回一个实现了LimitFunction接口的对象。
<Arguments extends unknown[], ReturnType>(
  fn: (...arguments: Arguments) => PromiseLike<ReturnType> | ReturnType,
  ...arguments: Arguments
): Promise<ReturnType>;

这个类型声明描述了一个函数声明,该函数接受一个函数作为第一个参数,该函数接受一个参数数组并返回一个PromisePromiseLike对象,或者直接返回一个值。函数的剩余参数是一个参数数组,最后该函数返回一个PromisePromiseresolved值的类型与ReturnType相同。

  • <Arguments extends unknown[], ReturnType>:这是一个泛型参数声明,其中Arguments表示参数数组的类型,ReturnType表示返回值的类型。
  • (fn: (...arguments: Arguments) => PromiseLike<ReturnType> | ReturnType, ...arguments: Arguments) => Promise<ReturnType>:这是一个函数声明,它接受两个参数。第一个参数fn是一个函数,它接受Arguments类型的参数数组并返回一个PromisePromiseLike对象,或者直接返回ReturnType类型的值。第二个参数arguments是一个剩余参数,它接受Arguments类型的参数数组。
  • : Promise<ReturnType>:这是函数的返回类型,表示该函数返回一个Promise对象,并且Promiseresolved值的类型为ReturnType。也就是说,该函数执行后将返回一个PromisePromiseresolved值的类型将与ReturnType相同。

完整源码:

import Queue from 'yocto-queue';

export default function pLimit(concurrency) {
    
    
	// 检查并发数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;

	// 下一个任务的处理函数
	const next = () => {
    
    
		activeCount--;

		// 如果队列中还有任务,则出队并执行
		if (queue.size > 0) {
    
    
			queue.dequeue()();
		}
	};

	// 执行任务的函数
	const run = async (fn, resolve, args) => {
    
    
		activeCount++;

		// 执行任务函数fn,并将结果保存到result中
		const result = (async () => fn(...args))();

		// 将结果resolve出去
		resolve(result);

		try {
    
    
			await result;
		} catch {
    
    }

		// 执行完任务后调用next函数,继续执行下一个任务
		next();
	};

	// 入队任务的函数
	const enqueue = (fn, resolve, args) => {
    
    
		// 将任务函数fn和resolve函数作为参数传递给run函数,并将run函数入队
		queue.enqueue(run.bind(undefined, fn, resolve, args));

		// 异步执行以下代码块
		(async () => {
    
    
			// 这个函数需要在下一个微任务中执行,以便在比较`activeCount`和`concurrency`之前等待
			// `activeCount`在任务函数出队并调用时会异步更新。if语句中的比较也需要异步执行,以获取`activeCount`的最新值。
			await Promise.resolve();

			// 如果当前正在执行的任务数量小于并发数并且队列中还有任务,则出队并执行
			if (activeCount < concurrency && queue.size > 0) {
    
    
				queue.dequeue()();
			}
		})();
	};

	// 生成器函数,用于创建一个Promise,将任务入队
	const generator = (fn, ...args) => new Promise(resolve => {
    
    
		enqueue(fn, resolve, args);
	});

	// 使用Object.defineProperties定义generator对象的属性
	Object.defineProperties(generator, {
    
    
		activeCount: {
    
    
			// activeCount属性的getter函数,返回当前正在执行的任务数量
			get: () => activeCount,
		},
		pendingCount: {
    
    
			// pendingCount属性的getter函数,返回队列中等待执行的任务数量
			get: () => queue.size,
		},
		clearQueue: {
    
    
			// clearQueue方法,用于清空队列
			value: () => {
    
    
				queue.clear();
			},
		},
	});

	return generator;
}

学习与收获

从这段代码中,我们可以学到以下几点:

  1. 使用第三方库 yocto-queue 来管理队列。该库提供了队列的常用操作方法,如入队、出队等。
  2. 使用 Number.isInteger() 方法来检查一个值是否为整数。
  3. 使用 Number.POSITIVE_INFINITY 来表示正无穷大。
  4. 抛出类型错误 TypeError,并提供错误消息。
  5. 使用 async/await 来处理异步操作。在这里,run 函数中的异步函数会被执行,并且在异步操作完成后调用 next 函数。
  6. 使用 await Promise.resolve() 来等待获取下一个微任务,并在微任务中进行异步操作。
  7. 使用 Object.defineProperties() 方法为一个对象添加多个属性并劫持。

总的来说,这段代码展示了如何使用队列和异步操作来限制并发数量,并提供了一些属性和方法来管理队列的状态。

猜你喜欢

转载自blog.csdn.net/p1967914901/article/details/132024057
今日推荐