从 p-limit 了解微任务队列

171 阅读5分钟

p-limit git地址

p-limit介绍

官网介绍

Run multiple promise-returning & async functions with limited concurrency
运行多个 返回promise 函数,并且可以限制 数量

并发❓

有两个任务A和B,在一段时间内,通过在A和B两个任务间切换,来完成两个任务,这种情况叫并发。

虽然js是单线程执行的,但是我们却可以同时发起多个异步操作,来起到并发的效果,虽然计算的过程是同步的。

Promise.all

const p = Promise.all([p1, p2, p3]);

js 中提供 提供了 promise.all 这个promise的实例方法,用来处理并发问题,但是不有多个异步的情况下,不能同时执行,p-limit就是为了限制 并发数量而开发的库

写在前面

p-limit 中使用了大量的async await 等 ES7 语法,对于理解微任务队列有很大的帮助,源码使用的是链表,为了方便理解,我使用数组代替

基本使用

const limit = pLimit(1);
// limit接受一个异步任务
const input = [
	limit(() => fetchSomething("foo", 3000)),
	limit(() => fetchSomething("bar", 1000)),
];

// Only one promise is run at once
const result = await Promise.all(input);
console.log(result);

function fetchSomething(arg, time) {
	return new Promise((res) => {
		setTimeout(() => {
			res(arg);
		}, time);
	});
}

其中 pLimit 是入口方法,接受一个数字,表示最大并发数,返回一个 limit函数,接收Promise函数,组成数组,交由 Promise.all 统一执行

入口

function pLimit(concurrency) {
    const queue = [];
    let activeCount = 0;
    ....
const generator = (fn, ...args) => {
    // 完成才算完成,所以传入 resolve
    return new Promise((resolve) => {
	enqueue(fn, resolve, args);
     });
   };
   
   return generator;
}

可以看出 plimit 返回一个 Promise 函数,调用函数enqueue,enqueue 接收 用户limit方法传入的 fn 和 这个promise中的 resolve函数引用,只要调用resolve就证明 这个promise执行成功

来看看 enqueue 方法

enqueue

enqueue主要是用于处理异步函数的入队操作。

const enqueue = (fn, resolve, args)=>{
    queue.push(() => run(fn, resolve, args));
    //
    console.log("enqueue");
    
    (async () => {
    
      await Promise.resolve();
      console.log(activeCount, "activeCount-enqueue");
        
       if (activeCount < concurrency && queue.length > 0) {
            queue.shift()();
        }
       
    }()
    
    console.log("enqueue End");
}

和一般的方法入队处理不同,不仅 push了一个由run包装的回调函数, 他下面包装了一层 自执行async 函数,为了使用await来形成微任务队列,为什么要形成微任务队列,源码有注释

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'的比较,因为activeCount'是异步更新的。

当run函数被取消队列并调用时。在if-statement中的比较中的比较也需要异步进行,以获得activeCount`的最新值。

其实就是将异步函数出队执行的操作放入微任务队列,而await Promise.resolve()也只是返回一个成功的状态。以便于获取最新的activeCount的值。

这个后面再说,这里形成了 产生了 微任务队列,判断如果 queue 队列还有值,就再去执行第一个queue

run 出队

在被出队后的异步函数真正执行的是run函数。它在内部控制了计数器的个数,执行resolve函数以及执行下一个异步任务。

 const run = async (fn, resolve, args) => {
   // 执行异步函数,计数器加一
   activeCount++;
  // 执行异步函数,保存执行结果
  const result = (async () => fn(...args))();
 // 执行resolve函数
 resolve(result);
 try {
   await result;
 } catch {}
 // 取出下一个执行
 next();
 };

这个地方依然使用了async-await 组合,产生了微任务, 最终还是要入微任务队列,最后执行 next方法

next

next函数主要处理此次异步函数执行完毕后的后续操作,包括对计数器减一,取出后续的异步任务。

 const next = () => {
 // 当前执行任务减一
   activeCount--;
 // 如果队列中还有任务,那么继续取出执行
   if (queue.size > 0) {
     queue.shift()();
   }
};

再次判断是否有 queue 有没有执行完的任务,如果有,继续shift拿出来执行,执行run方法

流程图

v2-a29eae274a555aefa5c040d8a96ee238_r.jpg

重点分析微任务队列

源码行数不多,但是对我来说,有点麻烦,具体是指 执行顺序


export default function pLimit(concurrency) {
	const queue = [];

	let activeCount = 0;

	const next = () => {
		activeCount--;
		console.log(activeCount, "activeCount-next");
		if (queue.length > 0) {
			queue.shift()();
		}
	};

	const run = async (fn, resolve, args) => {
		console.log("run start");
		activeCount++;

		const result = (async () => fn(...args))();
		resolve(result);

		console.log("runBefore");

		try {
                    await result;
		} catch {}

		console.log("runAfter");

		next();
	};

	const enqueue = (fn, resolve, args) => {

		queue.push(() => run(fn, resolve, args));

		console.log("enqueueBefore");

		(async () => {
		
                    await Promise.resolve();
		    console.log(activeCount, "activeCount-enqueue");
			
		    if (activeCount < concurrency && queue.length > 0) {
			   queue.shift()();
		    }
		})();

		console.log("enqueueAfter");
	};


	const generator = (fn, ...args) => {
	   return new Promise((resolve) => {
	       enqueue(fn, resolve, args);
	   });
	};

	return generator;
}

function fetchSomething(arg, time) {
    return new Promise((res) => {
	setTimeout(() => {
            res(arg);
	   }, time);
    });
}

const limit = pLimit(1);
const input = [
	limit(() => fetchSomething("foo", 3000)),
	limit(() => fetchSomething("bar", 1000)),
];

const result = await Promise.all(input);
console.log(result);
1.
 queue:[`run`,`run`]
 队列:[`enqueue1`,`enqueue2`]
 打印: enqueueBefore,enqueueAfter,enqueueBefore,enqueueAfter

 清空微任务队列  
 2. 
 当前微任务 `enqueue1`
 剩余微任务队列 [`enqueue2`]
 打印 activeCount,0
 出栈 `run`

3. 执行第一个 `run`
打印 run start,runBefore
添加微任务 next
微任务队列 [`enqueue2``next1`]

宏任务结束,清空微任务队列  
4. 
当前微任务 `enqueue2`  
剩余微任务队列 [`next1`]  
打印 activeCount  


5. queue弹出, 执行第二个`run`   
微任务队列 [`next1`]  


6. 执行第二个 `run`  
打印:run start,runBefore  
微任务队列 [`next1`,`next2`]  


宏任务结束,清空微任务队列  

7.   
当前微任务 `next1`  
打印:runAfter  
微任务队列:[`next2`]  

8. 执行第一个 `next`  
打印:next  

9. 执行最后微任务  
[`next2`]  
打印:runAfter,next  

核心

const run = async (fn, resolve, args) => {
		console.log("run start");
		activeCount++;

		// 通过这个地方保证输出顺序
		// 只有 fn 执行完毕,返回了结果
		// 才会执行 下面的 next 函数
		const result = (async () => fn(...args))();

		// 可以返回 promise 了
		resolve(result);

		console.log("runBefore");

	// 等待 当前的 run 异步函数执行完毕,才可以执行 队列中的下一个函数
		try {
		   await result;
		} catch {}

		console.log("runAfter");

		next();
	};

总结

从这个p-limit 学习如何保证多个异步函数怎么保证执行顺序,可以通过 await第一个异步函数,然后再执行next方法,了解微任务队列的用法,不得不说,用的真的不错
这种人,js就应该向他们收费

参考:
知乎:zhuanlan.zhihu.com/p/549829103…
若川视野:juejin.cn/post/708759…