1.手写webpacktapable源码,手写官方tapable的码手码软性能真的就一定是好的吗?
2.手写作业/文件太多不想写?手写模拟器测试版发布啦~
3.前端模板引擎之mustache手写实现
4.学习vue源码(9)手写代码生成器
5.手写模拟器易语言源代码?
6.Rnote - 简洁优雅、免费开源的写代草图手绘、手写标注笔记软件,手写跨平台支持 windows / mac / linux 系统
手写webpacktapable源码,码手码软官方tapable的写代分析scala项目源码性能真的就一定是好的吗?
完整的手写源码仓库tapable是Webpack?插件机制核心。?手写mini-tapable?不仅解读官方?tapable?的源码,还用自己的码手码软思路去实现一遍,并且和官方的写代运行时间做了个比较,我和webpack作者相关的手写讨论可以点击查看。webpacktapable源码内部根据newFunction动态生成函数执行体这种优化方式不一定是码手码软好的。当我们熟悉了tapable后,写代就基本搞懂了webpackplugin的手写底层逻辑,再回头看webpack源码就轻松很多
目录src目录。码手码软这个目录下是写代手写所有的tapablehook的源码,每个hook都用自己的思路实现一遍,并且和官方的hook执行时间做个对比。
tapable的设计理念:单态、多态及内联缓存由于在webpack打包构建的过程中,会有上千(数量其实是取决于自身业务复杂度)个插件钩子执行,同时同类型的钩子在执行时,函数参数固定,函数体相同,因此tapable针对这些业务场景进行了相应的优化。这其中最重要的是运用了单态性及多态性概念,内联缓存的原理,也可以看这个issue。为了达到这个目标,tapable采用newFunction动态生成函数执行体的方式,主要逻辑在源码的HookCodeFactory.js文件中。
如何理解tapable的设计理念思考下面两种实现方法,哪一种执行效率高,哪一种实现方式简洁?
//方法一:constcallFn=(...tasks)=>(...args)=>{ for(constfnoftasks){ fn(...args)}}//方法二:constcallFn2=(a,b,c)=>(x,y)=>{ a(x,y);b(x,y);c(x,y);}callFn及callFn2的目的都是为了实现将一组方法以相同的参数调用,依次执行。很显然,方法一效率明显更高,并且容易扩展,能支持传入数量不固定的一组方法。但是,如果根据单态性以及内联缓存的说法,很明显方法二的执行效率更高,同时也存在一个问题,即只支持传入a,b,c三个方法,参数形态也固定,这种方式显然没有方法一灵活,那能不能同时兼顾效率以及灵活性呢?答案是可以的。我们可以借助newFunction动态生成函数体的牛金所配资源码方式。
classHookCodeFactory{ constructor(args){ this._argNames=args;this.tasks=[];}tap(task){ this.tasks.push(task);}createCall(){ letcode="";//注意思考这里是如何拼接参数已经函数执行体的constparams=this._argNames.join(",");for(leti=0;i<this.tasks.length;i++){ code+=`varcallback${ i}=this.tasks[${ i}];callback${ i}(${ params})`;}returnnewFunction(params,code);}call(...args){ constfinalCall=this.createCall();//将函数打印出来,方便观察最终拼接后的结果console.log(finalCall);returnfinalCall.apply(this,args);}}//构造函数接收的arg数组里面的参数,就是taska、b、c三个函数的参数constcallFn=newHookCodeFactory(["x","y","z"]);consta=(x,y,z)=>{ console.log("taska:",x,y,z);};constb=(x,y,z)=>{ console.log("taskb:",x,y,z);};constc=(x,y,z)=>{ console.log("taskc:",x,y,z);};callFn.tap(a);callFn.tap(b);callFn.tap(c);callFn.call(4,5,6);当我们在浏览器控制台执行上述代码时:
拼接后的完整函数执行体:
可以看到,通过这种动态生成函数执行体的方式,我们能够同时兼顾性能及灵活性。我们可以通过tap方法添加任意数量的任务,同时通过在初始化构造函数时newHookCodeFactory(['x','y',...,'n'])传入任意参数。
实际上,这正是官方tapable的HookCodeFactory.js的简化版本。这是tapable的精华所在。
tapable源码解读tapable最主要的源码在Hook.js以及HookCodeFactory.js中。Hook.js主要是提供了tap、tapAsync、tapPromise等方法,每个Hook都在构造函数内部调用consthook=newHook()初始化hook实例。HookCodeFactory.js主要是根据newFunction动态生成函数执行体。
demo以SyncHook.js为例,SyncHook钩子使用如下:
const{ SyncHook}=require("tapable");debugger;consttesthook=newSyncHook(["compilation","name"]);//注册plugin1testhook.tap("plugin1",(compilation,name)=>{ console.log("plugin1",name);compilation.sum=compilation.sum+1;});//注册plugin2testhook.tap("plugin2",(compilation,name)=>{ console.log("plugin2..",name);compilation.sum=compilation.sum+2;});//注册plugin3testhook.tap("plugin3",(compilation,name)=>{ console.log("plugin3",compilation,name);compilation.sum=compilation.sum+3;});constcompilation={ sum:0};//第一次调用testhook.call(compilation,"mytest1");//第二次调用testhook.call(compilation,"mytest2");//第三次调用testhook.call(compilation,"mytest3");...//第n次调用testhook.call(compilation,"mytestn");我们用这个demo做为用例,一步步debug。
SyncHook.js源码主要逻辑如下:
constHook=require("./Hook");constHookCodeFactory=require("./HookCodeFactory");//继承HookCodeFactoryclassSyncHookCodeFactoryextendsHookCodeFactory{ }constfactory=newSyncHookCodeFactory();constCOMPILE=function(options){ factory.setup(this,options);returnfactory.create(options);};functionSyncHook(args=[],name=undefined){ //初始化Hookconsthook=newHook(args,name);//注意这里修改了hook的constructorhook.constructor=SyncHook;...//每个钩子都必须自行实现自己的compile方法!!!hook.compile=COMPILE;returnhook;}Hook.js源码主要逻辑如下:
//问题一:思考一下为什么需要CALL_DELEGATEconstCALL_DELEGATE=function(...args){ //当第一次调用时,实际上执行的是CALL_DELEGATE方法this.call=this._createCall("sync");//当第二次或者第n次调用时,此时this.call方法已经被设置成this._createCall的返回值returnthis.call(...args);};...classHook{ constructor(args=[],name=undefined){ this._args=args;this.name=name;this.taps=[];//存储我们通过hook.tap注册的插件this.interceptors=[];this._call=CALL_DELEGATE;//初始化时,this.call被设置成CALL_DELEGATEthis.call=CALL_DELEGATE;...//问题三:this._x=undefined是什么this._x=undefined;//this._x实际上就是this.taps中每个插件的回调//问题四:为什么需要在构造函数中绑定这些函数this.compile=this.compile;this.tap=this.tap;this.tapAsync=this.tapAsync;this.tapPromise=this.tapPromise;}//每个钩子必须自行实现自己的compile方法。compile方法根据this.taps以及this._args动态生成函数执行体compile(options){ thrownewError("Abstract:shouldbeoverridden");}//生成函数执行体_createCall(type){ returnthis.compile({ taps:this.taps,interceptors:this.interceptors,args:this._args,type:type});}..._tap(type,options,fn){ ...this._insert(options);}tap(options,fn){ this._tap("sync",options,fn);}_resetCompilation(){ this.call=this._call;this.callAsync=this._callAsync;this.promise=this._promise;}_insert(item){ //问题二:为什么每次调用testhook.tap()注册插件时,都需要重置this.call等方法?this._resetCompilation();...}}思考Hook.js源码中的几个问题问题一:为什么需要CALL_DELEGATE
问题二:为什么每次调用testhook.tap()注册插件时,都需要重置this.call等方法?
问题三:this._x=undefined是什么
问题四:为什么需要在构造函数中绑定this.compile、this.tap、this.tapAsync以及this.tapPromise等方法
当我们每次调用testhook.tap方法注册插件时,流程如下:
方法往this.taps数组中添加一个插件。this.__insert方法逻辑比较简单,但这里有一个细节需要注意一下,为什么每次注册插件时,都需要调用this._resetCompilation()重置this.call等方法?我们稍后再看下这个问题。先继续debug。
当我们第一次(注意是第一次)调用testhook.call时,实际上调用的是CALL_DELEGATE方法
constCALL_DELEGATE=function(...args){ //当第一次调用时,实际上执行的是CALL_DELEGATE方法this.call=this._createCall("sync");//当第二次或者第n次调用时,此时this.call方法已经被缓存成this._createCall的返回值returnthis.call(...args);};CALL_DELEGATE调用this._createCall函数根据注册的this.taps动态生成函数执行体。并且this.call被设置成this._createCall的返回值缓存起来,如果this.taps改变了,则需要重新生成。
此时如果我们第二次调用testhook.call时,就不需要再重新动态生成一遍函数执行体。考试系统登录界面源码这也是tapable的优化技巧之一。这也回答了问题一:为什么需要CALL_DELEGATE。
如果我们调用了n次testhook.call,然后又调用testhook.tap注册插件,此时this.call已经不能重用了,需要再根据CALL_DELEGATE重新生成一次函数执行体,这也回答了问题二:为什么每次调用testhook.tap()注册插件时,都需要重置this.call等方法。可想而知重新生成的过程是很耗时的。因此我们在使用tapable时,最好一次性注册完所有插件,再调用call
testhook.tap("plugin1");testhook.tap("plugin2");testhook.tap("plugin3");testhook.call(compilation,"mytest1");//第一次调用call时,会调用CALL_DELEGATE动态生成函数执行体并缓存起来testhook.call(compilation,"mytest2");//不会重新生成函数执行体,使用第一次的testhook.call(compilation,"mytest3");//不会重新生成函数执行体,使用第一次的避免下面的调用方式:
testhook.tap("plugin1");testhook.call(compilation,"mytest1");//第一次调用call时,会调用CALL_DELEGATE动态生成函数执行体并缓存起来testhook.tap("plugin2");testhook.call(compilation,"mytest2");//重新调用CALL_DELEGATE生成函数执行体testhook.tap("plugin3");testhook.call(compilation,"mytest3");//重新调用CALL_DELEGATE生成函数执行体现在让我们看看第三个问题,调用this.compile方法时,实际上会调用HookCodeFacotry.js中的setup方法:
setup(instance,options){ instance._x=options.taps.map(t=>t.fn);}对于问题四,实际上这和V8引擎的HiddenClass有关,通过在构造函数中绑定这些方法,类中的属性形态固定,这样在查找这些方法时就能利用V8引擎中HiddenClass属性查找机制,提高性能。
HookCodeFactory.js主要逻辑:
classHookCodeFactory{ constructor(config){ this.config=config;this.options=undefined;this._args=undefined;}create(options){ this.init(options);letfn;switch(this.options.type){ case'sync':fn=newFunction(...)breakcase'async':fn=newFunction(...)breakcase'promise':fn=newFunction(...)break}this.deinit();returnfn;}setup(instance,options){ instance._x=options.taps.map(t=>t.fn);}...}手写tapable每个Hook手写tapable中所有的hook,并比较我们自己实现的hook和官方的执行时间
这里面每个文件都会实现一遍官方的hook,并比较执行时间,以SyncHook为例,批量注册个插件时,我们自己手写的MySyncHook执行时间0.ms,而官方的需要6ms,这中间整整倍的差距!!!
具体可以看我的仓库
原文:/post/手写作业/文件太多不想写?手写模拟器测试版发布啦~
用途:用于模拟手写文字,目前已发布测试版。测试版效果如下:
声明:本程序目前更新至测试版,无任何商业用途。程序的进步和完善离不开每个人的反馈。感谢大家的支持!
最新版本获取方式:关注微信公众号:IamZLT,后台回复:手写模拟器,即可获取。
目前包含的功能:
下一步完善的功能:
界面整体布局:
菜单栏,包含关于、教程、反馈和捐赠,均跳转相关网页链接。在关于里会说明技术支持与版本更新情况,所属行业指数走势源码后续会附上源代码供进一步完善。软件何如使用可点击教程进行查看。软件的进步和完善离不开大家的反馈和建议,若遇到BUG或有意见和建议可以在反馈中进行评论,会及时回复。软件使用不收取任何费用,若想支持和鼓励软件的进一步完善,可在捐赠中支持我们。
地址栏,包含选择文件、保存路径、选择背景和选择字体,均为选择系统目录或文件。选择文件:即选择要模拟手写的文字内容,目前支持doc和docx的文字读取(不支持,为了最后导出没有错误,文件内尽量不要包含)。保存路径:即要导出时保存的路径。选择背景:选择纸张背景,支持jpg(JPG)、png格式。注意纸张背景最好垂直拍摄从而避免透视导致的问题(后续版本中会提供多种纸张)。选择字体:目前软件内置一种字体,若选择其他字体,请在此选择,注意字体后缀为ttf。
参数栏,负责手写字的参数设置。默然参数为默认背景下较为合适的参数。若选择默认背景,不需要进行过多的调节。字体:选择内置字体(目前只有一种),也可在地址栏中自定义字体。普通值:大小即手写字的大小(越大分辨率越高)。间距值,包括文字距上下左右边框的距离、字与字的距离、行与行之间的距离。扰动值:在0到设置值之间选取随机值增加到固定参数值。若字间距为2,字间距扰动为2,则每个字间距会在2到4之间随机选择。预览:预览的字数,默认值为,可手动修改,请不要设置过多避免预览时间过长。若预览的字数超过一页,最终只会预览出第一页。开源的易语言源码预览得到的会保存在保存路径内,并显示在预览栏中。其他参数:抗锯齿、滤镜。目前还不支持这两个功能。若字体设置过小,锯齿很明显。软件可以通过相关设置减少锯齿。滤镜为了手写字和背景更加统一,推荐在手机app内自己调节。
预览:在设置好路径文件,以及相关参数后,可手动点击预览,在预览栏内查看第一页效果,不支持实时预览。在预览过程中请耐心等待,防止代码中线程编写可能存在的逻辑BUG导致错误。预览完成时会有相关提示框。导出:若效果满意或想直接查看全部,可点击导出,导出的文件为png格式,保存路径为地址栏中的保存路径。导出运算量可能较大,请耐心等待,导出完成时会有相关提示栏。
前端模板引擎之mustache手写实现
模板引擎是将数据按照特定方式转化为视图(HTML)的技术。以 Mustache 为例,它是一款轻逻辑的前端模板引擎,允许处理 HTML、配置文件和源代码。Mustache 的语法简洁,使用 { { 和 }} 作为标记,仅包含占位符来表示动态数据,不包含如 if、else 和 for 循环等逻辑结构。
Mustache 模板引擎的核心原理是替换:将模板中的占位符替换为数据。它广泛应用于前后端分离架构中,与流行前端框架 Angular、React 和 Vue 相配合。Mustache 以稳定性和经典性著称。
安装 Mustache 模块后,只需几行代码即可使用。例如,将模板文件与数据传入引擎,即可生成 HTML 结果。模板中使用 { { 和 }} 包围的占位符会被实际数据替换。须注意的是,Mustache 不支持循环和条件语句。
对于列表渲染,须使用 { { #condition}}...{ { /condition}} 和 { { #list}}...{ { /list}} 语法。这些标记允许根据条件和迭代列表来渲染内容。例如,渲染一个包含商品列表的页面。
在使用 Mustache 的过程中,引擎将模板文件解析为 token,然后结合数据渲染为 HTML 字符串。token 是连接模板和数据的关键,它将文本、占位符和控制结构转换为易于操作的格式。
解析过程通过遍历模板字符串,查找并提取 { { }} 包围的数据,最终生成 token 数组。对于嵌套循环,引擎将整个循环部分作为单个 token,然后进一步解析为子 token。处理此场景时,需要使用栈来区分循环部分与非循环部分。
实现从模板到 token 的过程时,首先扫描模板字符串,提取 { { }} 包围的内容,并根据文本类型和循环标志将数据封装进 token 数组。在实现嵌套循环处理时,使用栈来管理循环块,确保正确解析循环内容。
完成 token 生成后,通过将 token 与数据结合,利用 renderTemplate 函数生成对应的 HTML 结果。最终结果通过传入模板文件和数据至 Mustache 类,可生成匹配的 HTML 字符串,并将其展示在界面上。
学习vue源码(9)手写代码生成器
深入学习 vue 源码的系列文章中,我们探讨了模板编译的解析器与优化器部分。在本文中,我们将聚焦于代码生成器的实现原理与操作流程,以实现从 AST(抽象语法树)到 render 函数代码字符串的转换。
代码生成器在模板编译流程中承担着至关重要的角色,其核心任务是将由解析器和优化器处理得到的 AST 转换为可执行的 render 函数代码字符串。这一过程主要通过调用一系列预定义的函数(如 _c、_v、_s)来构建动态代码片段,从而实现模板的动态渲染。
具体而言,代码生成器依据 AST 结构,递归地生成代码片段。对于一个简单的模板,代码生成器会调用 _c 来创建元素,_v 来创建文本节点,而 _s 则用于返回字符串值。这些函数的调用构建了 render 函数的核心逻辑,实现了模板的动态渲染。
解析器负责将模板字符串转换为 AST,例如将上述简单的模板转换为对应的 AST 结构。通过调用代码生成器,可以将 AST 转换为可执行的 render 函数代码字符串。生成后的代码字符串中包含了 _c、_v、_s 等函数调用,这些函数对应着动态创建元素、文本节点以及返回字符串值的操作。
理解代码生成器的关键在于,它如何根据 AST 结构构建渲染函数代码。这一过程涉及到对 AST 中元素、文本和属性的遍历与处理,通过调用特定的生成函数(如 genData 和 genChildren)来构建数据和子节点,最终生成完整的 render 函数代码字符串。
在实现细节中,代码生成器会针对 AST 中的不同节点类型,采用不同的处理逻辑。例如,对于没有属性的节点(el.plain 为 true),代码生成器无需执行数据生成逻辑(genData),而直接跳过该步骤。这种处理方式优化了代码生成效率,确保了渲染函数代码的简洁与高效。
综上所述,代码生成器在模板编译流程中起到了关键作用,通过将 AST 转换为可执行的 render 函数代码,实现了模板的动态渲染。这一过程涉及对 AST 的递归遍历、函数调用构建以及特定逻辑的实现,构成了 vue 模板编译的核心机制。深入理解代码生成器的实现原理有助于开发者更好地掌握 vue 模板编译的底层机制,为开发高质量、高效的应用打下坚实的基础。
手写模拟器易语言源代码?
手写模拟器是一个复杂的项目,不容易在易语言中实现,因为易语言主要用于编写桌面应用程序,而模拟器通常需要底层硬件访问和复杂的逻辑处理。以下是一个非常简化的示例,用易语言编写的模拟器,用于演示如何模拟一些基本的手写输入。
// 定义一个字符串变量来存储手写内容
手写内容 = ""
// 创建一个GUI窗口
窗口 = CreateWindow(0, 0, , , "手写模拟器", 0)
// 创建一个文本框用于显示手写内容
文本框 = CreateEdit(窗口, , , , , "")
// 创建一个按钮,用于清除手写内容
清除按钮 = CreateButton(窗口, , , , , "清除")
// 创建一个按钮,用于保存手写内容
保存按钮 = CreateButton(窗口, , , , , "保存")
// 创建一个画布,用于手写模拟
画布 = CreateCanvas(窗口, , , , )
// 设置画布背景颜色
CanvasSetBrushColor(画布, RGB(, , ))
CanvasFillRect(画布, 0, 0, , )
// 处理按钮点击事件
OnButtonClicked(清除按钮, 清除内容)
OnButtonClicked(保存按钮, 保存内容)
// 处理鼠标移动事件,模拟手写
OnMouseMove(画布, 手写)
OnMouseLeftDown(画布, 手写)
// 显示窗口
ShowWindow(窗口)
// 事件处理函数:鼠标移动时模拟手写
Function 手写(x, y)
if MouseIsDown(0) then
// 在画布上绘制手写效果
CanvasSetPenColor(画布, RGB(0, 0, 0))
CanvasSetPenWidth(画布, 2)
CanvasLineTo(画布, x, y)
// 将坐标加入手写内容
手写内容 = 手写内容 + "X" + Str(x) + "Y" + Str(y) + ","
end if
End Function
// 事件处理函数:清除手写内容
Function 清除内容()
手写内容 = ""
ClearCanvas(画布)
End Function
// 事件处理函数:保存手写内容
Function 保存内容()
SaveToFile("handwriting.txt", 手写内容)
MessageBox("手写内容已保存到 handwriting.txt 文件中。")
End Function
// 主循环
Do
Sleep(1)
Loop
上面的代码创建了一个简单的GUI窗口,其中包含一个文本框用于显示手写内容、两个按钮(清除和保存)以及一个模拟手写的画布。用户可以在画布上移动鼠标来模拟手写效果,然后通过按钮来清除或保存手写内容。手写内容将保存到名为 "handwriting.txt" 的文件中。
请注意,这只是一个非常基本的手写模拟器示例,实际的手写模拟器会更复杂,涉及到更多的绘图和手写识别算法。此外,易语言在这方面的功能相对有限,因此如果需要更高级的手写模拟器,可能需要考虑使用更强大的编程语言和工具来实现。
Rnote - 简洁优雅、免费开源的草图手绘、手写标注笔记软件,跨平台支持 windows / mac / linux 系统
Rnote,一款专为电脑打造的简洁而强大的手绘、手写标注工具,它支持Windows、Mac和Linux系统,让你的笔记创作不再受平台限制。它以矢量图形为基础,提供流畅的手写体验,用户可以根据个人习惯调整画笔压力,实现精确的绘图和注释。
Rnote的界面设计简洁直观,无论是做PDF书籍的标注,还是绘制专业设计草图,都非常得心应手。它支持多种形状和图表插入,以及高度可定制的纹理笔触,让你的笔记和设计工作更加专业且清晰。最令人惊喜的是,由于基于矢量图形,无论放大多少倍,笔记始终清晰无损,无论是导出PDF还是SVG,都能保持高质量输出。
此外,Rnote的无限画布功能给予用户无尽的创作空间,让你可以自由地记录和表达想法。虽然其官网在国内访问可能不稳定,但可以通过Github下载Windows版本,Mac版本则需从开发者编译的项目中获取,我会提供已打包的最新版本供你下载,只需注意选择适合你的芯片版本。
最重要的是,Rnote作为一款开源软件,遵循GPL-3.0协议,完全免费且开放源代码,你可以直接从Github获取并使用。如果你想体验这种高效又便捷的手写笔记方式,Rnote无疑是一个绝佳的选择。
手写 p-limit, 行代码实现并发控制
前端代码中,经常涉及异步逻辑的处理,这类逻辑可能串行执行,也可能并行执行。并行执行的逻辑通常需要进行并发控制,这是编程中常见需求,也是面试题的常考点。
通常我们会使用 p-limit 这样的工具来实现并发控制,例如,下面这段逻辑就是几个异步操作同时执行,且最大并发数限制为2。
那如何自行实现并发控制功能呢?
首先,需要创建一个函数,该函数接收并发数量参数,并返回一个添加并发任务的函数,我们将其命名为 generator。
在 generator 中,我们将添加的并发任务放入队列中,同时记录当前执行中的异步任务数量。
当任务入队后,会检查是否达到了并发上限,如未达到,继续执行更多任务。
具体实现逻辑如下,当任务执行时,计数并改变返回的 promise 状态,然后执行完成后,减少活跃任务数量并执行下一个任务,以此确保并发数限制。
现在,我们有了一段仅行代码的并发控制实现。
接下来,通过测试代码验证其效果。
测试代码使用 setTimeout 和 promise 实现,设置不同的延迟时间,并发数设置为2。经过测试,结果符合预期:首先并发执行前两个任务,当第一个任务执行完成2秒后,又执行了一个任务,再过一秒,所有任务执行完毕,同时执行了两个任务。
通过测试,我们确认实现了并发控制功能。
回顾整个实现过程,其实就是在队列中保存任务,初始时一次性执行最大并发数的任务,然后每完成一个任务即执行下一个。
此实现实现过程相对简单,但可以进一步优化,比如暴露并发数、提供任务队列清理功能等。
优化后的代码如下,使用 Object.defineProperties 定义只读属性 activeCount 和 pendingCount,并提供清理任务队列的函数。同时,对传入参数进行校验,确保其为整数且非负,Infinity 亦被允许。
优化还涉及确保并发数量准确控制,确保在所有微任务执行完毕后再获取 activeCount。这可以通过在关键逻辑中加入 await Promise.resolve() 实现。
实现并发控制功能的完整代码已通过余行代码实现,这便是 p-limit 源码的简化版本。感兴趣的同学可以自行尝试实现。
总结,js 代码在处理异步逻辑时,常需实现串行、并行执行,并进行并发控制。通过队列管理任务,初始时批量执行最大并发数的任务,每完成一个任务即执行下一个,确保并发控制的实现。此外,确保获取任务数量的准确性,需要在所有微任务执行完毕后获取 activeCount。通过余行代码即可实现并发控制功能,这与 p-limit 的实现原理相似,有兴趣的开发者可以自行尝试。