JS is single threaded.
First of all, as we all know, JS is a single thread, why this inefficient way of running is still not eliminated that? It depends on its use; JS is mainly used for user interaction and DOM manipulation. For example, if JS has two threads at the same time, one thread adds content to a DOM node, and the other thread deletes the node, the browser will be at a loss at this time. Which thread should be the standard? (To improve performance, the new HTML5 has added web workers, which can add child threads to the main thread but limit their ability to manipulate the DOM.)
Task Queue
JS is a single thread, so the execution of tasks need to queue, one by one, the previous task finished, the next task can start. However, when a task is asynchronous, the browser will have to wait a long time to get its results and continue executing, with the CPU idle in between. JS’s solution is to put the task on hold and move on to other tasks. Return to the task when a result is returned.
That’s on hold, where that’s on hold, and the answer is the task queue.
A synchronization task is executed on the main thread. The next task can be executed only after the previous task is completed. Asynchronous tasks are tasks that do not enter the main thread but enter the task queue. The tasks in the task queue can enter the main thread only after the main thread is complete.
Execute JS stack
First, let’s look at the concepts of heap and stack. The stack is used to statically allocate memory and the heap is used to dynamically allocate memory, both of which exist in computer memory. The heap is first in, last out, and the stack is first in, first out. All js tasks are executed in the JS execution stack. The task on the stack is entered first and then executed, but most of the time there is only one task in the JS execution stack. (Mentioned below)
Macro tasks and Microtasks (Task & Microtask)
As mentioned above, asynchronous tasks are not executed on the main thread. In fact, not only asynchronous tasks, but all microtasks are not executed on the main thread. In fact, we can call the above task queue microtask queue. Macro tasks run directly on the main thread, while microtasks need to queue up for execution.
Let’s look at the code (example1)
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
Copy the code
What is the output of that?
The order is:
script start
script end
promise1
promise2
setTimeout
Copy the code
First we treat the entire code as a script tag, which executes as a macro task directly into the JS execution stack:
Output ==script start==;
SetTimeout is encountered and 0 seconds later setTimeout is added to the “macro task queue” as an independent macro task. (Note that this is the macro task queue, also known as the main thread);
When I meet a promise, the first THEN after completion of the promise will be added to the “micro-task queue” as an independent micro-task, and the second THEN will be added to the micro-task queue as a micro-task.
And then print ==script end==;
The js stack is empty. There is a setTimeout in the macro task queue and two Promise (then) tasks in the microtask queue. Which one to execute first? Recalling the asynchronous task execution strategy we talked about earlier, it’s not hard to assume that the next entry into the JS execution stack will be the first promise (then);
Output = = promise1 = =;
And then we look at the macro task queue and the micro task queue. The microtask queue also has a promise (then), so the microtask is pushed onto the JS execution stack to execute;
Output = = promise2 = =;
At this point, the microtask queue is empty, so the task in the macro task queue is executed again, setTimeout;
Output = = setTimeout = =;
In summary, tasks are divided into macro tasks and micro tasks, corresponding to macro task queue (main thread) and micro task queue. A microtask is a task that is executed immediately after the currently executing script ends. When the JS execution stack is empty after the completion of a task, the task will be searched in the microtask queue first. When the microtask queue is not empty, a microtask will be added to the JS execution stack. When the current microtask queue is empty, the task in the macro task queue is executed.
How to distinguish microtasks from macro tasks:
Macro tasks: Are stacked and executed in strict chronological order, so the browser allows JavaScript internal tasks and DOM tasks to be executed in an orderly fashion. When one task finishes, the browser can rerender the page before the next task starts. Each task needs to be assigned, such as from a user click to a click event, rendering an HTML document, and setTimeout as in the above example.
This is because setTimeout will assign a new task to the Event loop after the delay is over, instead of executing it immediately. So the setTimeout callback will wait for all previous tasks to complete before running. This is why ‘setTimeout’ is printed after ‘script end’, because ‘script end’ is part of the first task and ‘setTimeout’ is a new task.
Microtasks: Typically tasks that need to be executed immediately after the execution of the current task, such as responding to a series of tasks, or executing tasks asynchronously without assigning a new task to reduce the performance overhead. A MicroTask queue is an independent queue from a task queue. Microtask tasks are executed after each task is completed. Microtasks generated in each task will be added to the microTask queue. Microtasks generated in the microTask will be added to the end of the current queue, and the MicroTask will process all tasks in the queue in sequence. Microtasks-type tasks currently include MutationObserver and Promise callback functions.
Each time a Promise is resolved (or rejected), its callback function is added to the MicroTask task queue as a new MicroTask. This also ensures that promises can be executed asynchronously. So when we call.then(resolve, reject), a new microtask is immediately generated and added to the queue, which is why ‘promise1’ and ‘promise2’ are printed after ‘script end’, Since tasks in the MicroTask queue must wait for the current task to finish before executing, ‘promise1’ and ‘promise2’ output precedes ‘setTimeout’ because ‘setTimeout’ is a new task, Microtask executes after the current task ends and before the next task begins.
Advanced version takes you deep into Task & Microtask (example2) :
<body>
<div class="outer">
<div class="inner"></div>
</div>
</body>
<script>
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
</script>
Copy the code
What is output when we click on the inner div?
The order is:
click
promise
mutate
click
promise
mutate
timeout
timeout
Copy the code
Why is that so?
A click operation is a macro task. When the inner click listener is completed, the task is considered complete and the promise (then) and mutationObserver callbacks in the microtask queue are executed. The microtask queue is empty after both tasks are completed, and then the bubbling outter click is performed. When the click and microtasks of the outter are completed, the two remaining setTimeout tasks in the macro task queue (main thread) will be searched. And push them one by one onto the execution stack.
Super Advanced version (example3) :
Will it make a difference when we add the following line of code to the js code above?
inner.click()
Copy the code
The answer is:
click
click
promise
mutate
promise
timeout
timeout
Copy the code
Why is there such a difference? Let’s analyze it carefully:
In the previous example, two microtasks were executed between two clicks, whereas in this example, they are executed after two clicks;
First, the event triggered by inner.click() is pushed onto the stack as a task, and the resulting inner listener function is pushed onto the stack as a task. When the task generated by this callback is completed, click is printed, and promises and mutate are added to the microtask queue. Shouldn’t we enforce promises and mutate? Inner. Click () has not been executed yet, so a continuation of the inner. Click () event triggers the outter listener function, which then prints click. Only then will the task in the microtask queue be executed.
To put it simply, in this case, the execution stack of the current script is stuck in the JS stack because we call inner.click() so that the event listener callback is executed synchronously with the currently running script rather than asynchronously. So the microtask in this example will not be executed after each click event, but after both click event executions have completed.
Event Loop
The JS execution stack continuously reads and executes tasks from the main thread and microtask queue. This process is cyclic, so the whole operation mechanism is also called Event Loop.
Note: All results of this article are for Chrome browser, other browsers may not be correct
Refer to the article: jakearchibald.com/2015/tasks-…
Author’s brief introduction
Qiyu, front-end development engineer of Tongbanjie, joined the team in January 2018, and is now mainly responsible for front-end project development of big data team.