var f1 = function () { setTimeout(function(){ console.log("f1", "First function call..."); }, 0); }; var f2 = function () { console.log("f2", "Second call..."); };and then you call them:
f1(); f2();
Checking your console, this is what you see:
If you were expecting to see "f1 First function call..." first, then you are not alone. This is what I initially expected based on my assumption that having a setTimeout set to 0 milliseconds, well mean, execute right away. Apparently this is not how things work.
This post is about how this unexpected behaviour led me into having a refresher of some of the core concepts regarding how JavaScript executes.
I and a colleague at work, were implementing a piece of functionality which basically translates to: "Create a function such that if that function is called with a delay flag being true, have a timeout for a line within the function, if not, then call that line immediately". Something like this:
var task = function (args1, args2, delayFlag) { ... ... ... if (delayFlag) { setTimeout(function(){ doThis(); },500); } else { doThis(); };
I wanted to eliminate the need for the if/else to make the code more succinct by doing:
setTimeout(function(){ doThis(); }, delayFlag? 500 : 0);
And this is when I got to realise that this, may screw things up, and alter the order of execution in the part of the code that would be calling the defined task() function, and that having setTimeout() to 0, is not exactly the same as calling the function immediately.
Ok. Then it is obvious I've got something new to learn here. So I decide to dig more into why this is the case. A couple of researching got me the reason behind this behaviour. And to understand why things tie up this way, I needed to revisit what it meant by JavaScript being single threaded, non blocking I/O and has an asynchronous event queuing model for code execution.
JavaScript being single threaded...
When Javascript runs, it runs in a single thread of execution. Which means there can only be one path of code execution at any point in time. I like to imagine my JavaScript code as a series of commands, and there is one invincible miniature goblin that follows through the series of commands and execute it one after the other. There can be only one of this miniature goblin. So if a particular task in the series of commands take more effort, the goblin would have to spend more time executing it. Unlike a Multithreaded language like Java, which can spurn off countless goblins and split tasks between them, JavaScript is stuck only with one.
So JavaScript runs in a single thread of execution. Check.
JavaScript being non blocking I/O...
In this context, I would like to see I/O operations as operations that changes the state of stuff outside the JavaScript environment. So apart from the more common I/O operations like reading/writing to files system/database, reading/writing to network resources etc. I would include DOM Manipulation too as an I/O operation. As the DOM is apart from JavaScript execution but JavaScript can be used write to the DOM and read value from the DOM.
That being said, it should be noted that I/O operations are generally expensive and takes time to completion.
So by JavaScript being non blocking I/O, it means that when it performs I/O operations, the single thread of execution does not pause and wait for the ongoing I/O operations to complete before moving on to the next task. This is possible because JavaScript makes use of callbacks for I/O operations. Which are set of instructions that can be predefined to execute whenever an I/O task completes (or in case of an Event, when an event occurs. Or in case of a Timer, when the delay expires).
When an I/O operation is defined, you always have the option to define its callback. So when the single thread encounters such an I/O instruction, it starts the execution, takes the callback for the task (if defined) and registers it to be pushed into an event queue when done and it moves on to the next task in line.
Example, uploading a file may look like this: The process starts and the uploadFinish() is queued up to execute when the uploading finishes. The JavaScript execution does not need to wait for the the file to finish uploading before continuing.
// JavaScript lines of code to execute .... .... var uploadFinish = function() { // instructions to fire when upload finishes }; var XHR = new XMLHttpRequest(); // registers the callback XHR.addEventListener('load', uploadFinish, false); XHR.open('POST', '/upload'); // starts the I/O operations XHR.send(new FormData(document.getElementById('uploadform'))); // Since I/O is non blocking, execution of instructions continues // JavaScript to execute continues. ... ...
The ability to start I/O operations, register their callbacks and move on to the next line of instruction, gives JavaScript amazing super powers for handling high I/O operations gracefully.
This callback model also applies to events. Where the event listeners are callbacks. So when a particular event occurs, say a user clicking on a button, a block of JavaScript can be registered as event listener to that button and set to execute if and only if the button is clicked:
var button = document.getElementById("button"); button.addEventListener("click", function(){ // instructions that should be executed on click // of the button goes here. });
so also for timers. Where a block of code can be queued up for execution after a certain time delay elapses.
var button = document.getElementById("button"); setTimeout(function(){ // instructions that should be executed // after 5 seconds elapses. }, 5000);
So this is what it means to be non blocking I/O. Check.
JavaScript having an asynchronous event queuing model for code execution
Apart from the code that you write which JavaScript executes sequentially, there is also another set of Instructions that JavaScript need to execute sequentially: The set of callbacks in the event queue. I like to think of it as JavaScript single thread of execution first running through the lines of code, executing the instruction it finds and stacking up the event queue with callbacks. After this is done, the single thread of execution then proceeds to execute the instructions in the event queue as necessary. The asynchronous nature comes from the fact that the order for queuing the callback is not based on the order they are defined, but rather based on when they returned. An operation defined later in the code may be completed earlier than the one defined at the top of the code. In this case the later callback would be queued up before the earlier one and it would be executed first.
Check.
so what about the Effect of setting SetTimeout to 0?..
let's see the code snippet again:
Two functions:
var f1 = function () { setTimeout(function(){ console.log("f1", "First function call..."); }, 0); }; var f2 = function () { console.log("f2", "Second call..."); };
and then you call the functions:
f1(); f2();
Having covered these basic parts of JavaScript, I found out the reason behind the counter intuitive behaviour of having the delay of setTimeout set to 0: the behaviour which led me down the path of having a refresher in the first place. And this was the fact that: it is not possible to have setTimeout with a time delay of 0 milliseconds. The Minimum value is determined by the browser and it is not 0 milliseconds. Historically browsers sets this minimum to 10 milliseconds, but the HTML5 specs and modern browsers have it set at 4 milliseconds. See MDN
With this fact, and everything else, it all makes sense.
When f1() is encountered in the path of execution, the function is called and in it a setTimeout is called which has a callback which would be queued up for execution after 0 seconds has expired. Which should translate to immediate, but since the actual minimum is 4 milliseconds, the callback function is queued for execution after 4 milliseconds.
In this period, since JavaScript is non blocking, and in this particular case, this is not a I/O operation but a timeout, the preceding line, f2() gets executed. And this is why the output of f2() appears before f1():
Update
Since this post has ended up being one of the popular post on this blog, I feel I should clarify the role the 4 milliseconds has to play so as not to mislead folks who land on this page trying to figure out why the hell setTimeout have such a quirky behaviour! Even though, finding about the 4 ms restriction helped in framing a mental model for this behaviour it is not necessarily accurate and responsible for the behaviour. As Nino Porcino said in the comments, "it doesn't matter the 4ms or minimum time". I think the critical characteristics of JavaScript to understand which leads to this behaviour is the fact that it is single threaded plus the behaviour of its event queue...
Think of it this way: all the code you write have only one chance to get executed by our mythical goblin. If per chance they lose this opportunity, (maybe because they are wrapped in setTimeout) the only option left for them is to just get back in line...in the event queue. The presence of setTimeout means you have lost this chance (even if the time is set to zero) and you have to go queue up again to get executed.
I am writing a book: TypeScript Beyond The Basics. Sign up here to be notified when it is ready.
9 comments:
Great Explanation :)
Ok, got it, thanks, it was what I was looking for. And it doesn't matter the 4ms or minimum time, the goblin has to finishe the current execution line first. For example:
setTimeout(function(){ console.log("hello"); },0);
console.log("1");
for(var t=1;t<10000000000;t++) {var h = Math.sqrt(t); }
console.log("2");
produces
1
2
hello
and not
1
hello
2
Also, this behaviour explains why when using promises (like in $Q) you can chain with .then() even after the deferred call is resolved. This because the actual resolve it's delayed after the end of the current execution batch. It took me a while to understand it.
Good Explaination
Nice explanation - fair play :)
Brilliant explanation.
Thanks for the very good explanation, that helps !
Great explanation but mind you, if the second function was asynchronous, then the functions would have executed in order, f1 first before f2
Great article, but there's still one thing I cannot wrap my head around -- can you please explain in layman terms why I'd need to add a 0s timeout for this when by default the checkbox is in unchecked state?
https://codepen.io/thdoan/pen/PoyxLVO
Post a Comment