Basically, this article covers following sub topics
- Quiz to predict the output of the sample JS code.
- An overview of browser, event loop, JS engine, event loop queues.
- How browser executes DOM events(click, http requests) and its callback?
- How is callback execution strategy for promises different than DOM events callback?
- Different execution strategy of tasks queued in Task queue and Micro task queue.
- What kind of tasks are queued in Task queue and Micro task queue?
- Conclusion (Answers to the quiz).
- Quiz to predict the output of the sample JS code
Test 1: What would be the sequence of log messages of following JS code?
Running example can be found here .
Test 2: We have a button with two click event listeners on the same button as shown in the code sample below.
Running example can be found here . Please try to predict the order of log messages when a button is clicked?
If we have correct versions of browsers, output of the above code samples are as follow
Test 1: Start, End, Promise 1, Promise 2, Settimeout 1, Settimeout 2
Test 2: Listener 1, Promise 1, Listener 2, Promise 2,Settimeout 1,Settimeout 2
I have tested on following versions of browsers(Chrome 65, Firefox 60, Safari 8.0.2)
If you have predicted it right, AWESOME!!! but if it surprises you, then you are welcome to read further :)
It is important to understand this behavior because popular browsers have implemented this execution strategy for performance reason(eg. to avoid race conditions). In order to understand the execution strategy of callback functions, let’s try to understand basic overview of how internal of browsers work together.
2. An overview of browser
2.1. User interface: It includes every part of the browser which is visible to the user except the window. For eg. the address bar, back/forward button, bookmarking menu, etc.
2.2. The browser engine: It acts as a bridge between UI and the rendering engine and provide several methods to interact with a web page such as reloading a page, back, forward etc.
2.3. The rendering engine: It is responsible for displaying requested content. For example, if the requested content is HTML, the rendering engine parses HTML and CSS and displays the parsed content on the screen.
2.4. Networking: It is responsible for network calls such as HTTP requests and get actual content to render.
2.4. UI backend: It is used for drawing basic widgets like combo boxes and windows. This backend exposes a generic interface that is not platform specific. Underneath it uses an operating system user interface method.
2.6. Data storage. This is a persistence layer. The browser may need to save all sorts of data locally, such as cookies. Browsers also support storage mechanisms such as localStorage, IndexedDB, WebSQL, and FileSystem.
All the internal components of browser work together to form an execution environment where actual JS code and other operations such as DOM manipulation events are executed.
A runtime environment is the execution environment provided to an application by the operating system. In a runtime environment, the application can send instructions or commands to the processor and access other system resources such as RAM, DISK etc. JS engine, Event queues, Event loop and Web/Dom apis forms the Runtime Environment.
Similarly, call stack load our main JS code and starts executing it. Whenever a function is encountered in our JS code, JS engine creates a new stack and piles it on top and starts executing that function.
Task queue is a data structure that holds callback functions to be executed. Task which is queued first is processed first (first-in-first-out behavior).
The event loop is the mastermind that orchestrates:
- When does it run?
- When do layout and style get updated?
- When do DOM changes get rendered?
It is continuously running programme which keeps monitoring it’s queues. If there is function/callback to execute in an event queue(aka Task queue), it loads the function in a Call Stack. Once the execution in a call stack is finished and stack is cleared, event loop will pick up new task from the task queue. During each tick, even loop picks up new task from the task queue until the queue is emptied. Following piece of a pseudocode illustrates basics of how event loop looks like
As you can see, there is a continuously running loop represented by the
while loop, and each iteration of this loop is called a "tick." For each tick, if an event is waiting on the queue, it's taken off and executed. These events are your function callbacks. (Source: You Don’t Know JS — Async and Performance series)
Following code shows what standard event loop specification says
3. How browser executes DOM events(click, http requests) and its callback?
There are different types of events supported by browser such as
- Keyboard events (keydown, keyup etc)
- Mouse events (click, mouseup, mousedown etc)
- Network events (online, offline)
- Drag and drop events (dragstart, dragend )etc
These events can have a callback handler which should be executed whenever event is fired. Whenever event is fired, it’s callback (aka task) is queued in the task queue. As shown in Test 2, when a button is clicked, it’s callback handler CB1() is queued in a task queue and event loop is responsible to pick it up and execute it in a call stack. There are several rules event loop applies, before picking up a task from a task queue and executing it in a call stack.
Event loop checks, if call stack is empty or not. If call stack is empty and there is nothing to execute in a micro task queue, than it picks up a task from a Task queue and execute it.
4. How is callback execution strategy for promises different than DOM events callback?
When JS engine, traverses through the code within a callback function and encounters web api events such as click, keydown etc, it delegates the task to runtime environment and now runtime decides where should it queue it’s call back handler (either in task queue or micro task queue?). Based on the standard specification, runtime will queue callbacks of DOM/web events in Task queue but not in micro task queue.
Similarly, one task(or callback function) can have multiple other tasks or micro tasks. When JS engine encounters promise object, it’s callback is queued in a micro task queue but not in Task queue.
As mentioned before, event loop will pick up a new task from a Task queue only when call stack is empty and there is nothing to execute in a micro task queue. Let’s assume, there are 3 tasks in Task queue, T1, T2 and T3. Task T1 has one task(say — setTimeout(T4, 0)) and two micro tasks(say promises — M1, M2). When task T1 is executed in the call stack, it will encounter setTimeout(…) and delegates it to runtime to handle its callback. Runtime will queue T4 in aTask queue. When engine encounters promise 1, it will queue its callback (M1) to micro task queue. Likewise, when it encounters another promise 2 object, it will queue it in a micro task queue. Now call stack becomes clear, so before picking up task T2 from the Task queue, event loop will execute all the callbacks (M1, M2) queued in micro task queue. Once micro tasks are executed in a call stack and stack is cleared, it is ready for Task T2.
NOTE (Exception): Even though window.requestAnimationFrame(…) is a function of DOM object window, it’s callback is queued in a micro task queue but it’s execution strategy is different. Execution strategy of window.requestAnimationFrame(…) has not been covered in this article.
5. What kind of tasks are queued in Task queue and Micro task queue?
Tasks are basically callback functions of promises or DOM/web api events. Because tasks in Micro task queue and Task queue are processed in a different way, browser should decide types of tasks which should be queued in Task queue or Micro task queue. According to the standard specification, callback handlers of following events are queued in Task queue
- DOM/Web events (onclick, onkeydown, XMLHttpRequest etc)
- Timer events (setTimeout(…), setInterval(…))
Similarly, callback handlers following objects are queued in Micro task queue
- Promises (resolve(), reject())
- Browser observers (Mutation observer, Intersection Observer, Performance Observer, Resize Observer)
NOTE: ECMAScript uses term Jobs to represent Micro tasks
6. Different execution strategy of tasks queued in Task queue and Micro task queue
Callbacks queued in task queue are executed in first-come-first-service order and browser may render between them (Eg. DOM manipulation, changing html styles etc).
Callbacks queued in Micro task queue are executed in first-come-first-service order, but at the end of every task from the task queue (only if call stack is empty). As mentioned above in the event loop’s pseudo code, Micro tasks are processed at the end of each task.
Test 1: When script mentioned in Test 1 is executed, console.log(“Start”) is executed first. When setTimeout(…) is encountered, runtime initiates a timer, and after 0ms (or specified time in ms), CB1 is queued in Task queue. Similarly, next CB2 is queued in a Task queue immediately after queuing CB1. When promise object is encountered, its callback i.e CB3 is queued in Micro task queue. Similarly, next callback of second promise object CB4 is also queued in Micro task queue. Finally, it executes last console.log(“End”) statement. According to standard specification, once a call stack is emptied, it will check Micro task queue and finds CB3 and CB4. Call stack will execute CB3 (logs Promise 1) and than CB4 (logs Promise 2). Once again, call stack is emptied after processing callbacks in Micro task queue. Finally, event loop picks up a new task from the Task queue i.e CB1 ( logs setTimeout 1) execute it. Likewise, event loop will pick up other task from Task queue and execute it in the call stack until Task queue is emptied.
For animated simulation of the above code sample, I would recommend you to read the blog post by Jack Archibald.
For the second test case, when JS engine encounters first button.addEventListener(...), it assigns responsibility to handle click callback to runtime (Browser). Similarly, it does the same stuff for second button event listener.
Because we have two click listener for a single button, whenever button is clicked, two callbacks( CB1, CB2) are queued in Task queue sequentially as shown in the diagram below
When call stack is empty, event loop picks up CB1 to execute. First console.log(“Listener 1 ”) is executed, then callback of setTimeout(…) is queued in Task queue as ST1 and callback of promise 1 is queued in Micro task queue as shown in the diagram below
When “Listener 1” is logged, P1 is executed because it is a micro task. Once P1 is executed, call stack is emptied and event loop picks up other Task CB2 from Task queue. When callback CB2 is processed, console.log(“Listener 2”) is executed first and callback for setTimeout(…) ST2 is queued in Task queue and promise (P2) is queued in micro task queue as shown in the diagram below
Finally, P2 , ST1 and ST2 are executed sequentially which logs Promise 2, Settimeout 1 and Settimeout 2.
Originally published on zeolearn.com