我有以下代码来演示此问题:
let count = 5;
while (count--) {
setTimeout(() => {
console.log('timeout');
process.nextTick(() => {
console.log('tick');
});
}, 0);
}
const largeNumber = 20000;
for (let i = 0; i < largeNumber; i += 1) {
for (let j = 0; j < largeNumber; j += 1) {
// do nothing here, just be sure all the setTimeout callbacks are in the queue when exiting sync code
}
}
我期望的输出如下:
timeout
tick
timeout
tick
timeout
tick
timeout
tick
因为事件循环检查timeouts
队列,它发现了第一个setTimeout
回调,运行它,然后检查nextTick
队列。而对于进一步的setTimeout
回调,它应该做同样的事情。
但我得到以下输出:
timeout
timeout
timeout
timeout
timeout
tick
tick
tick
tick
tick
为什么?
setTimeout
和nextTick
将各自在一个函数队列上放置一个函数,以便稍后调用。
当JavaScript事件循环不忙于执行其他操作时,它将查看该函数队列以查看是否有任何操作。
当第一个超时函数运行时,它使用nextTick
将一个函数放在队列的末尾(由于尽快运行)。
但是,队列中的下一个函数是由setTimeout
放置的下一个函数,它已经到期,因此它首先运行(依此类推)。
这是由于Deduplication:
对于
timers
和check
阶段,对于多个immediates和计时器,C到JavaScript之间存在单个转换。这种重复数据删除是一种优化形式,可能会产生一些意想不到的副作用。
您的代码显示重复数据删除优化导致的“意外副作用”。
实际上,doc中的示例与示例代码非常相似。他们使用setImmediate
而不是setTimeout
,但概念是相同的:
当在check
阶段有多个计时器事件等待时,Node
会在处理nextTickQueue
之前处理所有这些事件。
所以因为所有的setTimeout
调用都使用0
的超时,所以回调都会同时在队列中结束,并且由于重复数据删除优化,Node
会处理所有这些,这会导致'timeout'
被打印5次。一旦所有的setTimeout
回调运行,Node
处理nextTickQueue
,导致所有五个process.nextTick
回调运行,导致'tick'
被打印5次。
请注意,如果您引入一个微小的变量delay
,那么在相同的check
阶段,计时器事件不会在队列中结束,您将避免重复数据删除优化并获得您期望的输出:
let count = 5;
let delay = 0;
while (count--) {
setTimeout(() => {
console.log('timeout');
process.nextTick(() => {
console.log('tick');
});
}, delay += 1); // use a tiny variable delay
}
const largeNumber = 20000;
for (let i = 0; i < largeNumber; i += 1) {
for (let j = 0; j < largeNumber; j += 1) {
// do nothing here, just be sure all the setTimeout callbacks are in the queue when exiting sync code
}
}
输出:
timeout
tick
timeout
tick
timeout
tick
timeout
tick