user2690527 user2690527 - 4 months ago 21
Javascript Question

Order of events in JS event queue

Things (I believe to) understand

Despite the fact that modern browsers are multi-threaded, JavaScript is executed single-threaded and it is guaranteed to be executed synchronously. This means if the call stack is entered the JS runs uninterruptedly until the call stack becomes empty again. Especially, it is guaranteed that no two JS scripts run concurrently.

However, JavaScript is event-driven. According to the description of the concurrency model at MDN the browser maintains a queue of events. If idle the browser takes the first event from the queue and executes the associated piece of JS (if any). After the script terminates, the browser gains control again and proceeds with the next event. While a JS script is executed the browser does not do anything else, especially it does block the UI. The latter is responsible for the infamous "long-running script warning".

There are some truly asynchronous functions like

setTimeout
that are provided by the run-time system (aka browser). They can be used to put a new event on the event queue and the associated callback will be put on the call stack if the event has been triggered and the call stack is empty. A developer cannot create his "own" asynchronous functions without using one of the native asynchronous functions.

This being said,
setTimeout( someFunction, 0 )
provides an option how to split up a long-running piece of code that would otherwise trigger the "long-running script warning" into smaller pieces. If the call stack becomes empty the browser can catch up with other events (like UI) before executing the next piece of code.

An example

To check if I am right I created this piece of code

"use strict";
for( var i = 0; i != 5; i++ ) {
confirm( 'This is #: ' + i );
setTimeout(
( function(j) {
return function() { confirm( 'This is #: ' + j ); };
} )( i+10 ),
0
);
}


Expected results

I expected the alert boxes to come up in correct order: 0, 1, 2, 3, 4, 10, 11, 12, 13 and 14. My idea was that in every iteration the following two steps happen: (a) The first confirmation box is displayed synchronously. (b) A new event is pushed to the end of the event queue. However, the callback is not called yet but postponed, because the call stack is not empty as the loop is still running.

Hence, I expected to see the alerts 0, 1, 2, 3, 4 first in that order, because this is part of the synchronous loop. Then the loop terminates and the call stack becomes clear. The next event is taken from the event queue which happens to be the first asynchronous timeout. So I expected to see 10 next. After the confirmation dialog is closed the call stack becomes clear again. So I expected to see 11 next. And so on.

Actual results

The alert boxes are shown in mixed order, e.g. 0, 1, 2, 10, 11, 3, 12, 4, 13, 14.

Question

Why are the alert boxes in mixed order? If at least the synchronous part (0, 1, 2, 3, 4) was displayed en block and only the callbacks were mixed up, I would have accepted that the event queue does not guarantee an order of event. However, it seems that even the synchronous loop gets interrupted and interleaved with the asynchronous events. I thought this is guaranteed not to happen. What is going on here?

Answer

This is because in some browsers (Firefox in your case), the traditionally blocking alert, confirm, and prompt functions do not stop the event queue. They do stop execution of the current piece of code until the user interacts with it, but the loop continues to run and some JavaScript events continue to fire (not all events though).

Is this a Firefox bug?

Nope, your code just has undefined behavior.

In the spec, pausing is optional:

Optionally, pause while waiting for the user to acknowledge the message.

Firefox chooses not to in order to improve the user experience.

Another example:

Here is a simple example showing how resize events still fire. This shows how you can't be sure a variable accessible to an event handler won't be modified while an alert-type dialog is open.

(function(){
    'use strict';
    var resized = false;
    window.addEventListener('resize', function() {
        resized = true;
        console.log('resizing');
    });
    alert('resize the window before continuing');
    console.log('Resize events fired while alert open?: ' + resized);
})();
Comments