前言本文讨论的核心基础概念是EventLoop,它是JavaScript的执行模型,是异步编程的核心。在不同的平台上有不同的实现。浏览器基于HTML5规范实现自己的实现,而NodeJS基于libuv核心。虽然都实现了异步通知的效果,但是运行规则还是有些区别的。网上关于“事件循环”的文章很多,本文不再赘述。阅读本文前请准备好背景知识。重点是NodeJS运行的6个阶段以及宏任务队列(MacrotaskQueue)和微任务队列(MicrotaskQueue的基本概念)。本人能力有限,有不足之处请批评指教。本文要解决的问题是在NodeJS中运行全同步代码后先运行microtasks,还是先运行macrotasks?不同版本的NodeJS运行微任务的时机有区别吗?NextTickQueue和OtherMicroQueue都是NodeJS中的微任务队列。它们的运行时间有什么不同?微队列是清空一次,还是每个周期运行一个任务?例如console.log("start")setTimeout(()=>{console.log(1)Promise.resolve().then(()=>{console.log(2)})setTimeout(()=>{console.log(3)})setImmediate(()=>{console.log(4)})process.nextTick(()=>{console.log(5)})})setTimeout(()=>{console.log(6)Promise.resolve().then(()=>{console.log(7)})setTimeout(()=>{console.log(8)})setImmediate(()=>{console.log(9)})process.nextTick(()=>{console.log(10)})})process.nextTick(()=>{console.log(11)})console.log("结束")说明上面的例子基本可以解决上面的问题。每个回调函数只会打印出一个数字,也相当于函数的编号;全局“开始”和“结束”标记全局同步代码运行的开始和结束。示例中属于宏任务的API:setTimeout()setImmediate()示例中属于微任务的API:process.nextTick()会进入NextTickQueuePromise会进入OtherMicroQueue不同NodeJS版本的运行结果只选择稳定版供测试用;为了节省空间,将原来的竖排打印结果横排显示。setTimeout和setImmediate的执行顺序不一定在同一个模块,所以“49”和“38”多次运行的结果可能会互调。由于这是一个宏任务,这里就不讨论了。v6.17.1开始结束1116510274938v8.17.0开始结束1116510274938v10.23.0开始结束1116510274938v12.20.0开始结束1115261074938v14.15.3startend1115261074938结果分析根据以上结果,回答上述问题。由于11是紧跟在全局代码之后执行的,因此可以知道,微任务会在全局同步代码运行后开始运行。不同版本的NodeJS在运行上存在差异。以v10.23.0的结果为分界线,会有两个明显的结果(经过进一步验证,这个变化是从v11.0.0开始的)。下面将详细分析造成这种差异的原因。从v10.23.0的结果来看,“510”先于“27”执行,可见NextTickQueue优先于OtherMicroQueue。从v10.23.0的结果来看,在“27”处连续出现了“510”,可见微队列是一次性清空的。从同样连续出现的“49”和“38”也可以看出,宏队列也有清空一次的属性,只是针对不同的API属于不同的队列。分析新旧版本的区别,以v11.0.0版本为分界线。高于或等于v11.0.0的版本称为新版本;低于v11.0.0的版本称为旧版本。这里,宏任务队列不是讨论的重点,进行了简化。老版本运行进程运行全局同步代码:print"start";1进入宏任务队列;6进入宏任务队列;11进入微任务队列的NextTickQueue;打印“结束”。Macrotaskqueue:[1,6]NextTickQueueofmicrotaskqueue:[11]OtherMicroQueueofmicrotaskqueue:[]Runmicrotaskqueue,output11.Macrotaskqueue:[1,6]NextTickQueue微任务队列:[]OtherMicroQueue微任务队列:[]从宏任务队列中取出1,运行:print"1";2进入微任务队列的OtherMicroQueue;3进入宏任务队列;4进入宏任务队列;5进入微任务队列的NextTickQueue。宏任务队列:[6,3,4]NextTickQueue微任务队列:[5]OtherMicroQueue微任务队列:[2]从宏任务队列中取出6运行:print"6";7进入微任务队列8进入宏任务队列;9进入宏任务队列;10进入微任务队列的NextTickQueue。Macrotaskqueue:[3,4,8,9]NextTickQueueofmicrotaskqueue:[5,10]OtherMicroQueueofmicrotaskqueue:[2,7]Runmicrotaskqueue:clearNextTickQueue,print“5”、“10”;清除其他微队列,打印“2”、“7”。Macrotaskqueue:[3,4,8,9]NextTickQueueofmicrotaskqueue:[]OtherMicroQueueofmicrotaskqueue:[]后续运行的宏队列,此处省略。新版本在运行过程中运行全局同步代码:print"start";1进入宏任务队列;6进入宏任务队列;11进入微任务队列的NextTickQueue;打印“结束”。宏任务队列:[1,6]NextTickQueue微任务队列:[11]OtherMicroQueue微任务队列:[]运行微任务队列,输出11。宏任务队列:[1,6]NextTickQueue微任务队列:[]OtherMicroQueue微任务队列:[]从宏任务队列中取出1,运行:print"1";2进入微任务队列的OtherMicroQueue;3进入宏任务队列;4进入宏任务队列;5进入微任务队列的NextTickQueue。Macrotaskqueue:[6,3,4]NextTickQueueofmicrotaskqueue:[5]OtherMicroQueueofmicrotaskqueue:[2]Runmicroqueue:clearNextTickQueue,print"5";清除其他微队列,打印“2”。宏任务队列:[6,3,4]NextTickQueue微任务队列:[]OtherMicroQueue微任务队列:[]从宏任务队列中取出6运行:print"6";7进入微任务队列MicroQueue的Other;8进入宏任务队列;9进入宏任务队列;10进入微任务队列的NextTickQueue。Macrotaskqueue:[3,4,8,9]NextTickQueueofmicrotaskqueue:[10]OtherMicroQueueofmicrotaskqueue:[7]Runmicroqueue:clearNextTickQueue,print"10";清除其他微队列,打印“7”。Macrotaskqueue:[3,4,8,9]NextTickQueueofmicrotaskqueue:[]OtherMicroQueueofmicrotaskqueue:[]后续运行的宏队列,此处省略。差异总结差异出现在第四步。老版本从宏队列中取出任务执行,新版本处理微任务。所以我们可以得出一个结论:老版本会清空宏任务队列,然后运行微任务;而新版本会在每次运行宏任务时清空微任务队列。另外网上有传说,微任务队列的深度是有限制的。好像限制是1000,所以在这里验证一下。使用以下示例(一次填充10000个微任务):console.log('start')leta=0while(a<10000){process.nextTick(()=>{console.log(111)})a+=1}setTimeout(()=>{console.log(123)})console.log('end')在不同版本(v6到v12)下都能流畅运行,得到相同的结果:startend111x10000123结论:没有微队列的深度限制,但是微任务过多会导致系统一直在运行微任务而无法运行其他任务。比如例子中的宏任务中的123会在10000个微任务运行完毕后运行。体验上有明显的延迟,所以出于性能考虑,不应该大量使用microtasks。
