1

I am learning the concept of asynchronous programming in JavaScript (JS). But, I am having a hard time understanding the same. For the last few days, I had been reading various articles on the internet to understand it, but I am unable to grasp the idea.

So, here are the doubts I have:

setTimeout(function(){ alert("Hello 1"); }, 3000); // .....(i)
console.log("Hi!");                              // .....(ii)
setTimeout(function(){ alert("Hello 2"); }, 2000); // .....(iii)
  1. Consider the above code. I learnt that JS uses a call-stack and an event-queue to order the execution of instructions. In the above code, when the JS interpreter sees the (i) line, it will enqueue that setTimeout into the event-queue, then moves to (ii), puts it in the call-stack, executes it, then moves to (iii), where it again enqueues the setTimeout into the event-queue (and this queue is not empty), right?

  2. If what I had written in the above question is correct, then once we get to the end of the code since the call-stack is empty the setTimeouts enqueued into the event-queue get executed one by one, right? - That means if we assume it took (say) 10ms to come to the end of the code, then since the event-queue has the setTimeout (i) in the front, it waits for 3s, then pops the alert: "Hello 1", at the time = 3010ms, the dequeues it, and similarly the setTimeout (iii) gets executed after 2 more seconds and then the alert: "Hello 2" pops at the time = 5010ms, right?

  3. Let's suppose that instead of setTimeouts at (i) and (iii), we had addEventListener()'s with some call-back functions. Even in this case, will the call-back functions of the event listeners be enqueued in the event-queue? I feel they don't get enqueued because we could have triggered the call-back of (iii), before the call-back of (i). So, what exactly happens in this case? Is there anything else other than the call-stack and event-queue that somehow stores the information about them and triggers their call-backs accordingly?

In a nut-shell how exactly are the instructions ordered? What exactly happens in the background?

I would be really thankful for a comprehensive answer. It would be great if you can also provide links to some comprehensive materials on this topic.

Thank you for the help!

Aditya
  • 195
  • 9
  • setTimeout() doesn't have accumulative delay so the order of output will be ii, iii, i. You can get an accumulative delay by using async functions and your own [sleep function](https://stackoverflow.com/a/39914235/12101554) – Samathingamajig Nov 09 '20 at 17:44
  • I know you already have an answer to your question, but do read my answer, it will definitely add more clarity to your understanding. Goodluck! – Link Nov 09 '20 at 18:23

2 Answers2

1

You are correct up until this point:

That means if we assume it took (say) 10ms to come to the end of the code, then since the event-queue has the setTimeout (i) in the front, it waits for 3s, then pops the alert: "Hello 1", at the time = 3010ms

setTimeout will queue the callback to run after a certain time from the moment the setTimeout is called. For example, if setTimeout(fn, 3000) is run, and then 5 seconds of expensive blocking code runs, fn will run immediately after those 5 seconds. If 1 second of blocking code runs instead, fn will run 2 seconds after that blocking code finishes. For example:

console.log('script start');
// Putting the below in a setTimeout so that the above log gets rendered

setTimeout(() => {
  setTimeout(() => {
    console.log('setTimeout callback');
  }, 1000);
  const t0 = Date.now();
  while (Date.now() - t0 < 700);
  console.log('loop done');
}, 30);

Above, you can see that the for loop takes some time to finish, but once it does, the setTimeout callback runs nearly immediately afterwards.

You can think of it like: when setTimeout is called, at Date.now() + delay, a new task gets pushed to the macrotask queue. Other code may be running at the time the task gets pushed, or it may have taken some time before the code after the setTimeout finished, but regardless, the callback will run as soon as it can after Date.now() + delay.

This process is described precisely in the specification:

  1. (After waiting is finished...) Queue a global task on the timer task source given method context to run task.

The task does not exist in the queue (or in the stack) until the time elapses, and the function call only goes into the stack once the task starts running - which may occur as soon as the time elapses, or it may take some additional time if a different task is running at that time.


we had addEventListener()'s with some call-back functions. Even in this case, will the call-back functions of the event listeners be enqueued in the event-queue?

No - their handlers will only get put into the queue once the listener fires.

CertainPerformance
  • 260,466
  • 31
  • 181
  • 209
  • Running the code snippet with the button produces weird timing differences because of how stackoverflow using console.log to display the console to the screen, pasting the code in your own console produces intended results – Samathingamajig Nov 09 '20 at 17:50
1

As you might be aware by now JavaScript engine executes on a single thread, so how are asynchronous operations handled? You are partially true in the below statement, but there is more to it :

Consider the above code. I learnt that JS uses a call-stack and an event-queue to order the execution of instructions.

True, we do have a call stack and an event loop. But we also have a WEB APIs environment, Call-back Queue and a Micro-task Queue.

Whenever there is any asynchronous task, it moves to the WEB API Environment, for example, when you have an tag with a very large image in the "src" attribute, this image is not downloaded synchronously, because that would block the thread, instead it is moved into the WEB API Environment where the image is loaded.

<img src="largeimg.jpg">

Now, if you want to do something once the image is loaded, you will need to listen to the image's 'load' event.

document.querySelector('img').addEventListener('load', imgLoadCallback);

Now once the image has been loaded, this callback function is still not executed, instead now it is moved into the callback queue. The callback function waits in the callback queue, the event loop will check for synchronous code, and wait until the call stack is empty. Once the call stack is empty, the event loop will push in a first in callback function into the call stack in one event loop tick. And that is when that call back function is executed.

However, this changes when there are micro-tasks such as Promises. When there is a promise, it is sent to the microtask queue. Microtasks will always have priority over the callbacks and they can and will halt the callbacks until they are executed, event loop will always prioritize microtasks.

This is how the JavaScript Call Stack, Event Loop, Call Back Queue, Microtasks Queue and WEB API Environments work.

Now Run this below code, before running try to guess the outcome. It will be exactly as per what I have written above :

//Synchronous Code - Always prioritized over async code
console.log('Asynchronous TEST start');

//It is a 0 Second Timer, But a timer is not a microtask
setTimeout(() => console.log('0 sec timer'), 0);

//Promise is a microtask
Promise.resolve('Resolved promise 1').then(res => console.log(res)); 

//2nd promise is a microtask too
Promise.resolve('Resolved promise 2').then(res => {
  for (let i = 0; i < 1000000000; i++) {} //very large loop
  console.log(res);
});

//Synchronous Code - Always prioritized over async code
console.log('Test end');

SPOILER ALERT for above snippet:

As you can see, the timer runs in the end although it is a 0 second timer, it does not actually execute at 0 seconds. Why is that? Because Settimeout uses a callback, and promises are microtasks, Microtask Priority is always greater than Callback Priority

Link
  • 1,048
  • 1
  • 14
  • Great answer! Are there any other data structures that order the execution, other than the call-stack, callback queue and the micro-task queue? Could you provide any links, where I can find more information about them? Thank you! – Aditya Nov 10 '20 at 03:56
  • 1
    It will execute only after the execution of any synchronous code. So yes in cases till the end of the file. Point to note is that although the micro-tasks themselves will be sent to the WEB API Environment initially when their line of code is read, they are only executed when there is nothing else present in the callstack. – Link Nov 10 '20 at 04:02
  • 1
    I was able to find this really good Medium article detailing this exact same thing. It has some good images as well which depict the things I've written in my answer https://medium.com/@jitubutwal144/javascript-how-is-callback-execution-strategy-for-promises-different-than-dom-events-callback-73c0e9e203b1 – Link Nov 10 '20 at 04:28