Before we talk about the browser event loop, let’s consider a few questions:
- How does a single thread handle asynchronous operations?
- What exactly is an event loop?
- What are tasks and microtasks?
Single threaded and asynchronous
Why single thread?
Today’s browsers are multi-process architectures. Take Chrome as an example, including browser processes, renderers, web processes… And so on. I mentioned it in my previous notes, so I won’t repeat it. So why are browsers single-threaded? In fact, the single thread here refers to the main thread of the browser rendering page, HTML parsing, CSS calculation, JS code execution, layout, drawing, etc., are all completed by the main thread, that is, any one of the tasks timed out will block the execution of subsequent tasks.
Synchronous code and asynchronous tasks
JavaScript tasks are divided into synchronous tasks and asynchronous tasks. When the main thread interprets and executes the JavaScript code, the synchronous task enters the execution stack in turn, and the asynchronous task is added to the asynchronous task queue, and the asynchronous marker is added to wait for execution
Perform stack and task scheduling
Synchronization tasks enter the execution stack in turn and store function parameters, context, and so on in a data structure called variable objects. After the function completes execution, the variable object is popped from the execution stack.
The next synchronization task is pushed onto the execution stack until all synchronization tasks are completed
Event loop
After a synchronous task is executed, the main thread traverses the asynchronous task queue and determines whether the task can be executed based on the asynchronous task tag. For example, setTimeout adds callback to the execution stack when the time of the scheduled task expires. The main thread polling the asynchronous task queue execution process is an event loop. Multiple event loops can occur within an execution frame (approximately 16.6ms at 60FPS), depending on the browser refresh rate and the complexity of asynchronous task time
Task and Microtask
So the event loop is clear, but what are macro tasks and micro tasks? In fact, the asynchronous task queue is a macro task queue, while the micro task queue is based on the task queue generated during the execution of macro tasks. Common tasks are classified as follows
- Macro task: JavaScript main program, scheduled task (setTimeout, setInterval), I/O operation, UI render, etc.
- Microtask: Promise callback (.then and.cache), queueMicrotask (draft), Object.observe, MutationObserver, process.nexttick in NodeJS, etc
Main thread scheduling single macro task execution process:
- First, execute the macro task body code;
- When the macro task is executed, the micro task is generated and added to the micro task queue.
- After the macro task body code is executed, the microtasks generated by the execution are traversed.
- If another microtask is generated during the execution of the microtask, it is added to the microtask queue.
- Until all the microtasks generated by the macro task are completed, that is, the microtask queue is cleared.
- Continue the event loop to execute the next executable macro task…
Microtask queue execution uses a first-in-first-out rule, but the microtask queue is not a real queue structure, it is a set. Refer to the HTML document here
Read many articles that micro tasks take precedence over macro tasks, which is not accurate. As far as individual macro tasks are concerned, the body code must take precedence over the microtask execution, because the creation of the microtask depends on the body execution of the macro task. Without a parent, there can be no child, wouldn’t you say? From the macro task queue perspective, the microtask generated by the previous macro task must take precedence over the next macro task; this is the rule that JavaScript code executes sequentially.
Okay, event loops, macro tasks, micro tasks we’ve got it all figured out, so let’s check it out
// Main program console.log('script start') SetTimeout (function () {console.log('script timeout running')}, 0) Promise execution code also belongs to main program code const promise = new promise ((resolve, reject) => {// macro task generated during microtask execution, Log ('promise running') setTimeout(() => {console.log('promise timeout running')}, Then (() => {console.log('promise then1 running')) Promise.resolve().then(() => {console.log(' Promise then resolve running')})}) Promise.resolve().then(function () { Join queueMicrotask(() => console.log('queueMicrotask running')) console.log('promise then2 RUNNING ')}) Console. log('script end') // Main program execution endsCopy the code
The output