How does the event loop – JS work?
preface
There are a lot of articles about the cycle of events in the community right now, each with a different focus. But the purpose is the same: to give us a deeper understanding and understanding of what the cycle of events is. How does it work?
This article will walk you through the workflow of the event loop from the browser process scheduling + event loop pseudocode. Hopefully this will give you a better sense of the cycle of events.Copy the code
The original link: lei4519. Making. IO/blog/techno…
Overview of event cycles
- The JS engine is single-threaded, or the browser schedules the JS engine single-threaded.
- An event loop is an implementation of asynchronous (non-blocking) code executed by a single thread.
- The JS engine executes JS code and is based on an event loop.
- Why single thread?
- To prevent multiple JS threads from conflicting DOM operations at the same time, such as one updating the DOM property and another deleting the DOM.
- Is there another solution?
- In other languages, where multithreaded access to shared data is common, the typical solution is locking.
- Why don’t JS use multi-thread + lock?
- Because JS was originally designed to be a simple, easy-to-use scripting language that acts as a secondary place in the browser. So the complexity of introducing thread locks is unacceptable.
Browser processes and threads
To make sense of the event loop, you can’t get around the browser’s processes and threads.
Let's start with a question: What is asynchronous code? Where did it come from?Copy the code
Chrome’s multi-process architecture
The Browser process
The browser’s main process (coordinating, controlling)
- Responsible for browser interface display and user interaction. Such as address bar, bookmark bar, forward, back, etc
- Responsible for page management, creating and destroying other processes
- Network resources, local storage, and file systems
Plug-in process
- Each type of plug-in corresponds to a process that is created only when the plug-in is used
GPU process: used for 3D drawing
Renderer process (browser kernel)
- The main function is page rendering, script execution, event processing and so on
Threads in the renderer process (browser kernel)
GUI rendering thread
Responsible for rendering
-
Render thread workflow
-
The GUI rendering thread and the JS execution thread are mutually exclusive, and when one executes, the other is suspended.
-
The common statement that JS script loading and execution will block parsing of the DOM tree refers to the mutual exclusion phenomenon.
-
During JS execution, writes to GUI threads are not executed immediately, but are stored in a queue for execution when the JS engine is idle (more on the macro task below).
document.body.style.color = '# 000' document.body.style.color = '# 001' Copy the code
-
document.body.style.color = ‘#002’ “`
- If the JS thread's current macro task takes too long, it will render the page incoherently, giving the user the impression that the page is stuck. - '1000 ms / 60 frames = 16.6 ms'Copy the code
JS engine thread
Responsible for executing Javascript code, which is what the V8 engine refers to.
-
When the JS engine executes the code, it will put the code blocks to be executed into the task queue as one task after another. The JS engine will constantly check and run the tasks in the task queue.
// HTML <script> console.log(1) console.log(2) console.log(3) </script> // Wrap code that needs to be executed as a task const task = () => { Log (1) console.log(2) console.log(3)} // Add a task to the queue.Copy the code
-
Javascript engine execution logic: pseudo-code (all pseudo-code is written for understanding, not real browser implementation) :
// Task queue const queueTask = [] // Add the task to the task queue export const pushTask = task= > queueTask.push(task) while(true) { // Keep checking to see if there are any tasks in the queue if (queueTask.length) { // Queue: first in, first out const task = queueTask.shift() task() } } Copy the code
Event trigger thread
Event listening trigger
-
document.body.addEventListener('click', () => {})
-
Pseudo code:
// JS thread -> listen for events function addEventListener(eventName, callback) { sendMessage('eventTriggerThread'.this, eventName, callback) } // The event triggers the thread -> listen for the event of the element // Event trigger thread -> element trigger event function trigger(callback) { pushTask(callback) } Copy the code
Timing trigger thread:
Timers setInterval and setTimeout thread
-
Pseudo code:
// JS thread -> start timing function setTimeout(callback, timeout) { sendMessage('timerThread', callback, timeout) } // Timer thread -> Set timer to start timing // Timer thread -> Timer ends function trigger(callback) { pushTask(callback) } Copy the code
Asynchronous HTTP request threads
Ajax, fetch request
-
Pseudo code:
// JS thread -> start request XMLHttpRequest.send() sendMessage('netWorkThread', options, callback) // Network threads -> Start the request // Network thread -> Request response successful function trigger(callback) { pushTask(callback) } Copy the code
What is an asynchronous task? Where did it come from?
- Asynchronous tasks are tasks that are handled and executed by other browser threads.
- The JS engine calls the browser API to notify other threads to start work and pass in the callback function successfully executed. When the work is finished, other threads will push the callback function into the task queue, and the JS engine will execute the callback function.
Example: The running process of a task queue
- What happens from entering the URL to rendering the page?
- Only the task queue related process will be described in detail
-
Enter the URL in the address bar, request THE HTML, the browser receives the response, passes the HTML text to the renderer thread, and the renderer thread parses the HTML text.
.</div> <script> document.body.style.color = '#f40' document.body.addEventListener('click'.() = > {}) setTimeout(() = > {}, 100) ajax('/api/url'.() = > {}) </script> </body> Copy the code
-
When the rendering thread encounters a
pushTask(<script>) Copy the code
-
The JS thread checks that there is a task in the task queue and starts executing the task.
- Queue writes to the DOM
- Tells the event trigger thread to listen for events
- Tell the timer thread to start the timer
- Tell the network thread to start the request
-
The first macro task completes, executes the write operation queue (render the page)
while(true) { if (queueTask.length) { const task = queueTask.shift() task() requestAnimationFrame() // Render after write operation queue render() // Check if you have enough free time requestIdleCallback() } } Copy the code
-
The first task is completely finished, and the task queue is empty. Three asynchronous tasks are registered in the first task, but the JS engine doesn’t care about that. All it has to do is iterate over the task queue.
-
To simplify the process, assume that three asynchronous tasks are completed at the same time, so there are three tasks in the task queue
// Task queue const queueTask = [addEventListener, setTimeout, ajax] Copy the code
-
However, no matter how many tasks are performed, the above process is repeated in a loop, which is called an event loop.
Microtask queue
This is a pre-ES6 event loop with only one task queue, which is easy to understand.
In the ES6 standard, ECMA requires the JS engine to add a new queue to the event loop: the microtask queue
- Why add a queue? What are the problems to be solved?
Problems with macro task queues
What it actually does: Vue for performance optimization, changes to responsive data do not immediately trigger view rendering, but are placed in a queue and executed asynchronously. (JS engine’s thoughts on GUI thread write operations)
So how do you implement this? To execute asynchronously, you need to create an asynchronous task, and setTimeout is the best choice.
// Reactive data modification
this.showModal = true
// Record the view that needs to be re-rendered
const queue = []
const flag = false
/ / triggers the setter
function setter() {
// Record the components to render
queue.push(this.render)
if (flag) return
flag = true
setTimeout(() = > {
queue.forEach(render= > render())
flag = false})}Copy the code
What’s wrong with this implementation?
// Task queue
const queueTask = [addEventListener, setTimeout, ajax]
Copy the code
Using the above example, there are now three tasks in the task queue, and the Vue responsive changes are made in the first task, addEventListener.
Assuming that setTimeout completes immediately, the task queue now looks like this:
// Task queue
const queueTask = [addEventListener, setTimeout, ajax, vueRender]
Copy the code
This result is consistent with the running logic of the task queue, but not what we want.
Because the view update code is too late, remember that instead of executing the next task immediately after each task is executed, requestAnimationFrame, render the view, check the remaining time to perform requestIdleCallback, etc.
In this order of execution, vueRender’s code is executed after the page has been rendered twice.
What we want to achieve is that the asynchronous code should ideally be executed as soon as the current task is completed. The ideal task queue would look like this.
// Task queue
const queueTask = [addEventListener, vueRender, setTimeout, ajax]
Copy the code
This is equivalent to adding an insert queue to the macro task queue, but if you change it that way, the whole thing is messed up. The previous asynchronous tasks had a first come, first served order, so the order of asynchronous tasks was completely out of control.
To sum up the above questions
- We want to be able to create asynchronous tasks that are faster and more efficient because the execution is too granular and there are too many things to do between tasks.
- Now the task queue logic cannot move.
- The JS engine itself does not have the ability to create asynchronous tasks.
- In this example, the asynchronous task that needs to be executed has nothing to do with any other thread, and we just want to optimize performance with the asynchronous task.
The solution
Since the logic of the previous task queue could not move, it was better to add a new queue: the microtask queue.
Asynchronous tasks created by the JS engine are put into this microtask queue. Asynchronous tasks created from other threads are placed in the same queue as before (the macro task queue).
The microtask queue is emptied after the macro task is executed.
After joining the microtask queue, the code of the JS engine is implemented:
// Macro task queue
const macroTask = []
// Microtask queue
const microTask = []
while(true) {
if (macroTask.length) {
const task = macroTask.shift()
task()
// Clear the microtask queue after macro task execution
while(microTask.length) {
const micro = microTask.shift()
micro()
}
requestAnimationFrame()
render()
requestIdleCallback()
}
}
Copy the code
Note the implementation of the while loop, which executes until the queue is empty as long as there are tasks in the microtask queue. That is, if a new microtask is created during the execution of the microtask (pushing a new value into the microtask queue), the new microtask will also be executed in the while loop
// microtask queue = []
Promise.resolve()
.then(() = > {
console.log(1)
Promise.resolve()
.then(() = > {
console.log(2)})})// microtask queue = [log1 function body]
// function body = microtask queue. Shift ()
// microtask queue = []
// log1 function body ()
// microtask queue = [log2 function body]
// log2 function body = microtask queue shift()
// microtask queue = []
// Render the view
Copy the code
So that’s why we have microtask queues, and the logic for running microtask queues.
Then, MutationObserver, setImmediate(IE, Nodejs), MessagePort.onMessage
That’s the loop of events. Thank you for watching.