Reputation: 5253
I have the following code to demonstrate the issue:
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
}
}
The output I expect is the following:
timeout
tick
timeout
tick
timeout
tick
timeout
tick
Because the event loop checks the timeouts
queue, it founds the first setTimeout
callback, runs it, and check the nextTick
queue after. And for the further setTimeout
callbacks it should do the same.
But I get the following output:
timeout
timeout
timeout
timeout
timeout
tick
tick
tick
tick
tick
Why?
Upvotes: 4
Views: 327
Reputation: 45840
This is due to Deduplication:
For the
timers
andcheck
phases, there is a single transition between C to JavaScript for multiple immediates and timers. This deduplication is a form of optimization, which may produce some unexpected side effects.
Your code exhibits the "unexpected side effects" caused by the deduplication optimization.
In fact, the example in the doc is very similar to your sample code. They are using setImmediate
instead of setTimeout
, but the concept is the same:
When there are multiple timer events waiting in the check
phase, Node
processes all of them before processing the nextTickQueue
.
So because all of the setTimeout
calls use a timeout of 0
, the callbacks all end up in the queue at the same time and due to the deduplication optimization, Node
processes all of them which causes 'timeout'
to be printed all 5 times. Once all of the setTimeout
callbacks run, Node
processes the nextTickQueue
which causes all five process.nextTick
callbacks to run which causes 'tick'
to be printed 5 times.
Note that if you introduce a tiny variable delay
so the timer events don't end up in the queue during the same check
phase you will avoid the deduplication optimization and will get the output you were expecting:
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
}
}
Output:
timeout
tick
timeout
tick
timeout
tick
timeout
tick
Upvotes: 1
Reputation: 944150
setTimeout
and nextTick
will each put a function on a queue of functions to call later.
When the JavaScript event loop isn't busy doing something else, it will look at that queue of functions to see if any are due to be run.
When the first timed out function is run, it uses nextTick
to put a function on the end of the queue (due to run as soon as possible).
However, the next function on the queue is the next function put there by setTimeout
and it is already due, so it runs first (and so on).
Upvotes: 2