Intro
In this article, I'm going to talk about a very important javascript concept: the event loop. It is one of the most fundamental and vital part when learning about javascript, helps to understand this programming language to a deeper level, and sometimes it's especially critical when dealing with some bugs. So let's get to it, event loops!
Call Stack and Single Thread
Before getting into event loops, there are some basic things we should get a head start. The first one is the idea of call stacks
and single thread processing
.
JavaScript is a single threaded
language, which we all know, but what exactly does this mean? Well, it means that javascript can only do one single task at a time, can only process one module of code at a time, meaning that javascript processes code line by line, one line at a time.
Call Stacks
record where our code is processed to. For example, if we process a function, we'll push
this function to the top of the call stack, and when done processing, this function would be popped
out of the stack.
For example:
function a() {
b();
}
function b() {
console.log("hi");
}
a();
For the code above, the call stack would like this:
Idea behind asynchronous execution
Single thread JavaScript
So now we know javascript is a single thread language. It is primarily used to interact with users and to control DOM elements.
Javascript also has the concept of asynchronous
and synchronous
. With this mechanism, it resolves the problem of blocking
. Here, we give a simple explanation between these two mechanisms.
synchronous
If when a function returns, the caller is able to get the expected result, then this function is a synchronous
function.
asynchronous
If when a function returns, the caller is unable to get the expected result immediately, instead, the caller needs to use some kind of way to callback this expected result at some point in the future, then this function is a asynchronous
function.
Multi thread Browser
Now we know javascript is single threaded, meaning js can only do one task at a time. So how, why are browsers able to process asynchronous
tasks simultaneously.
This is because browsers are multi threaded
. When js needs to process asynchronous
tasks, browsers are going to activate another thread in service of these asynchronous
tasks. Put it in a more simple way, when we say JavaScript is single
threaded, it means that there is only one single thread actually processing the js code, which is the engine that browsers provide for js(primary thread). Besides the primary thread for processing js code, there are lots of other threads which are not mainly in usage of running js code.
For example, if there is a request to send data in the main thread, the browser will distribute this task to the Http request thread
, then proceed doing other tasks, and when the data is successfully fetched, it will then continue to the callback
js code where it left, and then distribute the callback
tasks to the primary thread to process js code.
In other words, when you write js code to send data requests no matter in any protocols, you think you are the one sending the request, however, it is actually the browser that is the one sending the request. For Http
request as an instance, it is actually the http request thread of the browser that sends the request. Javascript code is just responsible for the callback process.
To conclude shortly, when we say a js asynchronous task, to be frank, the asynchronous
ability isn't an inherent feature of javascript, it is actually the ability that browsers provide.
As we see a modern architecture of browsers, there are more than one renderers, and more of them are uncharted in this pic.
Event loops for Browsers
JavaScript classifies its tasks into two catagories: synchronous
and asynchronous
tasks.
synchronous
tasks: For tasks queued for execution on the main thread, only when one task has been completely executed can the next task be executed.asynchronous
tasks: Instead of entering the main thread, it is placed in thetask queue
. If there are multiple asynchronous tasks, they need to wait in the task queue. The task queue is similar to a buffer. The next task will be moved to theexecution stack
and the main thread will execute the task on theexecution stack
.
Well, mentioning the task queue
and execution stack
, we have to first explain what these are.
execution stack
and task queue
-
execution stack
:
As can be seen from the name, it is a stack data structure that stores function calls, following the principle of first-in, last-out(FILO). It is mainly responsible for keeping track of all the code being executed. Whenever a function is executed, the function is popped from the stack; if there is code that needs to be executed, a push operation is performed. It kinda works like the call stack previously mentioned above.
-
task queue
:
Again, as can be seen from the name, the task queue uses the queue data structure, which is used to store asynchronous tasks
and follows the principle of first-in, first-out(FIFO). It is mainly responsible for sending new tasks to the queue for processing.
When JavaScript executes code, it arranges the synchronized code in the execution stack in order, and then executes the functions within in order. When an asynchronous task is encountered, it is put into the task queue, and after all the synchronous codes of the current execution stack are executed, the callback of the completed asynchronous task will be removed from the task queue and put into the execution stack. It works just like a loop and so on and so one, until all tasks are executed.
In an event-driven mode which applies to javascript, at least one execution loop is included to check for new tasks in the task queue. By looping continuously, the callback, or say more plainly, the results, of the asynchronous task is taken out to the main thread for execution.
This whole process is called the event loop
.
Macro and Micro tasks
In fact, there is more than one task queue. According to different types of tasks, it can be divided into micro task queue
and macro task queue
. Here, we'll list some of the most common tasks that you may ecounter, forming a more clear understanding of the difference between micro and macro tasks.
Macro tasks:
script.js(overall code)
,setTimeout
,setInterval
,I/O
,UI interaction events
,setImmediate
(Node.js environment)Micro tasks:
Promise
,MutaionObserver
,process.nextTick
(Node.js environment)
Tasks in task queues are executed as in the picture below:
It can be seen that the execution of Event loops when processing the macro-tasks and micro-tasks is as:
The JavaScript engine first takes the first task from the macro task queue;
After the execution is completed, take out all the tasks in the micro-tasks and execute them in sequence (this includes not only the first micro-tasks in the queue at the beginning of execution).If new micro-tasks are generated during this step, they also need to be executed. That is to say, the new micro-tasks generated during the execution of micro-tasks
will not be
postponed to the next cycle for execution, but will continue to be executed in the current cycle.Then take the next task from the macro task queue. After the execution is completed, take out all the tasks in micro task queues again, and the cycle repeats until the tasks in the two queues are all taken out.
So to conclude, an Eventloop cycle will process one macro-task and all the micro-tasks generated in this loop.
Let's take a look at the example below:
console.log("sync1");
setTimeout(() => {
console.log("setTimeout");
}, 0);
new Promise((resolve) => {
console.log("sync2");
resolve();
}).then(() => {
console.log("promise.then");
});
console.log("sync3");
The output should be:
"sync1";
"sync2";
"sync3";
"promise.then";
"setTimeout";
Well, if you're answer is not exactly same as the output shown above, don't worry, let's dig in and see how does this piece of code process exactly.
When the first console is encountered, it is a synchronous code, which is added to the execution stack, executed and popped from the stack, and
sync1
is printed outWhen
setTimeout
is encountered, it is amacro task
and is added to the macro task queueWhen encountering the console in
new Promise
, because it is resolved immediately, it is a synchronous code, which is added to the execution stack, executed and popped from the stack, andsync2
is printed outWhen encountering
Promise then
, it is a microtask and added to the microtask queueWhen the third console is encountered, it is a synchronous code, which is added to the execution stack, executed and popped from the stack, and
sync3
is printed outAt this point, the execution stack is empty, so execute all tasks in the microtask queue, and print out
promise.then
After executing the tasks in the micro-task queue, execute one task in the macro-task queue and print out
setTimeout
At this point, both the macro-task queue and micro-task queue is empty, end of execution
For step 6 and 7, you might be confused, that why shouldn't setTimeout
print before promise.then
, as when done executing console.log("sync3");
, it should look back at the macro-task queue first since execution stack is empty, then execute all tasks in micro-tasks.
Well, the tricky part lies in the script
macro-task. Notice that the whole javascript code, as in script
, is a macro-task. Moreover, it is always the first
macro-task that will be added to the macro-task queue and the first
to be executed.
I'm sure everything is clear now. So actually, after executing console.log("sync3");
, it indicates that the first
macro-task is completed. Thus, it will continue the first round of Eventloop
by looking into the micro-task queue, seeing Promise.then
, execute it, and boom! This is when the the first round of Eventloop
actually stops. The the second round of Eventloop
then starts again, and so on...
From the workflow of macrotasks and microtasks above, the following conclusions can be drawn:
Micro-tasks and macro-tasks are bound, and each macro-task will create its own micro-task queue when executed.
The execution duration of the microtask will affect the duration of the current macrotask. For example, during the execution of a macro-task, 10 micro-tasks are generated, and the time to execute each micro-task is 10ms, then the time to execute these 10 micro-tasks is 100ms. It can also be said that these 10 micro-tasks caused a 100ms delay for the macro-task.
There is only one macro-task queue, and each macro-task has its own micro-task queue.So each round of the Eventloop consists of one macro-task + multiple micro-tasks.
A very important point is to always remember that the first task in the macro-task queue will always be the
overall script code
.
Below is a question also about the output of event loops, a bit more complicated. Maybe it's time for you to try it yourself!
setTimeout(function () {
console.log(" set1");
new Promise(function (resolve) {
resolve();
}).then(function () {
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then4");
});
console.log("then2 ");
});
});
new Promise(function (resolve) {
console.log("pr1");
resolve();
}).then(function () {
console.log("then1");
});
setTimeout(function () {
console.log("set2");
});
console.log(2);
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then3");
});
Your answer should be like:
pr1
2
then1
then3
set1
then2
then4
set2
Top comments (0)