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

事件循环

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

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

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

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

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

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

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

有哪些宏任务和微任务?

宏任务:

  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中,运行这段代码

其实今天的重点不是上面的内容,通过上面的知识,我们试着来运行下面这段代码:

    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"的时候,输出结果确实符合我们的想法

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

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

但是当设置type:"module"的时候,输出结果却变了,得到1 7 8 6 2 4 3 5 9 11 10 12

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

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

问题分析与解决

大家可以多尝试几段 Promise.then 与 process.nextTick 同时出现的代码,会发现都是和预期不一致。

我们可以简单的认为是“module模式下微任务比then优先级高”吗?

不行! 再次回到上面的代码观察,会发现上面其实 出现了多次“Promise.then 与 process.nextTick 同时出现”,但是最终只有最外层的发生了顺序变化。也就是说,只有最外层的微任务和nextTick出现了顺序不对的问题。

通过和大家一起查阅多个资料,最终原因其实是: 在ESM模式下,代码其实是运行在async/await下的

也就是说,处理后的ESM代码,其实长这样

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

所以,在ESM运行时,代码其实是在微任务阶段中,必须清空完微任务队列,才会轮到nextTick。 而CommonJS是同步运行的,所以得到的是预期结果

这也可以很好的解释为什么只有最外层的nexttick有顺序问题,实际上还是和官方文档说的规则一样:在Node.js事件循环的每一轮中,process.nextTick()队列总是在微任务队列之前处理,最外层的代码是在微任务中,当然轮不到nextTick。而在setTimeout中的nextTick就会比Promise.then优先执行。

现在再回来看这段代码:

// CommonJS模式 - 普通分析即可
// 在esm模式中, 由于esm会被包裹在await后执行,所以相当于第一轮代码执行是微任务,这时候的NextTick需要等待微任务清空完成后才能执行。所以先输出 8 再 6。
// 后续在time1中又遇到了nextTick和微任务同时出现的情况,这时候就是普通宏任务环境了,所以 nextTick优先级高于微任务, 先输出 3 再 5
const d = () => {
    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')
        })
    })
}
d()

总结

当面试时遇到了事件循环问题,如果遇到了nextTick,把CommonJS和ESM的区别说清楚,相信会给你带来额外加分

参考文献:

  1. 为什么顺序不一致(看评论):NodeJS process.nextTick与commonJs和ESM中的queueMicrotask,执行顺序是什么? _大数据知识库 (saoniuhuo.com)
  2. 为什么ESM是异步的: Node.js 如何处理 ES6 模块 - 阮一峰的网络日志 (ruanyifeng.com)

你可能感兴趣的:(JavaScript,javascript,事件循环,node.js,ESModule,CommonJS)