事件循环
今天想来分享一下nodejs的type: "module"对事件循环的影响。
先来复习一下事件循环基本知识。
总体是 同步 --> 异步 、 宏任务 --> 微任务 (宏任务分为异步和同步, 同步宏 --> 微 --> 异步宏 依次循环)
细节是 同步任务(script) -----> 清空 微任务 队列 -----> 宏任务 ......循环
注意是清空微任务,如果在微任务的时候再次加入微任务,会继续执行微任务
如果是宏任务过程中发现有微任务 ,就算是当前的宏任务队列还有在等待的,也会先把微任务先搞完才会去下一个宏任务
有哪些宏任务和微任务?
宏任务:
- script (可以理解为外层 同步 代码)
- setTimeout/setInterval
- UI rendering/UI事件
- postMessage,MessageChannel
- I/O操作、setImmediate (Node.js)(setImmediate是什么?setImmediate优先级比setTimeout高 )(因为定时器其实不可能0ms,所以会比setImmediate晚)
微任务:
- Promise.then
- queueMicrotask(() => {}) 创建一个微任务
- Object.observe(已废弃;Proxy 对象替代)
- MutaionObserver
NodeJS的nextTick队列:
- process.nextTick(Node.js) (有独立的队列,比微任务早执行)
官方原话:在Node.js事件循环的每一轮中,process.nextTick()队列总是在微任务队列之前处理
出现问题 - 在NodeJS中,运行这段代码
其实今天的重点不是上面的内容,通过上面的知识,我们试着来运行下面这段代码:
js
console.log('1');
setTimeout(function () {//time1
console.log('2');
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {//then1
console.log('5')
})
process.nextTick(function () {//next1
console.log('3');
})
})
process.nextTick(function () {//next2
console.log('6');
})
new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {//then2
console.log('8')
})
setTimeout(function () {//time2
console.log('9');
process.nextTick(function () {//next3
console.log('10');
})
new Promise(function (resolve) {
console.log('11');
resolve();
}).then(function () {//then3
console.log('12')
})
})
根据我们常规的分析,nextTick比微任务先执行,得到的结果应该是1 7 6 8 2 4 3 5 9 11 10 12
当package.json的type不设置,或者设置为"commonjs"的时候,输出结果确实符合我们的想法
但是当设置type:"module"的时候,输出结果却变了,得到1 7 8 6 2 4 3 5 9 11 10 12
问题分析与解决
大家可以多尝试几段 Promise.then 与 process.nextTick 同时出现的代码,会发现都是和预期不一致。
我们可以简单的认为是"module模式下微任务比then优先级高"吗?
不行! 再次回到上面的代码观察,会发现上面其实 出现了多次"Promise.then 与 process.nextTick 同时出现" ,但是最终只有最外层的发生了顺序变化。也就是说,只有最外层的微任务和nextTick出现了顺序不对的问题。
通过和大家一起查阅多个资料,最终原因其实是: 在ESM模式下,代码其实是运行在async/await下的
也就是说,处理后的ESM代码,其实长这样
所以,在ESM运行时,代码其实是在微任务阶段中,必须清空完微任务队列,才会轮到nextTick。 而CommonJS是同步运行的,所以得到的是预期结果
这也可以很好的解释为什么只有最外层的nexttick有顺序问题,实际上还是和官方文档说的规则一样:在Node.js事件循环的每一轮中,process.nextTick()队列总是在微任务队列之前处理,最外层的代码是在微任务中,当然轮不到nextTick。而在setTimeout中的nextTick就会比Promise.then优先执行。
现在再回来看这段代码:
js
// CommonJS模式 - 普通分析即可
// 在esm模式中, 由于esm会被包裹在await后执行,所以相当于第一轮代码执行是微任务,这时候的NextTick需要等待微任务清空完成后才能执行。所以先输出 8 再 6。
// 后续在time1中又遇到了nextTick和微任务同时出现的情况,这时候就是普通宏任务环境了,所以 nextTick优先级高于微任务, 先输出 3 再 5
const test = () => {
console.log('1');
setTimeout(function () {//time1
console.log('2');
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {//then1
console.log('5')
})
process.nextTick(function () {//next1
console.log('3');
})
})
process.nextTick(function () {//next2
console.log('6');
})
new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {//then2
console.log('8')
})
setTimeout(function () {//time2
console.log('9');
process.nextTick(function () {//next3
console.log('10');
})
new Promise(function (resolve) {
console.log('11');
resolve();
}).then(function () {//then3
console.log('12')
})
})
}
总结
当面试时遇到了事件循环问题,如果遇到了nextTick,把CommonJS和ESM的区别说清楚,相信会给你带来额外加分
参考文献: