事件循环 - nextTick与微任务 - 在 NodeJS的ESM模式和CJS模式下,nextTick与.then执行顺序不一致的问题

事件循环

今天想来分享一下nodejs的type: "module"对事件循环的影响。

先来复习一下事件循环基本知识。

总体是 同步 --> 异步 、 宏任务 --> 微任务 (宏任务分为异步和同步, 同步宏 --> 微 --> 异步宏 依次循环)

细节是 同步任务(script) -----> 清空 微任务 队列 -----> 宏任务 ......循环

注意是清空微任务,如果在微任务的时候再次加入微任务,会继续执行微任务

如果是宏任务过程中发现有微任务 ,就算是当前的宏任务队列还有在等待的,也会先把微任务先搞完才会去下一个宏任务

有哪些宏任务和微任务?

宏任务:

  1. script (可以理解为外层 同步 代码)
  2. setTimeout/setInterval
  3. UI rendering/UI事件
  4. postMessage,MessageChannel
  5. I/O操作、setImmediate (Node.js)(setImmediate是什么?setImmediate优先级比setTimeout高 )(因为定时器其实不可能0ms,所以会比setImmediate晚)

微任务:

  1. Promise.then
  2. queueMicrotask(() => {}) 创建一个微任务
  3. Object.observe(已废弃;Proxy 对象替代)
  4. MutaionObserver

NodeJS的nextTick队列

  1. 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的区别说清楚,相信会给你带来额外加分

参考文献:

  1. 为什么顺序不一致(看评论):NodeJS process.nextTick与commonJs和ESM中的queueMicrotask,执行顺序是什么? _大数据知识库 (saoniuhuo.com)
  2. 为什么ESM是异步的: Node.js 如何处理 ES6 模块 - 阮一峰的网络日志 (ruanyifeng.com)
相关推荐
阿伟来咯~21 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端26 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱28 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai38 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨39 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry3 小时前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端4 小时前
Content Security Policy (CSP)
前端·javascript·面试