Let JavaScript "Sleep" for a While
In many programming languages, such as Python, we can easily pause a program for 3 seconds using time.sleep(3)
. However, 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.
Let's look at our final implementation code, which is also the core of our discussion today:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Main business logic: an asynchronous function
*/
async function run() {
console.log('B: run function starts executing, preparing to serve the 1st customer.');
let i = 0;
while (i < 5) {
i += 1;
console.log(`C: [Loop ${i}] Ordering begins. Give 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 will leave here to execute other synchronous code
await sleep(3000);
// After 3 seconds, the JS engine will return here to continue execution
console.log(`E: [Loop ${i}] The food is ready! The waiter comes back and serves the food 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, and now he immediately returns to continue processing the main task, instead of waiting foolishly. The main task is completed.');
When running this code for the first time, the output order may 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 be executed completely before it's the turn of the subsequent code? To understand all this, we need to introduce the core execution model of JavaScript.
JS is Single-Threaded, Like a Restaurant
Imagine the world of JavaScript as an efficiently operating single-threaded restaurant:
- 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. The "order board" in his hand is the Call Stack, recording the tasks currently being executed.
- Kitchen (Web APIs): An independent department with many chefs. They specialize in handling time-consuming tasks, such as network requests, file reading and writing, and our protagonist -
setTimeout
timer. The work of the kitchen does not take up the waiter's time. - Food Delivery Window (Task Queue / Callback Queue): The dishes made by the kitchen (callback functions after asynchronous tasks are completed) are placed here, waiting for the waiter to pick them up.
- Waiter's Work Rules (Event Loop):
- Clear the Work at Hand: The waiter will prioritize processing all synchronous tasks on the "order board".
- Check the Food Delivery Window: When the "order board" is empty, the waiter will take a look at the "food delivery window".
- Add New Dishes: If there is food in the food delivery window, take one dish (a callback task) and put it on your "order board" to start serving.
- Loop Back and Forth: The waiter never rests and continues to repeat steps 2 and 3. This is the famous Event Loop.
The Cornerstone of async/await
: Understanding Promise
The above sample code has async
and await
keywords, what are these, why use them?
Before we dive into the magic of async/await
, we must first understand its cornerstone - Promise
. async/await
is just a "syntax sugar" that makes Promise
more comfortable to use.
1. What is a Promise? - A Commitment to the Future
Imagine you go to a fast food restaurant to order food, the waiter will not let you wait in front of the counter for the burger to be made. He will give you a receipt, and say: "I promise that after the burger is made, come and pick it up with this receipt."
This receipt is the Promise
. It represents an asynchronous operation that will only produce results in the future. It has three states:
- Pending: You just got the receipt, and the burger is still being made. This is the initial state.
- Fulfilled: The burger is ready! You can pick up the food with the receipt. This promise has been fulfilled.
- Rejected: The kitchen found that there was no bread and couldn't make a burger. This promise was rejected.
2. How to use Promise? - .then()
and .catch()
After getting the receipt, you won't just stand there, but can play with your phone first. But you need to know what to do next:
.then(onFulfilled)
: You tell yourself, "then, if the burger is ready (fulfilled), I will pick it up and eat it.".catch(onRejected)
: You also made the worst plan, "If (catch) they tell me they can't make it (rejected), I will complain."
In code, it looks like this:
// This is an asynchronous operation that simulates "making a burger", it returns a Promise
function makeBurger() {
return new Promise((resolve, reject) => {
console.log('The kitchen starts making burgers...');
// Simulate taking 2 seconds
setTimeout(() => {
if (Math.random() > 0.2) { // 80% success rate
resolve('Delicious burger'); // Success! Call resolve
} else {
reject('No bread!'); // Failure! Call reject
}
}, 2000);
});
}
// Order food and handle follow-up
makeBurger()
.then(burger => {
console.log('Successfully got:', burger);
})
.catch(error => {
console.error('An error occurred:', error);
});
console.log('I got the receipt, I\'ll go play with my phone first~');
Running this code, you will find that "I got the receipt..."
will be printed immediately, and the result of the burger will be printed after 2 seconds. This is asynchronous!
3. The Relationship between Promise
and sleep
Function
Now look at our sleep
function again, and it becomes clear:
function sleep(ms) {
// 1. new Promise: Returns a "promise receipt"
return new Promise(resolve => {
// 2. setTimeout: Put an asynchronous task to the kitchen (Web APIs)
// Task is: after ms milliseconds, call resolve()
setTimeout(resolve, ms);
});
}
sleep(3000)
is promising: "I guarantee that after 3000 milliseconds, this promise will become fulfilled
." It has no failure (reject
) situation, it is a simple promise that will only succeed.
4. Evolution from .then()
to await
Using .then()
chain calls to handle multiple asynchronous tasks can easily form "callback hell" when the logic is complex, and the readability of the code becomes poor.
// Callback hell style
sleep(1000)
.then(() => {
console.log('1 second passed');
return sleep(1000); // Return new Promise
})
.then(() => {
console.log('Another second passed');
return sleep(1000);
})
.then(() => {
console.log('A total of 3 seconds passed');
});
async/await
was born to solve this problem. await
is like a magic button that can automatically "pause" and "wait" for the Promise
receipt to be cashed, and then automatically execute the next line of code, making the asynchronous process look as clear as synchronous:
async function doSomething() {
await sleep(1000);
console.log('1 second passed');
await sleep(1000);
console.log('Another second passed');
await sleep(1000);
console.log('A total of 3 seconds passed');
}
Promise
is the core object of JavaScript for handling asynchronous operations, it represents a promise.async/await
is an elegant syntax for controlling the Promise process. Now, with a deep understanding ofPromise
, let's look at the previous "restaurant waiter model" again, and everything will be logical.await
is waiting for thePromise
receipt to change from "in progress" to "successful".
Code Execution Panoramic Perspective: Step-by-Step Tracking of the Waiter's Footprints
Now, let's follow the waiter and walk through the code execution flow step by step.
async function run() {
console.log('B: run function starts executing, preparing to serve the 1st customer.');
let i = 0;
while (i < 5) {
i += 1;
console.log(`C: [Loop ${i}] Ordering begins. Give 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 will leave here to execute other synchronous code
await sleep(3000);
// After 3 seconds, the JS engine will return here to continue execution
console.log(`E: [Loop ${i}] The food is ready! The waiter comes back and serves the food 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, and now he immediately returns to continue processing the main task, instead of waiting foolishly. The main task is completed.');
Phase 1: Fast Processing of Main Tasks (T ≈ 0 seconds)
console.log('A: ...')
:- The waiter receives the first task: print log
A
. This is a synchronous task, and 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
run
function. He immediately enters therun
function.
- The waiter receives the second task: execute the
console.log('B: ...')
:- Inside the
run
function, the waiter executes the first line of code, printing logB
. - Console Output:
B: run function starts executing, preparing to serve the 1st customer.
- Inside the
while
Loop (1st time) &console.log('C: ...')
:- The waiter enters the loop and prints log
C
. - Console Output:
C: [Loop 1] Ordering begins. Give the order to the chef...
- The waiter enters the loop and prints log
await sleep(3000)
- Key Turning Point!:- The waiter encounters
await
. He did two things: a. Executesleep(3000)
: This is equivalent to giving an order "remind me after 3 seconds" to the kitchen (Web APIs). The kitchen's timer starts timing, which is not the waiter's business. b. Theawait
keyword tells the waiter: "Therun
function task is paused, you can leave!" - Result: The waiter "suspends" the
run
function, and then immediately withdraws and returns to the main task to see if there are any other things on his "order board".
- The waiter encounters
console.log('D: ...')
:- The waiter finds that there is one last line of code in the main task that has not been executed! He handles it immediately.
- Console Output:
D: The waiter has given the 1st customer's order to the kitchen...
- So far, we have explained why
D
is printed in advance.await
does not block everything, it only pauses theasync
function it is in and returns control to the caller.
Phase 2: Waiting and Awakening (T = 0 to 3 seconds)
- At this time, all the main synchronous code has been executed. The waiter's "order board" is empty.
- According to the work rules, he starts constantly polling and checking the "food delivery window" (Task Queue).
- The kitchen is timing the first
sleep
order, and the food delivery window is empty. The waiter is in an "idle but vigilant" waiting state.
Phase 3: Callback and Loop of Asynchronous Tasks (T ≥ 3 seconds)
When T=3 seconds,
setTimeout
is completed:- The kitchen's timer rang! The chef put a notification "can continue to execute the
run
function" (that is, theresolve
function) into the food delivery window (Task Queue).
- The kitchen's timer rang! The chef put a notification "can continue to execute the
Event Loop Response:
- The waiter immediately finds that there is a new task in the food delivery window! He retrieves this task and returns to where the
run
function was paused.
- The waiter immediately finds that there is a new task in the food delivery window! He retrieves this task and returns to where the
run
Function Resumes Execution:console.log('E: ...')
: The waiter continues to work from the next line ofawait
and prints logE
.- Console Output:
E: [Loop 1] The food is ready! ...
while
Loop (2nd time):- The loop continues, and the waiter prints the next log
C
. await sleep(3000)
(2nd time): History repeats itself! The waiter once again hands over the new order to the kitchen, and is once again "kicked" out of therun
function byawait
.
- The loop continues, and the waiter prints the next log
What does the waiter do during the second pause?
- He returns to the main line again. But all the main tasks have already been completed.
- He checks the food delivery window again. The kitchen just received a new order, and the food delivery window is also empty.
- Therefore, from T=3.01 seconds to T=6 seconds, the waiter enters the "idle waiting" state again, waiting for the next
setTimeout
to complete.
This loop of "execution -> await
-> pause -> wait -> wake up -> continue execution" will continue until the while
loop ends.
- Loop ends,
console.log('F: ...')
:- When all 5 loops are completed, the
run
function continues to execute downward and prints the final logF
. - At this time, the
async
functionrun
is truly executed.
- When all 5 loops are completed, the
Deepen Knowledge Points
async
Keyword:- It marks an ordinary function as an asynchronous function.
- Calling an
async
function will immediately return a Promise object without waiting for all the code inside the function to be executed.
await
Keyword:- Can only be used inside
async
functions. - It will pause the execution of the current
async
function and wait for the expression after it (usually a Promise) to becomefulfilled
. await
is the syntax sugar for asynchronous flow control, it makes asynchronous code look as intuitive as synchronous code, but its underlying layer is still based on Promise and event loop.
- Can only be used inside
The Essence of Non-Blocking: JavaScript's single thread implements a "non-blocking" concurrent model through the event loop mechanism. When encountering time-consuming tasks such as I/O operations or timers, the main thread will hand it over to other modules (such as Web APIs) for processing, and it will continue to execute subsequent code, thereby ensuring the smoothness of the UI and the responsiveness of the program.
async/await
is the superstructure of this model.Execution Timing Discrimination:
- The code outside an
async
function will be executed immediately after the function's firstawait
and yield control. - The code inside an
async
function afterawait
must wait for theawait
Promise to complete, and then be put back into the main thread for execution through the scheduling of the event loop.
- The code outside an