本文将通过两个对比实验来探讨这一主题,并分析执行顺序的不确定性及其影响因素。
实验1: 无同步任务的情况下,Timer与I/O的执行顺序
const fs = require("fs");
setTimeout(() => {
console.log("setTimeout");
}, 0);
fs.readFile(__filename, () => {
console.log("readFile");
});
执行结果
实验的执行结果显示,setTimeout和readFile的执行顺序并不固定。
原因探讨
经查阅 chromium V8 中 DOMTimer.cpp源码发现: setTimeout(fn, 0)实际上会被Node.js设置为setTimeout(fn, 1)
进一步解释
V8引擎写了 setTimeout如果是0ms 最小按照1ms来执行,Nodejs依然需要计算1ms是否已经过去, 分为2种情况:
- 如果cpu不繁忙,事件循环在0.05ms进入Timer阶段,Timer的回调函数不会立即回到队列,此时控制权将移交给I/O queue,因此优先输出readFile;在下一次事件循环中,Nodejs发现1ms已经过去,继续执行setTimeout;
- 如果cpu繁忙,事件循环在1.05ms进入Timer阶段,此时1ms已经过去,Timer的回调函数已经在Timer queue了,因此优先输出setTimeout,随后输出readFile。
影响因素
影响Timer和I/O执行顺序的因素包括:
- 进程性能:Node.js进程的当前负载会影响事件循环的执行速度,进而影响Timer和I/O的执行顺序。
- 其他应用程序:当前机器上其他正在运行的应用程序可能会占用CPU和I/O资源,从而影响Node.js进程的性能。
- Node.js事件循环本身:事件循环的内部机制,包括不同阶段的执行顺序和调度策略,也会影响Timer和I/O的执行顺序。
实验2: 有耗时同步任务的情况下,Timer与I/O的执行顺序
const fs = require("fs");
setTimeout(() => {
console.log("setTimeout");
}, 0);
fs.readFile(__filename, () => {
console.log("readFile");
});
for (let i = 0; i < 2000000000; i++) {}
执行结果
实验的执行结果显示,setTimeout和readFile的执行顺序是固定的。
进一步解释
加了同步任务后,根据node的事件循环机制,优先执行用户输入的代码(即同步任务),执行完毕后,此时进入Timer阶段已经过去了1ms,Timer的回调函数已经在Timer queue了,因此优先输出setTimeout,随后输出readFile。这是确定的,