My blog is on CSDN. If you are interested, you can follow me on CSDN

JS event loop principle and asynchronous execution process these knowledge points may be a little difficult for beginners, but it is necessary to overcome the barrier, escape is not to solve the problem, this article aims to help you understand them thoroughly.

Note: This article is mainly based on the browser environment, Node environment has not been studied for the time being. The content of the article is also learned from others and combined with their own understanding, the relevant references will be given at the end of the article, if there is infringement, please inform the rectification, or there are incorrect descriptions are also welcome to remind. This article will be a bit long, so be patient

No nonsense, let us from simple to complex, step by step, to enjoy the feast of knowledge ~

JS is single-threaded

We all know that JS executes single-threaded (reason: We don’t want to manipulate the DOM in parallel. The DOM tree is not thread-safe. If you have multiple threads, that will cause collisions. A JS engine can only do one thing at a time (there is a very important feature of JS execution: Run to complete, just run until complete). JS is single threaded, so WHEN I request data at the back end of the web page, HOW can I still manipulate the page: I can scroll the page, I can also click the button, this is not with JS is single threaded conflict? This problem has bothered me for a long time, and a big reason is that I thought browsers were just a JS engine. Here’s what I thought the browser would look like, using Google chrome as an example:

A quick note: V8 is the JS execution engine for Google Chrome, which is used to run JS codefunctionEach frame (which holds the execution environment of the current function) is pushed into the Call stack in the order in which the code is executed, with the function at the top of the stack executing first and the next function popping up after the execution. The heap is used to store various JS objects.
Code 1

function foo() {
    bar()
    console.log('foo')}function bar() {
    baz()
    console.log('bar')}function baz() {
    console.log('baz')
}

foo()
Copy the code

We define foo, bar, baz, and then call foo. The console output is:

baz
bar
foo
Copy the code

The execution process is as follows:

  1. A global anonymous function executes first (the global entry to JS execution will be ignored in subsequent examples), and when foo is called, it pushes foo onto the execution stack.
  2. If foo is executed and bar is called in foo’s body, the bar function is pushed onto the stack.
  3. After executing the bar function, baz function is called in the bar function body, and the baz function is pushed onto the execution stack.
  4. Execute the baz function with only one statement in the function bodyconsole.log('baz'), execute, print on console:baz, and then the baz function completes and pops up the execution stack.
  5. At this point, the top of the stack is bar, in the body of barbaz()The statement has been executed, then the next statement (console.log('bar')), print at the console:bar, and then the bar function completes and pops up the execution stack.
  6. At this point, the top of the stack is function foo, in the body of function foobar()The statement has been executed, then the next statement (console.log('foo')), print at the console:fooThen foo completes and pops up the stack.
  7. At this point, the stack is empty and the round is complete.

Again, the execution flow chart corresponding to the above steps is as follows:

2. Event Loop

Let’s change code 1, code 2 looks like this:

function foo() {
    bar()
    console.log('foo')}function bar() {
    baz()
    console.log('bar')}function baz() { 
    setTimeout((a)= > {
        console.log('setTimeout: 2s')},2000)
    console.log('baz') 
}

foo()
Copy the code

All else being equal, we add a setTimeout function to the baz function. , according to the hypothesis 1, and the browser only consists of a JS engine, then all of the code must be executed simultaneously (because the JS execution is single-threaded, so the current stack function execution time need how long, to perform this function in the stack here after must wait for it to perform other functions of the pop-up to execute (this is the meaning of the code block). SetTimeout in the body of the baz function should wait 2 seconds, print setTimeout: 2s in the console, and then print: baz. So we expect the output order to be: setTimeout: 2s -> baz -> bar -> foo (this is wrong).

If the browser is really designed like this, there must be something wrong!! When we encounter time-consuming operations such as AJAX requests and setTimeout, our page will need to wait for a long time, so it will be blocked and nothing can be done, resulting in “fake death” of the page, which is definitely not what we want.

It’s certainly not what I thought, but it’s important to note that JS is single-threaded, and that’s true, but a browser doesn’t just have a JS engine, it has other threads that do other things. The following image (which references Philip Roberts’ talk: Help, I’m Stuck in an Event-Loop (YouTube)) can be seen on the wall: Help, I’m Stuck in an Event-Loop

JS engine
Web APIs
GUI rendering
In the HTML standard definition, the data structure of a task queue is not actually a queue, but a Set.
Event loop mechanism

The thread of role
JS engine thread Also known as the JS kernel, it handles JavaScript scripts. (For example V8 engine)

The JS engine thread is responsible for parsing THE JS script and running the code.

② The JS engine waits for tasks in the task queue to arrive and then processes them.

③ No matter what time in a Tab page (renderer processThere is only one JS threadRun the JS program.
Event trigger thread Belongs to the renderer process rather than the JS engine and is used to control the event loop

① When the JS engine executes a code block such as setTimeout (or other threads from the browser kernel, such as mouse click, Ajax asynchronous request, etc.), it will add the corresponding task to the event thread.

② When the corresponding event meets the trigger condition is triggered, the thread will add the event to the end of the queue to be processed, waiting for the JS engine to process.

Pay attention to: Due to the single-thread relationship of JS, the events in the queue to be processed are queued to be processed by the JS engine and will be executed when the JS engine is idle.
Timing trigger thread The thread of setInterval and setTimeout

① Browser timing counters are not counted by the JS engine.

②JS engine is single thread, if in the blocking thread state will affect the accuracy of timing, therefore, through a separate thread to time and trigger timing.

③ After the timing is complete, add it to the event queue and wait for the JS engine to be idle.

Pay attention to: W3C specifies in the HTML standard that setTimeout interval less than 4ms is counted as 4ms.
Asynchronous HTTP request threads XMLHttpRequest opens a new thread request through the browser after the connection

If a callback function is set, the asynchronous thread will detect a state changeGenerates a state change eventThe callback is placed in the event queue and executed by the JS engine.
GUI rendering thread Responsible for rendering the browser interface, including:

Parsing HTML, CSS, building DOM tree and RenderObject tree, layout and drawing, etc.

(2) Repaint and Reflow processing.

Here’s a quick summary of the event loop:

  1. The JS thread is responsible for processing THE JS code, and when it encounters asynchronous operations, it passes these asynchronous events to the Web APIs and continues to execute.
  2. The Thread of the Web APIs adds the received events to the task queue (presumably to the event queues in the task collection) in a regular order.
  3. After the JS thread has finished processing all the current tasks (execution stack is empty), it will check to see if there are any events waiting to be processed in the task queue, if so, it will fetch an event callback and put it into the execution stack to execute.
  4. Then repeat step 3.

Let’s take a look at the process of executing code 1 in a real browser. Here’s a sample process site, also mentioned in Philip Roberts’ talk (written by him), that you can try for yourself: portals

Code 1
Code 2

The Web API starts timing
baz -> bar -> foo -> setTimeout: 2s
SetTimeout is the minimum delay time, not the exact wait time.

Now that you have a feel for javascript execution in your browser, let’s strike while the iron is hot and explore event loops and async

3. Event loops (advanced) and asynchrony

3.1 Try setTimeout(fn, 0)

SetTimeout: 0s -> foo (0 seconds)

function foo() {
    console.log('foo');
}
setTimeout(function() {
    console.log('setTimeout: 0s');
}, 0);

foo();
Copy the code

Foo -> setTimeout: 0s

Minimum delay >=4ms
this
Whenever the JS execution stack encounters an asynchronous function, it will be pushed to the Web APIs mindlessly
The event
The callback

3.2 Promises in the Event Loop

Now it’s time to take it a step further, and we can’t wait to debut promises in ES6! (This article isn’t about promises, but if you’re not familiar with promises, check them out first.) In fact, the above browser model is based on THE ES5 standard, and the ES6+ standard has a new type of task queue, which becomes the following two types:

  1. A macrotask queue (macrotask queue, callback queue)Macrotask Queue: Strictly speaking, there is no macroTask queue in HTML. It is an ES5 event queue that stores callbacks to DOM events, AJAX events, setTimeout events, etc. Can be achieved bysetTimeout(func)Can befuncAdd a function to the macro task queue (usage scenario: to divide computatively long tasks into smaller chunks so that the browser has time to process user events and display the progress).
  2. Microtask Queue: Stores Promise events, nextTick events (Node.js), and so on. There’s a special functionqueueMicrotask(func)Can befuncFunction added to the microtask queue.

So now the event cycle model looks like this:

  1. The JS thread is responsible for processing THE JS code, and when it encounters asynchronous operations, it passes these asynchronous events to the Web APIs and continues to execute.
  2. The Web APIs thread adds the received events to the task queue according to certain rules, macro events (DOM events, Ajax events, setTimeout events, etc.) to the macro task queue, and micro events (Promise, nextTick) to the microevent queue.
  3. JS after thread to handle all the current task (execution stack is empty), it will go to the small task queue for events, and all the events in the micro task queue is done one by one, until the task queue is empty and then to macro task queue to retrieve an event execution (finish each take a macro task in the queue after the event has been completed, check all the task queue).
  4. Then repeat step 3.

A picture is worth a thousand words. Draw a flow chart to make it clearer and help you remember:

Execute stack –> Microtask –> Render –> Next macro task

3.2.1 Use Promise alone

Let’s warm up with just Promise:

function foo() {
    console.log('foo')}console.log('global start')

new Promise((resolve) = > {
    console.log('promise')
    resolve()
}).then((a)= > {
    console.log('promise then')
})

foo()

console.log('global end')
Copy the code

The console output is:

global start
promise
foo
global end
promise then
Copy the code

Code execution explanation:

  1. performconsole.log('global start')Statement, print out:global start.
  2. Continue to execute, encounterednew Promise(....)(Here is a point:When the new keyword is used to create a Promise object, the function passed to the Promise is called executor. The executor function is executed automatically when the Promise is created, and the stuff in then is executed asynchronously), the anonymous function in the Promise parameter is executed in step with the main thread, executeconsole.log('promise')Print out:promise. In the implementationresolve()Then the Promise state changes to Resolved and continuethen(...)Then is submitted to the Web API for processing, which adds it to the microtask queue (note: there is already a Promise event in the microtask queue at this point).
  3. After delivering the Promise event, the execution stack continues down to the statementfoo(), execute the foo function to print:foo.
  4. The execution stack continues and the statement arrivesconsole.log('global end')After executing, print:global end. At this point, the current event loop is complete and the stack is empty.
  5. The event loop mechanism first checks whether the microtask queue is empty, finds a Promise event to be executed, pushes it onto the execution stack, executes the code in then, and executesconsole.log('promise then'), print out:promise then. At this point, a new round of events (Promise events) has ended and the stack is empty. (Note: the microtask queue is empty.)
  6. When the execution stack becomes empty, check the microtask queue first and find that the microtask queue is empty. Then check the macro task queue and find that the macro task queue is also empty. Then the execution stack enters the waiting event state.

Use a GIF to show the process (note: the demo site does not draw a microtask queue, we need to imagine a microTask queue) :

3.2.2 Promise combined with setTimeout

Now that we’ve looked at the execution flow of individual macro and microtasks, let’s mix the events of these two tasks to see what happens, with a code example to try it out:

function foo() {
    console.log('foo')}console.log('global start')

setTimeout((a)= > {
    console.log('setTimeout: 0s')},0)

new Promise((resolve) = > {
    console.log('promise')
    resolve()
}).then((a)= > {
    console.log('promise then')
})

foo()

console.log('global end')
Copy the code

The console output is:

global start
promise
foo
global end
promise then
setTimeout: 0S
Copy the code

Code execution explanation:

  1. performconsole.log('global start')Statement, print out:global start.
  2. Further down the line, setTimeout is encountered, and the JS execution stack hands it off to the Web API for processing. After a 0 second delay, the Web API adds the setTimeout event to the macro task queue (note: there is already a setTimeout event waiting to be processed in the macro task queue at this point).
  3. The JS thread passes the setTimeout event to itself and continues executing, encountering itnew Promise(....)The anonymous function in the Promise argument executes synchronously, and executesconsole.log('promise')Print out:promise. In the implementationresolve()Then the Promise state changes to Resolved and continuethen(...)Then is submitted to the Web API for processing, which adds it to the microtask queue (note: there is already a Promise event in the microtask queue at this point).
  4. After delivering the Promise event, the execution stack continues down to the statementfoo(), execute the function foo to print outfoo.
  5. The execution stack continues and the statement arrivesconsole.log('global end')After executing, print:global end. At this point, the current event loop is complete and the stack is empty.
  6. The event loop mechanism first checks whether the microtask queue is empty, finds a Promise event to be executed, pushes it onto the execution stack, executes the code in then, and executesconsole.log('promise then'), print out:promise then. At this point, a new round of events (Promise events) has ended and the stack is empty. (Note: the microtask queue is empty.)
  7. When the execution stack becomes empty, I first check the microtask queue and find that the microtask queue is empty. Then I check the macro task queue and find that there is a setTimeout event to be processed. Then I push the anonymous function in setTimeout into the execution stack and execute itconsole.log('setTimeout: 0s')Statement, print out:setTimeout: 0s. At this point, a new round of events (setTimeout events) has ended and the stack is empty. (Note: the microtask queue is empty, as is the macro task queue.)
  8. When the execution stack becomes empty, check the microtask queue first and find that the microtask queue is empty. Then check the macro task queue and find that the macro task queue is also empty. Then the execution stack enters the waiting event state.

This example is explained in some detail. There are three cycles of events. For the same reason, use a GIF to visually illustrate the code execution process.

async_function

3.3 Async /await in event loop

Here is a brief introduction to async functions:

  • In front of the functionasyncThe function of the keyword is 2 points: ① this functionAlways return a promise. ② Allow internal use of functionsawaitThe keyword.
  • The keywordawaitMake the async function wait (the execution stack cannot stop and wait, of course) until the promise is fulfilled and the result is returned.awaitThis only works in async functions.
  • Async functions are just a syntax for getting a promise result more elegantly than promise (when a promise is invoked chained).

As above, let’s first separate async function to see how the process is

function foo() {
    console.log('foo')}async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')}async function async2() {
    console.log('async2')}console.log('global start')
async1()
foo()
console.log('global end')
Copy the code

Here we add two async functions: async1 and async2. The result is as follows:

global start
async1 start
async2
foo
global end
async1 end
Copy the code

Let’s take a look at how the code is executed:

  1. Executed firstconsole.log('global start'), print out:global start.
  2. performasync1()Enter the async1 function and executeconsole.log('async1 start'), print out:async1 start. Then performawait async2()Here,awaitWhat keywords do isThe code below await can only be executed after the promise following await returns the result(At this point, the microtask queue has an event, which is actually a Promise event), andawait async2()The statement is executed as if it were a normal functionasync2(), into the async2 function body; performconsole.log('async2'), print out:async2. The execution stack of async2 function is displayed after execution.
  3. becauseawaitThe statement after the keyword has been paused, async1 is finished, and the stack pops up. JS main thread continues down, executefoo()The function prints:foo.
  4. performconsole.log('global end'), print out:global end. There is no more code to execute after this statement, and if the stack is empty, the execution of this round ends.
  5. At this point, the event loop mechanism starts to work: similarly, check the microtask queue first, and then check the macro task queue after all existing microtask events have been executed. Currently the events in the microtask queue are in the async1 functionawait async2()After the async2 function executes, the Promise state becomes settled and the code can proceedawaitThe code after the statement is executed as a microtask eventconsole.log('async1 end')Statement, print out:async1 end. The stack is empty again, and the event completes.
  6. The event loop mechanism checks the microtask queue and finds that the microtask queue is empty. Then it checks the macro task queue and finds that the microtask queue is also empty. Then it enters the waiting event state.

So far, we have mastered the single event type, let’s exercise it comprehensively!

4. Comprehensive (self-test)

Here are some common questions to test your mastery and further consolidate it. Here no longer step by step analysis, there are confused can leave a message to answer.

4.1 Simple Fusion

// Write the output
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');
Copy the code

Output result:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
Copy the code

4.2 deformation 1

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2 makes the following changes:
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');
Copy the code

Output results:

script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
Copy the code

4.3 deformation 2

async function async1() {
    console.log('async1 start');
    await async2();
    // Change as follows:
    setTimeout(function() {
        console.log('setTimeout1')},0)}async function async2() {
    // Change as follows:
    setTimeout(function() {
        console.log('setTimeout2')},0)}console.log('script start');

setTimeout(function() {
    console.log('setTimeout3');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

console.log('script end');
Copy the code

Output results:

script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
Copy the code

4.4 deformation 3

async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')}async function a2 () {
    console.log('a2')}console.log('script start')

setTimeout((a)= > {
    console.log('setTimeout')},0)

Promise.resolve().then((a)= > {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) = > {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) = > {
    console.log(res)
    Promise.resolve().then((a)= > {
        console.log('promise3')})})console.log('script end')
Copy the code

Output results:

script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
Copy the code

So, did you get everything right? In fact, it is a mash-up of these asynchronous events. As long as we master their execution process, it is not difficult to analyze them step by step. They are all paper tigers.

5. Conclusion

Breathe a sigh of relief, you’re tired, but congratulations: you should have mastered the loop of events and asynchronous execution. Finally, let’s summarize the main points covered in this article.

  1. JS is single-threaded and can only process one thing at a time. However, browsers have multiple threads, and the JS engine avoids the problem of a single thread being blocked by time-consuming asynchronous events by distributing these time-consuming asynchronous events (AJAX requests, DOM operations, etc.) to the Wep APIs thread.

  2. The Web APIs thread adds completed events of all the events it receives to the corresponding task queue by category. There are two types of task queues:

    • Macrotask Queue: Actually, it’s calledTask queue, ES5 saidtask queue, which is shown in this figurecallback queueMacrotask is the alias we gave it to distinguish it from the new microTask queue in ES6. Macrotask does not exist in the HTML standard. It stores DOM events, AJAX events, setTimeout events, and so on.
    • Microtask Queue: It holds Promise events, nextTick events, and so on. Higher priority than MacroTask.
  3. The event loop mechanism is used to coordinate events and user interactions Interaction, JS scripts, rendering, networking, etc. (defined by THE HTML standard and implemented by each browser vendor). The process of the event loop is as follows:

    • The JAVASCRIPT engine executes an event and, when it encounters an asynchronous event, hands it off to the browser’s Web APIs thread for processing. The event then continues to execute, never preempted, until run to complete.
    • When the JS engine finishes executing the current event (that is, the execution stack becomes empty), it will first check the MicroTask queue and complete the execution of all the pending events in the MicroTask queue.
    • After all the microtask events are executed, the page can be rendered, which indicates that the process of a round event cycle is over. Then look at the MacroTask queue, add a macro event to the execution stack, start a new round of events, and execute all microtask events after execution. And so on. This is the execution of the event loop.

    To help understand: Macro task events like ordinary users, and micro task event like a VIP user, perform the stack to put all the best after waiting for a VIP customer service can give waiting for the average user services, and every time after service is a normal user to see if there are any VIP user waiting, if have, the VIP user priority (PS: RMB players can really do whatever they want hah…) . Of course, when the stack is serving a normal user, even if a VIP user arrives, he will have to wait for the stack to serve the normal user before his turn.

  4. SetTimeout is the minimum delay time, not the exact wait time. In fact, the minimum delay >=4ms, less than 4ms will be regarded as 4ms.

  5. The Promise object is created with the keyword new and the promise constructor. The constructor takes a function called “executor Function” as its argument (that is, new Promise(…)). In the… “). This “processor function” is automatically executed when a promise is created, and anything after.then is asynchronous content that is handed over to the Web APIs and then added to the microtask queue.

  6. Async /await: async is a syntactic sugar for Generator functions. The async function declaration is used to define an asynchronous function that returns an AsyncFunction object. When an async function is executed and an await keyword is encountered, the await statement produces a promise. The code following the await statement is paused until the promise has an outcome (state becomes settled).

That’s it. Take a break

References:

  • What is the Execution Context & Stack in JavaScript?
  • The JavaScript Event Loop
  • What is the JavaScript event loop?
  • Understanding JS: The Event Loop
  • Tasks, microtasks, queues and schedules
  • HTML Living Standard
  • Concurrency model and event loop
  • Asynchronous JavaScript(callback)
  • Explain event loops and task queues
  • From browser multi process to JS single thread, JS running mechanism is the most comprehensive combing
  • More on the Event Loop
  • Let’s talk about JavaScript event loops in a nutshell