🚀 Explore more React Hooks possibilities? Visit www.reactuse.com for complete documentation and install via npm install @reactuses/core
to supercharge your React development efficiency!
Introduction
In front-end development, JavaScript's Event Loop mechanism is crucial for understanding asynchronous programming and optimizing performance. It's not only a frequently asked interview question but also the foundation for writing efficient, non-blocking code. However, many developers only have a superficial understanding of it, especially when modern asynchronous APIs like async/await
, Promise
, and requestAnimationFrame
are intertwined, making their execution order often confusing.
Today, we will delve into the mysteries of the JavaScript Event Loop through a classic ByteDance interview question. This question cleverly combines synchronous code, setTimeout
, requestAnimationFrame
, async/await
, and Promise
, aiming to test your comprehensive understanding of the event loop mechanism. Are you ready? Let's uncover this mystery together and make JavaScript's asynchronous execution no longer a puzzle!
Initial Code Exploration
Let's first look at the source code of this interview question:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
requestAnimationFrame(() => {
console.log("requestAnimationFrame");
});
async1();
new Promise(resolve => {
console.log("promise1");
resolve();
}).then(() => {
console.log("promise2");
});
console.log("script end");
Without running the code, can you accurately state the final output order? This is the secret that this article will reveal.
Deep Dive into JavaScript Event Loop Mechanism
To fully understand the execution order of the code above, we first need to have a clear understanding of JavaScript's event loop mechanism. JavaScript is a single-threaded language, which means it can only execute one task at a time. So, how does it handle time-consuming asynchronous operations (like network requests, timers, user interactions) without blocking the main thread? The answer is the event loop.
The core concepts of the event loop include:
1. Synchronous and Asynchronous Tasks
- Synchronous Tasks: Tasks that are queued and executed on the main thread. Only when the previous task is completed can the next task be executed. They directly enter the JavaScript engine's Call Stack and are executed immediately.
- Asynchronous Tasks: Tasks that do not enter the main thread directly but instead enter the "Task Queue". Only when all synchronous tasks in the main thread have been executed will the event loop take asynchronous tasks from the task queue and put them into the execution stack for execution.
2. Call Stack
The Call Stack is a Last-In, First-Out (LIFO) data structure used to store function calls. When a function is called, it is pushed onto the stack; when the function finishes execution, it is popped off the stack. The JavaScript engine continuously checks if the Call Stack is empty during code execution. If it is empty, it means all synchronous tasks have been executed.
3. Web APIs
Browsers (or Node.js environments) provide a series of Web APIs for handling asynchronous operations, such as setTimeout
, setInterval
, XMLHttpRequest
, DOM events, etc. When the JavaScript engine encounters these asynchronous APIs, it hands these tasks over to the corresponding Web API module for processing, without blocking the main thread. When the Web API finishes processing, it places the corresponding callback function into the task queue.
4. Task Queues
The task queue is where asynchronous task callback functions are stored. Depending on the type of task, task queues are divided into two types:
-
Macrotask Queue: Also known as the Task Queue or Callback Queue. It mainly includes:
-
setTimeout
-
setInterval
- I/O operations (e.g., file reading/writing, network requests)
- UI rendering (browser environment)
-
MessageChannel
-
setImmediate
(Node.js)
-
-
Microtask Queue: Has higher priority than the macrotask queue. It mainly includes:
-
Promise.then()
,.catch()
,.finally()
-
async/await
(essentially syntactic sugar for Promises) -
MutationObserver
-
process.nextTick
(Node.js)
-
5. Event Loop Mechanism
The event loop is JavaScript's "scheduler" for implementing asynchronous operations. Its workflow can be summarized in the following steps:
- Execute Synchronous Code: The JavaScript engine first executes all synchronous tasks in the Call Stack until the Call Stack is empty.
- Execute Microtasks: When the Call Stack is empty, the event loop checks the microtask queue. If the microtask queue is not empty, it executes all microtasks at once until the microtask queue is empty. During the execution of microtasks, if new microtasks are generated, these new microtasks are also added to the end of the current microtask queue and executed in the current loop.
- Execute Macrotask: When the microtask queue is empty, the event loop takes one macrotask from the macrotask queue (note: only one macrotask is taken per loop) and puts it into the execution stack for execution.
- Repeat Loop: Steps 2 and 3 above are repeated continuously, forming a loop, until all tasks are executed.
In short, the priority of the event loop is: Synchronous Tasks > Microtasks > Macrotasks. After each macrotask is executed, the microtask queue is cleared, and then the next macrotask is executed. This is key to understanding the execution order of complex asynchronous code.
In-depth Analysis of the ByteDance Interview Question
Now, let's combine our knowledge of the event loop and analyze the execution order of the code in this interview question step by step. Remember, the event loop in a browser environment includes a rendering phase, and the execution timing of requestAnimationFrame
(RAF) is closely related to rendering.
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
requestAnimationFrame(() => {
console.log("requestAnimationFrame");
});
async1();
new Promise(resolve => {
console.log("promise1");
resolve();
}).then(() => {
console.log("promise2");
});
console.log("script end");
Detailed Execution Steps:
-
console.log("script start");
- This is synchronous code, executed immediately.
- Output:
script start
-
setTimeout(() => { console.log("setTimeout"); }, 0);
-
setTimeout
is a macrotask. It is added to Web APIs, and after 0 milliseconds (or as quickly as possible), its callback function is placed into the macrotask queue. - Current State: Macrotask Queue:
[setTimeout callback]
-
-
requestAnimationFrame(() => { console.log("requestAnimationFrame"); });
-
requestAnimationFrame
(RAF) is a special asynchronous task that does not belong to the macrotask or microtask queue. Its callback will be executed before the browser's next repaint. - Current State: RAF Queue:
[requestAnimationFrame callback]
-
-
async1();
- Call
async1
function, enter the function. -
console.log("async1 start");
: Synchronous code, executed immediately. - Output:
async1 start
-
await async2();
:- Call
async2
function, enter the function. -
console.log("async2");
: Synchronous code, executed immediately. - Output:
async2
-
async2
function finishes execution and returns a resolved Promise.await
pauses the execution ofasync1
and adds the code afterawait
inasync1
(i.e.,console.log("async1 end");
) as a microtask to the microtask queue.
- Call
- Current State: Microtask Queue:
[async1 remaining code]
- Call
-
new Promise(resolve => { console.log("promise1"); resolve(); }).then(() => { console.log("promise2"); });
- The
Promise
constructor is executed synchronously. -
console.log("promise1");
: Synchronous code, executed immediately. - Output:
promise1
-
resolve()
is called, and thePromise
state becomes resolved. The.then()
callback function (i.e.,console.log("promise2");
) is added to the microtask queue. - Current State: Microtask Queue:
[async1 remaining code, promise2 callback]
- The
-
console.log("script end");
- This is synchronous code, executed immediately.
- Output:
script end
At this point, all synchronous code has finished execution, and the Call Stack is empty. The event loop begins checking the microtask queue.
-
Execute Microtask Queue
- The event loop takes the first microtask from the microtask queue: the code after
await
inasync1
. -
console.log("async1 end");
: Executed. - Output:
async1 end
- The event loop continues to take the next microtask from the microtask queue: the
promise2
callback. -
console.log("promise2");
: Executed. - Output:
promise2
- The microtask queue is empty.
- The event loop takes the first microtask from the microtask queue: the code after
-
Browser Rendering Phase (if any)
- Before executing the next macrotask, the browser may perform a render. At this time, if there are RAF callbacks, they will be executed before rendering.
-
console.log("requestAnimationFrame");
: Executed. - Output:
requestAnimationFrame
- The RAF queue is empty.
-
Execute Macrotask Queue
- The event loop takes the first macrotask from the macrotask queue: the
setTimeout
callback. -
console.log("setTimeout");
: Executed. - Output:
setTimeout
- The macrotask queue is empty.
- The event loop takes the first macrotask from the macrotask queue: the
Final Output Order:
script start
async1 start
async2
promise1
script end
async1 end
promise2
requestAnimationFrame
setTimeout
This order perfectly demonstrates the priority of synchronous code, microtasks executing immediately after the current macrotask, and requestAnimationFrame
executing before rendering.
The Uniqueness of requestAnimationFrame
: Independent Animation Queue
In the code analysis above, we noticed that the requestAnimationFrame
(RAF) callback function executes after all microtasks are completed, but before the setTimeout
macrotask. This is not accidental, but rather determined by RAF's unique mechanism.
What is requestAnimationFrame
?
requestAnimationFrame
is a browser API used to optimize animations and visual updates. It tells the browser that you want to perform an animation and requests the browser to call your specified callback function before the next repaint. Its main advantages are:
- Synchronization with Browser Refresh Rate: RAF callbacks execute when the browser is ready to render the next frame, typically 60 times per second (60fps). This means your animations are synchronized with the browser's rendering cycle, avoiding dropped frames or stuttering, and providing a smoother visual experience.
- Resource Saving: When the page is inactive (e.g., minimized or switched to a background tab), RAF automatically pauses, thereby saving CPU and battery resources.
- Avoiding Layout Jitter: Performing DOM operations within an RAF callback ensures that they are completed before the browser performs layout and painting, thereby reducing unnecessary reflows and repaints and improving performance.
RAF's Queue: Neither Macrotask nor Microtask
This is a common misconception: many people believe that RAF callbacks belong to either the macrotask or microtask queue. However, according to the WHATWG HTML standard, RAF callbacks belong to neither macrotasks nor microtasks. They have their own independent scheduling mechanism, often referred to as the "animation frame callback queue" or "rendering queue."
Execution Timing of RAF Callbacks:
In the browser's event loop, a complete cycle roughly follows these steps:
- Execute one macrotask (e.g., take a
script
task orsetTimeout
callback from the macrotask queue). - Execute all microtasks (until the microtask queue is empty).
- Execute
requestAnimationFrame
callbacks: Before the browser prepares for the next render, all registered RAF callbacks are executed. - The browser performs rendering (layout, painting).
- Repeat the above process, entering the next event loop cycle, and taking the next macrotask from the macrotask queue.
It is precisely because RAF callbacks execute after microtasks, before the next macrotask, and before browser rendering, that they can ensure smooth and efficient animations. It provides developers with an opportunity to perform visual updates at the optimal time.
Differences between RAF and setTimeout
:
Feature | requestAnimationFrame |
setTimeout(callback, 0) |
---|---|---|
Scheduling Time | Before the browser's next repaint | At least after the specified delay, placed in the macrotask queue, waiting for the main thread to be idle |
Frame Rate Sync | Yes, synchronized with browser refresh rate, usually 60fps | No, may lead to dropped frames or stuttering |
Resource Consumption | Pauses when page is inactive, saves resources | Attempts to execute even when page is inactive, may consume resources |
Animation Smoothness | Smoother, avoids layout jitter | May lead to unsmooth animations, prone to jitter |
Queue Type | Independent animation frame callback queue | Macrotask queue |
Understanding the independence of RAF and its execution timing is crucial for writing high-performance web animations and optimizing user experience.
Diagrams of Event Loop and RAF Queue
To more intuitively understand the mechanisms of JavaScript event loop and requestAnimationFrame
, we provide the following diagrams:
JavaScript Event Loop Overview
The diagram above illustrates the core components of the JavaScript event loop: the Call Stack, Web APIs, Microtask Queue, Macrotask Queue, and the Event Loop itself. Synchronous code executes in the Call Stack, while asynchronous tasks are processed by Web APIs and then enter the corresponding task queues, eventually being scheduled for execution by the event loop.
Event Loop Including requestAnimationFrame
This diagram further refines the event loop by incorporating the requestAnimationFrame
animation frame callback queue. It clearly depicts the execution order of macrotasks, microtasks, and RAF callbacks within an event loop cycle, as well as the timing of browser rendering. RAF callbacks execute after microtasks and before browser rendering, ensuring the smoothness of animations.
Conclusion and Reflection
Through this in-depth analysis of the ByteDance interview question, we have not only reviewed the core concepts of the JavaScript event loop, including synchronous tasks, asynchronous tasks, the call stack, Web APIs, macrotask queue, and microtask queue, but more importantly, we have understood how async/await
and Promise
utilize the microtask mechanism to achieve efficient asynchronous flow control, and the unique advantages and execution timing of requestAnimationFrame
as an independent animation queue.
Understanding the event loop is an essential path to becoming an excellent front-end developer. It can help you:
- Predict code execution order: Especially in complex asynchronous scenarios.
- Optimize performance: Avoid blocking the main thread and improve user experience.
- Debug asynchronous issues: Locate and resolve bugs caused by asynchronous execution order more quickly.
We hope this article helps you gain a deeper understanding of the JavaScript event loop mechanism and enables you to navigate future interviews and practical development with ease. Practice is the best teacher; we recommend you try different combinations of asynchronous code, observe their output, and deepen your understanding.
References:
Top comments (0)