There have been a lot of articles about Event loops in nuggets, so I’m going to make some cold rice to record my understanding. After all, a bad keyboard is worse than a good memory.
For me, understanding the cycle of events started with an interview question, yes, the one you’ve probably seen, that many people in Denver have said.
So the problem is over here. Let’s expand it
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')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')})console.log('script end')
Copy the code
This topic is about event loops, microtasks, timers and async/await. If you haven’t figured it out yet, follow along.
JavaScript threads, asynchrony, and event loops
We all know that JavaScript is single-threaded, and single-threaded means you can only do one thing at a time. Why isn’t JavaScript designed with multiple threads? Here is a quote from Teacher Ruan Yifeng’s blog:
The single thread of JavaScript, relative to its purpose. As a browser scripting language, JavaScript’s primary purpose is to interact with users and manipulate the DOM. This means that it has to be single-threaded, which can cause complex synchronization problems. For example, if there are two threads of JavaScript at the same time, one thread adds content to a DOM node, and the other thread removes that node, which thread should the browser use?
So the core feature of JavaScript that is single threaded is not going to change. One problem with single threads is that code blocks. In single-thread mode, tasks are executed in a queue and must wait for the completion of the previous task before entering the next one. Therefore, if the previous task takes too long, the later task has to stay in the waiting state, which is a bad experience of “stuck” for users. Moreover, this blocking wait is not due to poor hardware performance, but a waste of time.
In order to solve this problem, the concept of asynchrony is introduced, and all tasks are divided into two categories, one is synchronous task, the other is asynchronous task. A synchronization task is a task that is queued on the main thread. A synchronization task is executed only after the first task is completed. Asynchronous tasks enter the event queue instead of the main thread. After the main thread completes the synchronization task, it reads the event from the event queue and then enters the main thread for execution.
So where is the cycle of events? In fact, the main thread reads the events in the “Event queue”. This process is repeated over and over again. This operation mechanism is called “Event Loop”. The event loop is the js execution mechanism.
The process of the event cycle
So if you look at the cycle of events, you draw a picture, you see it elsewhere and you draw it again.
When a task enters the execution stack, it is first determined whether it is synchronous or asynchronous, and they go to different “places”. Synchronous tasks go directly to the main thread for execution, while asynchronous tasks go into the Event Table and register callback functions. Common asynchronous tasks are timers, DOM Event listeners, and Ajax requests. When they meet the trigger criteria, callback events are pushed to the Event Queue, waiting for the main thread to read and execute them. The main thread does not read events from the event queue until it has completed all synchronization tasks. This process is repeated all the time, which is why it is called “event loop” rather than “task loop”.
After understanding the process of the event loop, use a piece of code to practice:
console.log(1)
setTimeout(() = >{
console.log(2)},0)
console.log(3)
Copy the code
This is a very simple problem, right? Everyone can do it. Now you can use the process in the picture above to illustrate:
console.log(1) // Is a synchronization task, put it in the main thread
setTimeout(a)// It is an asynchronous task, placed in the Event Table, and 0 ms later the callback is pushed to the Event Queue
console.log(3) // Is a synchronization task, put it in the main thread
// According to the process, the synchronization task is executed first, that is, print 1 and 3 first. When the synchronization task is completed, check whether there is an executable function in the event queue, see the timer callback, execute it, print 2
Copy the code
One small detail about the timer is that, instead of executing the callback immediately after the timer expires, the callback is pushed into the event queue, and the timer callback is not triggered if the synchronization task has not finished.
Macrotasks and microtasks
The above little chestnut 🌰 explains the process of the cycle of events, but when you take this routine to do the interview question said in the beginning, you will find that events are not so simple. To simplify things, let’s look at this:
console.log(1)
setTimeout(() = >{
console.log(2)},0)
new Promise((resolve, reject) = >{
console.log('new Promise')
resolve()
}).then(() = >{
console.log('then')})console.log(3)
Copy the code
Or according to the above flow chart process to analyze the problem:
console.log(1) // Synchronization is performed in the main thread
setTimeout(a)// Asynchronously, put to Event Table, 0 ms later callback pushed to Event Queue
new Promise Console. log('new Promise')
.then // Asynchronously, place it in the Event Table
console.log(3) // Synchronization, main thread execution
Copy the code
So by analysis, it should result in 1 => ‘new Promise’ => 3 => 2 => ‘then’.
But when you go to the browser to execute, you’ll find: 1 => ‘new Promise’ => 3 => ‘then’ => 2.
The event queue is actually a “first in, first out” data structure, and the first event is read by the main thread first. In this example, the setTimeout callback is queued first, which should be executed before the.then callback, but the result is the opposite.
The reason, as we all know, is that the division between synchronous and asynchronous is not so precise and has to be explained by the common terms “Macrotask” and “Microtask”.
In fact, I did not find macrotask-related content on MDN, but I could find microtask-related content. It seems that macro Tasks should be called “Tasks”, while microtasks are “microtasks”. Here is an article from MDN to look at: Queues and schedules Using microtasks in JavaScript with queueMicrotask(), and queues and schedules
Ok, forget the name problem, let’s look at macro tasks and micro tasks, and here’s a summary of others, something like this:
- Macro tasks include: Entire code contained in script, setTimeout, setInterval, setImmediate (in NodeJS)
- Microtasks include: Promise, process.nexttick (in Nodejs)
And here’s a picture (if it’s okay) :
According to this process, its execution mechanism is:
- Execute a macro task, and if it encounters a microtask, place it in the event queue of the microtask
- After the current macro task is executed, the event queue of the microtask is viewed and all the microtasks in it are executed in sequence
Ok, after macro and micro tasks, this is the topic above:
console.log(1)
setTimeout(() = >{
console.log(2)},0)
new Promise((resolve, reject) = >{
console.log('new Promise')
resolve()
}).then(() = >{
console.log('then')})console.log(3)
// Run the entire code first, which is a macro task
// If console.log(1) is encountered, print 1
// When a timer is encountered, it belongs to a new macro task, which is reserved for later execution
// When you encounter a new Promise, this is implemented directly, print 'new Promise'
//. Then belongs to the microtask, which is put into the microtask queue and executed later
// If console.log(3) is encountered, print 3 directly
// Now go to the list of microtasks to see if there are any microtasks, find the. Then callback, execute it, print 'then'
// When the first macro task is completed, the next macro task is executed. There is only one timer macro task left
Copy the code
The emphasis here is on the order in which the microtasks are executed. It is not particularly accurate to say that the microtasks are executed after the macro tasks are executed. The microtasks generated in that round should be executed only after the macro tasks in that round are completed. In other words, the tasks in the micro-task list are generated based on the execution process of a macro task. When the macro task is executed, the tasks in the micro-task list should be executed successively, and then the next macro task should be executed, and so on, forming the event cycle mechanism. (Feels like a tongue twister)
Now try again to do the interview question above, if you didn’t get it right then look at the async/await section again.
Async and await
To get to know something, we can start with its name. Async means’ asynchronous’ and await means’ async wait ‘. So async is used to declare an asynchronous method and await is used to wait for an asynchronous method to execute.
To clarify, await must be executed in an async declared function, so how can async functions be executed? In fact, async functions can be run directly and called just like ordinary functions. So what is the function of async?
About the async
Copy this code to the browser console and run it:
function fn1 (){
return 'hello'
}
async function fn2 (){
return 'hi'
}
console.log(fn1()) // hello
console.log(fn2()) // Promise {<resolved>: "hi"}
Copy the code
The result is that async wraps the return value of the function as a Promise object. If the value returned by return is used, it is encapsulated as a Promise object by promise.resolve (). What if nothing is returned, for example:
async function log (){
console.log('hello')}Copy the code
You’re smart enough to know it turned out to be a Promise.
Since async functions return a Promise object, it can also be treated with.then() :
async function test (){
return 'hello'
}
test().then(val= >{
console.log(val) // hello
})
Copy the code
Next, look at the await.
About await
Conventional thinking makes people think that “await” can only be used to wait for an async function to complete. In fact, there is no requirement that “await” can only be followed by an async function call. Let’s test this:
function fn1 (){
return 'fn1'
}
async function fn2 (){
return 'fn2'
}
async function test (){
const v1 = await 123
const v2 = await fn1()
const v3 = await fn2()
console.log(v1) / / 123
console.log(v2) // fn1
console.log(v3) // fn2
}
test()
Copy the code
You can see that await can be called directly with a normal function, or even with a direct object.
In everyday development, we use “await” in combination with “async” and “await”. Usually “await” is used to wait for a Promise returned by an async function to get the result in “resolve”. For await, there are no more than two results, “yes Promise object” and “no Promise object”.
Look, here’s the point. “Await” will block the code behind it. “await” will block the code behind it.
async function fn1 (){
console.log(1)
await test() // The test function is not blocked
console.log(2) // this is blocked
}
Copy the code
When waiting for something other than a Promise, await blocks the code below, executes the asynchronous code outside of async, and returns to the async function to await something other than a Promise as the result of an await expression. Execute the previously blocked code.
When waiting for the Promise object, the same code will be blocked below. The synchronization code outside async will be completed first, waiting for the Promise object to fulfil, which will obtain the value of resolve as the result of the expression. Also execute the previously blocked code.
An 🌰 :
async function fn1 (){
console.log(1)
await fn2()
console.log(2)}async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
Copy the code
This code prints the results in sequence: 1, fn2, 3,2. Make await await a non-promise object and have the same effect. Change the code:
async function fn1 (){
console.log(1)
await fn2()
console.log(2)}function fn2 (){ // Remove async, fn2 is a normal function and does not return a Promise
console.log('fn2')
}
fn1()
console.log(3)
// results: 1, fn2, 3,2
Copy the code
Tao friend, have you realized yet? ! 😂
Face up to the problem
I believe that after reading some of the above analysis, to do this problem will be very clear, after all, well founded. Take a look:
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')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')})console.log('script end')
Copy the code
Analysis process:
- Execute the entire code, encountered
console.log('script start')
Print the result directly and output itscript start
; - Encountered timer, it is a macro task, put first do not execute;
- encounter
async1()
, execute async1 and print firstasync1 start
What if I get await? So async2, printasync2
, then block the following code to jump out of the synchronization code; - Jump to the
new Promise
In this case, just execute, printpromise1
, the following encounter.then()
, it is a microtask, placed in the microtask list for execution; - The last line is printed directly
script end
Now that the synchronization code is done, what do I do? Don’t forget to also have part of the await code below, printasync1 end
; - Now that the macro task is over, go perform the micro task, perform the then callback, print
promise2
; - Everything is done in the last macro task, let’s move on to the next macro task. Who is it? It’s a timer. It prints
settimeout
.
So the final result is: script start, async1 start, Async2, promise1, script end, AsynC1 end, promise2, setTimeout.
Note that if you run this code on an earlier version of NodeJS, the result may be different.
That’s it. ⛽ ️ ⛽ ️ ⛽ ️
Add a small topic, very interesting:
let a = 0
let b = async () => {
a = a + await 10
console.log('2', a)
}
b()
a++
console.log('1', a)
Copy the code