Introduction
Javascript is a single thread language by default. However, as web world moves fast, a single thread cannot undertake heavy tasks. It’s hard to change the fundamental mechanism from a single thread to multi-threads. To solve this problem, people come up with the concept of event loop. The concurrency model of javascript is based on event loop [1]
Event loop
Prior to explaining the event loop. It’s better to understand something called block I/O and non-block I/O. Suppose the application you are coding is a single threaded application, whenever you send a request to the Internet or read files from the local file system. It blocks, which means the rest of code can only be running after the request finished. for example:
import requests
// this is i/o request
val = requests.get('http://example.com')
print(val)
requests.get('http://example2.com')
renderPage()
Line 3 will run first, depends on the network, we may wait milliseconds or
seconds, before the fetch finish, the rest code cannot be executed, which
obviously is inefficient, because code print(val)
may depends on the result
above. However, the rest code does not, but you still have to wait until the
request finished to execute the remaining code.
So how does event loop work? Here is an image shows the fundamental concept:
To make it simple, every time when you do any I/O task, this task will be set as an event and put inside an event queue then the rest code will be executed as normal. After you run out all your functions in your call stack. Event loop will come to the event queue to check if there any job has not been done, if so, event loop will fetch the event inside the queue based on priority (macro and micro event, we will talk it in the next section). There is a event loop visualizer created by Philip Roberts [2].
To make it concrete, here is an example code.
setTimeOut(function(){
console.log("event 1");
}, 1000);
setTimeOut(function(){
console.log("event 2");
}, 2000);
both setTimeOut will be put inside an event queue, waiting to be executed. Event 1 will be executed after 1000 milliseconds (1 second), event 2 will be executed after 2000 milliseconds (2 seconds). Actually, it is not precisely 1 second or 2 seconds, we will discuss the reason later on.
Macro task vs Micro task
Have a look following code, think about what will be printed
setTimeout(function(){
console.log(1);
}, 0);
new Promise(function(resolve, reject){
console.log(2);
resolve(3);
console.log(4);
}).then(function(value){
console.log(value);
})
console.log(5);
## 2
## 4
## 5
## 3
## 1
The output is surprise right? Let’s analyze it,
Both setTimeout and Promise are events so that they will be executed
asynchronously. However, since promise is a micro task which has higher priority
than setTimeout (macro task), even we have setTimeout is 0, promise will run
first, the code inside promise will run first, therefore 2 is printed first,
resolve is a callback function, normally resolve will wait for the result of an
async call. Therefore, the code will continue running to print 4 and return a
promise back then we have console.log(5)
which will print number 5. Afterwards,
the callback of promise running which prints number 3, finally number 1 will be
printed.
Javascript event has two categories
- Macro task
- Micro task (higher priority)
By default, when the single main thread encountering functions (sync), it will
put functions inside call stack, execute them then pop them out, if the function
running for a long time without returning value, the main thread has to wait there.
This is why single thread not effcient. However, javascript will make the time
consuming task (I/O) return a placehold
value first. The I/O task will be send
to browser kernel, when task finished, task and its callback functions will be send
back to task queue. The advantage of this strategy is that the main thread is able
to continue running without blocking, after the main thread run out off all the
functions that inside the call stack, the main thread will ask the task queue for
more jobs to do, once there has any task, it will be pushed into the call stack
execute, pop up. This kind of steps will be running in a loop, that’s why we
call it event loop.
Macro task
- SetTimeout
- SetInterval
- SetImmediate
- ajax
Micro task
- async/await
- promise
Let’s do a crazy test to see if we really understand event loop. What will be printed the following code?
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
## script start
## async1 start
## async2
## promise1
## script end
## async1 end
## promise2
## setTimeout
So let’s demystify the code above, first of all, async function is essentially
return a promise. However, async1 and async2 are just defined here not called.
Therefore script start
print first. Then setTimeout is another async function
which has lower priority. Afterwards, async1() is called, async1 start
print
first, async2() is another promise, the code inside will be printed, hence,
async2
was printed. Next, we have another promise, this time promise1
was
printed, since resolve() is a callback, the code will continue running.
script end
prints next. Let’s trace back what we have left, async1() has not
finished yet. Therefore, async1 end
will be printed and our callback of promise
has not finished yet, promise2
will be printed. Finally, the macro task
setTimeout
will be printed.
Reference
1. Concurrency model and Event Loop [Internet].MDN Web Docs2019;Available from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
2. 2014;Available from: http://latentflip.com/loupe