Let JavaScript "Sleep" for a While
In many programming languages, like Python, we can easily pause the program for 3 seconds using time.sleep(3). But in JavaScript, it's not that simple. If we use a "busy-waiting" loop to block the main thread, the entire browser page will freeze, which is absolutely unacceptable.
Our goal is to implement a non-blocking sleep function that can "pause" the execution of a piece of code without freezing the entire program.
Here is our final implementation, which is the core of today's discussion:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Main business logic: an asynchronous function
*/
async function run() {
console.log('B: The run function starts executing, ready to serve the 1st customer.');
let i = 0;
while (i < 5) {
i += 1;
console.log(`C: [Loop ${i}] Order taking starts. Hand the order to the chef and tell him to notify me when it's ready.`);
// await will pause the run function, but the JS engine leaves here to execute other synchronous code
await sleep(3000);
// After 3 seconds, the JS engine returns here to continue execution
console.log(`E: [Loop ${i}] The dish is ready! The waiter returns and serves the dish to the customer.`);
}
console.log(`F: All 5 customers have been served, the run function has completely finished executing.`);
}
// --- Script Main Line (Global Scope) ---
console.log('A: The waiter (JS main thread) starts the day\'s work.');
run();
console.log('D: The waiter has given the 1st customer\'s order to the kitchen. Now he immediately returns to continue handling the main line tasks, instead of waiting foolishly. Main line tasks are complete.');The first time you run this code, the output order might confuse beginners:
A -> B -> C (Loop 1) -> D -> (Wait 3 seconds) -> E (Loop 1) -> C (Loop 2) -> (Wait 3 seconds) -> E (Loop 2) -> C (Loop 3) -> (Wait 3 seconds) -> E (Loop 3) ... -> F
Why does D cut in line before E? Shouldn't the run() function finish executing before the subsequent code runs? To understand this, we need to introduce JavaScript's core execution model.
JS is Single-Threaded, Like a Server Restaurant
Imagine the world of JavaScript as an efficiently operating single-threaded restaurant:
- The Waiter (JavaScript Main Thread / Call Stack): There is only one waiter in the restaurant. He is extremely fast but can only handle one thing at a time. His "order pad" is the Call Stack, recording the tasks currently being executed.
- The Kitchen (Web APIs): An independent department with many chefs. They specialize in handling time-consuming tasks like network requests, file I/O, and our main character—the
setTimeouttimer. The kitchen's work does not occupy the waiter's time. - The Pickup Counter (Task Queue / Callback Queue): Dishes prepared by the kitchen (callback functions after asynchronous tasks complete) are placed here, waiting for the waiter to pick them up.
- The Waiter's Work Rule (Event Loop):
- Finish Current Work: The waiter prioritizes completing all synchronous tasks on his "order pad."
- Check the Pickup Counter: When the "order pad" is empty, the waiter glances at the "pickup counter."
- Serve New Dishes: If there are dishes at the pickup counter, he takes one (a callback task) and places it on his "order pad" to start serving.
- Repeat: The waiter never rests, continuously repeating steps 2 and 3. This is the famous Event Loop.
The Foundation of async/await: Understanding Promise
The example code above contains the keywords async and await. What are they, and why use them?
Before we delve into the magic of async/await, we must first understand its foundation—Promise. async/await is just "syntactic sugar" that makes using Promise more comfortable.
1. What is a Promise? — A Promise for the Future
Imagine you order food at a fast-food restaurant. The waiter doesn't make you wait at the counter for the burger to be made. He gives you a receipt and says: "I promise (Promise), when the burger is ready, come get it with this receipt."
This receipt is the Promise. It represents an asynchronous operation whose result will be available in the future. It has three states:
- Pending: You just got the receipt; the burger is still being made. This is the initial state.
- Fulfilled: The burger is ready! You can pick it up with the receipt. The promise has been fulfilled.
- Rejected: The kitchen ran out of buns and can't make the burger. The promise has been rejected.
2. How to Use a Promise? — .then() and .catch()
After getting the receipt, you don't just stand there; you can play on your phone. But you need to know what to do next:
.then(onFulfilled): You tell yourself, "Then, if the burger is ready (fulfilled), I'll go pick it up and eat it.".catch(onRejected): You also prepare for the worst: "In case (catch) they tell me it can't be made (rejected), I'll go complain."
In code, it looks like this:
// This is an asynchronous operation simulating "making a burger," it returns a Promise
function makeBurger() {
return new Promise((resolve, reject) => {
console.log('Kitchen starts making the burger...');
// Simulate taking 2 seconds
setTimeout(() => {
if (Math.random() > 0.2) { // 80% success rate
resolve('A delicious burger'); // Success! Call resolve
} else {
reject('No buns left!'); // Failure! Call reject
}
}, 2000);
});
}
// Place the order and handle the outcome
makeBurger()
.then(burger => {
console.log('Successfully got:', burger);
})
.catch(error => {
console.error('Error:', error);
});
console.log('I got the receipt, going to play on my phone~');When you run this code, you'll see "I got the receipt..." printed immediately, and the burger result will print 2 seconds later. This is asynchrony!
3. The Relationship Between Promise and the sleep Function
Now, looking back at our sleep function, it becomes clear:
function sleep(ms) {
// 1. new Promise: Returns a "promise receipt"
return new Promise(resolve => {
// 2. setTimeout: Hands an asynchronous task to the kitchen (Web APIs)
// The task is: call resolve() after ms milliseconds
setTimeout(resolve, ms);
});
}sleep(3000) is promising: "I guarantee that after 3000 milliseconds, this promise will become fulfilled." It has no failure (reject) case; it's a simple promise that only succeeds.
4. The Evolution from .then() to await
Using .then() chaining to handle multiple asynchronous tasks can lead to "callback hell" when the logic is complex, making the code less readable.
// Callback hell style
sleep(1000)
.then(() => {
console.log('1 second passed');
return sleep(1000); // Return a new Promise
})
.then(() => {
console.log('Another second passed');
return sleep(1000);
})
.then(() => {
console.log('Total of 3 seconds passed');
});async/await emerged to solve this problem. await is like a magic button that automatically "pauses" and "waits" for that Promise receipt to be fulfilled, then automatically executes the next line of code, making asynchronous flow look as clear as synchronous code:
async function doSomething() {
await sleep(1000);
console.log('1 second passed');
await sleep(1000);
console.log('Another second passed');
await sleep(1000);
console.log('Total of 3 seconds passed');
}
Promiseis the core object in JavaScript for handling asynchronous operations; it represents a promise.async/awaitis an elegant syntax for controlling Promise flow. Now, with a deep understanding ofPromise, looking back at the "restaurant waiter model," everything falls into place.awaitis precisely waiting for thatPromisereceipt to change from "pending" to "fulfilled."
Code Execution Panorama: Step-by-Step Tracking of the Waiter's Footsteps
Now, let's follow the waiter step by step through the code execution flow.
async function run() {
console.log('B: The run function starts executing, ready to serve the 1st customer.');
let i = 0;
while (i < 5) {
i += 1;
console.log(`C: [Loop ${i}] Order taking starts. Hand the order to the chef and tell him to notify me when it's ready.`);
// await will pause the run function, but the JS engine leaves here to execute other synchronous code
await sleep(3000);
// After 3 seconds, the JS engine returns here to continue execution
console.log(`E: [Loop ${i}] The dish is ready! The waiter returns and serves the dish to the customer.`);
}
console.log(`F: All 5 customers have been served, the run function has completely finished executing.`);
}
// --- Script Main Line (Global Scope) ---
console.log('A: The waiter (JS main thread) starts the day\'s work.');
run();
console.log('D: The waiter has given the 1st customer\'s order to the kitchen. Now he immediately returns to continue handling the main line tasks, instead of waiting foolishly. Main line tasks are complete.');
Phase One: Rapid Processing of Main Line Tasks (T ≈ 0 seconds)
console.log('A: ...'):- The waiter receives the first task: print log
A. This is a synchronous task; he completes it immediately. - Console Output:
A: The waiter (JS main thread) starts the day's work.
- The waiter receives the first task: print log
run():- The waiter receives the second task: execute the
runfunction. He immediately enters therunfunction.
- The waiter receives the second task: execute the
console.log('B: ...'):- Inside the
runfunction, the waiter executes the first line of code, printing logB. - Console Output:
B: The run function starts executing, ready to serve the 1st customer.
- Inside the
whileLoop (1st iteration) &console.log('C: ...'):- The waiter enters the loop and prints log
C. - Console Output:
C: [Loop 1] Order taking starts. Hand the order to the chef...
- The waiter enters the loop and prints log
await sleep(3000)- The Critical Turning Point!:- The waiter encounters
await. He does two things: a. Executessleep(3000): This is equivalent to handing a "remind me in 3 seconds" order to the Kitchen (Web APIs). The kitchen's timer starts counting down; this is no longer the waiter's concern. b. Theawaitkeyword tells the waiter: "Pause therunfunction task for now, you can leave!" - Result: The waiter "suspends" the
runfunction and immediately leaves, returning to the main line tasks to check if there's anything else on his "order pad."
- The waiter encounters
console.log('D: ...'):- The waiter finds there's still one last line of code in the main line tasks not executed! He handles it immediately.
- Console Output:
D: The waiter has given the 1st customer's order to the kitchen... - This explains why
Dis printed early.awaitdoes not block everything; it only pauses theasyncfunction it's in and returns control to the caller.
Phase Two: Waiting and Waking (T = 0 to 3 seconds)
- At this point, all main line synchronous code has been executed. The waiter's "order pad" is empty.
- Following the work rule, he starts constantly polling the "pickup counter" (Task Queue).
- The kitchen is timing the first
sleeporder; the pickup counter is empty. The waiter is in an "idle but alert" waiting state.
Phase Three: Callback of Asynchronous Tasks and Looping (T ≥ 3 seconds)
At T=3 seconds,
setTimeoutcompletes:- The kitchen timer rings! The chef places a notification "can continue executing the
runfunction" (i.e., theresolvefunction) into the Pickup Counter (Task Queue).
- The kitchen timer rings! The chef places a notification "can continue executing the
Event Loop Response:
- The waiter immediately notices a new task at the pickup counter! He retrieves this task and returns to where the
runfunction was paused.
- The waiter immediately notices a new task at the pickup counter! He retrieves this task and returns to where the
runFunction Resumes Execution:console.log('E: ...'): The waiter continues working from the line afterawait, printing logE.- Console Output:
E: [Loop 1] The dish is ready!...
whileLoop (2nd iteration):- The loop continues; the waiter prints the next log
C. await sleep(3000)(2nd time): History repeats! The waiter hands a new order to the kitchen and is again "kicked out" of therunfunction byawait.
- The loop continues; the waiter prints the next log
What does the waiter do during the second pause?
- He returns to the main line again. But the main line tasks were completed long ago.
- He checks the pickup counter again. The kitchen just received a new order; the pickup counter is also empty.
- So, from T=3.01 seconds to T=6 seconds, the waiter enters the "idle waiting" state again, waiting for the next
setTimeoutto complete.
This cycle of "execute -> await -> pause -> wait -> wake -> continue execution" repeats until the while loop ends.
- Loop Ends,
console.log('F: ...'):- After all 5 loops complete, the
runfunction continues downward, printing the final logF. - At this point, the
asyncfunctionrunis truly finished executing.
- After all 5 loops complete, the
Deepening Knowledge Points
asyncKeyword:- It marks a regular function as an asynchronous function.
- Calling an
asyncfunction immediately returns a Promise object, without waiting for all the code inside the function to finish executing.
awaitKeyword:- Can only be used inside an
asyncfunction. - It pauses the execution of the current
asyncfunction, waiting for the following expression (usually a Promise) to becomefulfilled. awaitis syntactic sugar for asynchronous flow control. It makes asynchronous code look as intuitive as synchronous code, but its underlying mechanism is still based on Promise and the event loop.
- Can only be used inside an
The Essence of Non-Blocking: JavaScript's single-threaded nature, through the event loop mechanism, achieves a "non-blocking" concurrency model. When encountering I/O operations or timers and other time-consuming tasks, the main thread hands them off to other modules (like Web APIs) for processing, while itself continues executing subsequent code, thus ensuring UI fluidity and program responsiveness.
async/awaitis the superstructure built upon this model.Execution Timing Analysis:
- Code outside an
asyncfunction will immediately get executed after the function firstawaits and yields control. - Code inside an
asyncfunction after anawaitmust wait for theawaited Promise to complete and, through the scheduling of the event loop, be placed back onto the main thread for execution.
- Code outside an
