前端代码往往要处理各种异步逻辑。有些是串行的:constpromise1=newPromise(function(resolve){//异步逻辑1...resolve();});constpromise2=newPromise(function(resolve){//异步逻辑2...resolve();});promise1.then(()=>promise2);等待承诺1;等待承诺2;有些是并行的:awaitPromise.all([promise1,promise2]);等待Promise.race([promise1,promise2]);并行异步逻辑有时需要并发控制。并发控制是一个常见的需求,也是面试中经常考的一道面试题。一般我们会用p-limit来做:importpLimitfrom'p-limit';const限制=pLimit(2);constinput=[limit(()=>fetchSomething('foo')),limit(()=>fetchSomething('bar')),limit(()=>doSomething())];constresult=awaitPromise。all(input);console.log(result);比如上面的逻辑就是几个异步逻辑并行执行,最大并发数为2。那么如何实现这样的并发控制呢?我们自己写一个:首先,我们需要传入并发数,返回一个添加并发任务的函数,我们称之为generator:constpLimit=(concurrency)=>{constgenerator=(fn,...args)=>newPromise((resolve)=>{//...});returngenerator;}这里添加的并发任务需要排队,所以我们准备一个队列,记录当前正在进行的异步任务。常量队列=[];让activeCount=0;constgenerator=(fn,...args)=>newPromise((resolve)=>{enqueue(fn,resolve,...args);});添加了异步任务入队,即enqueue。enqueue的作用是在队列中加入一个异步任务,只要没有达到并发限制就执行另一批任务:constenqueue=(fn,resolve,...args)=>{queue.push(run.bind(null,fn,resolve,...args));if(activeCount0){queue.shift()();}};具体运行逻辑如下:construn=async(fn,resolve,...args)=>{activeCount++;constresult=(async()=>fn(...args))();解决(结果);尝试{等待结果;}赶上{}下一个();};计数,运行这个函数,改变最后返回的promise的状态,执行完之后再进行下一步:下一步自然是将活动任务数减一,然后再运行另一个任务:constnext=()=>{activeCount--;if(queue.length>0){queue.shift()();}};这样就保证了并发数的限制。现在整个代码如下,只有40行代码:constpLimit=(concurrency)=>{constqueue=[];让activeCount=0;constnext=()=>{activeCount--;如果(queue.length>0){queue.shift()();}};construn=async(fn,resolve,...args)=>{activeCount++;constresult=(async()=>fn(...args))();解决(结果);尝试{等待结果;}抓住{}下一个();};constenqueue=(fn,resolve,...args)=>{queue.push(run.bind(null,fn,resolve,...args));if(activeCount0){queue.shift()();}};constgenerator=(fn,...args)=>newPromise((resolve)=>{enqueue(fn,resolve,...args);});返回生成器;};这样就实现了并发控制。不信我们来看看:准备这样一段测试代码:constlimit=pLimit(2);functionasyncFun(value,delay){returnnewPromise((resolve)=>{console.log('start'+value);setTimeout(()=>resolve(value),delay);});}(异步函数(){constarr=[limit(()=>asyncFun('aaa',2000)),limit(()=>asyncFun('bbb',3000)),limit(()=>asyncFun('ccc',1000)),limit(()=>asyncFun('ccc',1000)),limit(()=>asyncFun('ccc',1000))];constresult=awaitPromise.all(arr);控制台.log(结果);})();没什么好说的,就是setTimeout+promise,设置不同的延迟时间。并发数为2,我们试试:先并发执行前两个任务,2s执行完一个任务,再执行一个任务,再过一秒,两者都执行,同时执行两个任务.经过测试,我们实现了并发控制!回头看我们的实现过程,其实就是一个队列来保存任务。开始时,一次性执行最大并发数的任务,然后每次执行后启动一个新的。还是比较简单的。以上40行代码是最简化的版本,其实还有一些可以改进的地方,让我们继续改进吧。首先,我们需要暴露并发数,让开发者手动清理任务队列。我们这样写:Object.defineProperties(generator,{activeCount:{get:()=>activeCount},pendingCount:{get:()=>queue.length},clearQueue:{value:()=>{queue.长度=0;}}});使用Object.defineProperties只定义get函数,这样activeCount和pendingCount只能读取,不能更改。它还提供了清除任务队列的功能。然后给传入的参数添加一个验证逻辑:if(!((Number.isInteger(concurrency)||concurrency===Infinity)&&concurrency>0)){thrownewTypeError('Expected`concurrency`tobeanumberfrom1及以上');}不是整数或小于0,会报错。当然,Infinity也是可以的。最后,其实还有一点需要改进,就是这里:constenqueue=(fn,resolve,...args)=>{queue.push(run.bind(null,fn,resolve,...参数));if(activeCount0){queue.shift()();}};应该改为:constenqueue=(fn,resolve,...args)=>{queue.push(run.bind(null,fn,resolve,...args));(async()=>{awaitPromise.resolve();if(activeCount0){queue.shift()();}})();};因为activeCount的逻辑--在任务执行完之后执行,如果任务还没有执行完,此时不允许activeCount。所以,为了保证并发数能够得到准确的控制,需要等到所有的微任务都执行完之后,再取activeCount。执行完所有微任务后如何执行逻辑?添加一个新的微任务还不够吗?所以有这个awaitPromise.resolve();逻辑。这样,就是一个完整的并发控制逻辑,p-limit也是这样实现的。感兴趣的同学可以自己试一下:constpLimit=(concurrency)=>{if(!((Number.isInteger(concurrency)||concurrency===Infinity)&&concurrency>0)){thrownewTypeError('Expected`concurrency`为1及以上的数字');}常量队列=[];让activeCount=0;constnext=()=>{activeCount--;if(queue.length>0){queue.shift()();}};construn=async(fn,resolve,...args)=>{activeCount++;constresult=(async()=>fn(...args))();解决(结果);尝试{等待结果;}抓住{}下一个();};constenqueue=(fn,resolve,...args)=>{queue.push(run.bind(null,fn,resolve,...args));(async()=>{awaitPromise.resolve();if(activeCount0){queue.shift()();}})();};const生成器=(fn,...args)=>newPromise((resolve)=>{enqueue(fn,resolve,...args);});Object.defineProperties(generator,{activeCount:{get:()=>activeCount},pendingCount:{get:()=>queue.length},clearQueue:{value:()=>{queue.length=0;}}});返回生成器;};const限制=pLimit(2);functionasyncFun(value,delay){returnnewPromise((resolve)=>{console.log('start'+value);setTimeout(()=>resolve(value),delay);});}(异步函数(){constarr=[limit(()=>asyncFun('aaa',2000)),limit(()=>asyncFun('bbb',3000)),limit(()=>asyncFun('ccc',1000)),limit(()=>asyncFun('ccc',1000)),limit(()=>asyncFun('ccc',1000))];constresult=awaitPromise.all(arr);控制台.log(结果);})();总结js代码经常要处理异步逻辑的序列化和并行化,可能还需要做并发控制,这也是面试中常见的考点实现并发控制的核心是通过一个队列保存所有的任务,然后在开始时分批执行一批任务直到最大并发数,每执行完一个任务之后再执行一个新的任务。需要注意的是,为了保证获取到的任务数量是准确的,必须在所有微任务都执行完毕后获取数量。只需要40多行代码就可以实现并发控制。其实这就是p-limit的源码。如果你有兴趣,可以自己实现。