JS下的Promise
Many tutorials either oversimplify or overcomplicate Promises. Here I’ll organize my understanding to reinforce my knowledge.
Why Promises Were Created
Promises were created in the realm of asynchronous programming to solve callback hell.
A.then(B).then(C)
This syntax is clearly better than nested callbacks.
Promise Usage Notes
Promises have three states:
pending
,fulfilled
, andrejected
Promise.then creates microtasks, and it’s important to know that
microtasks have higher execution priority than macrotasks
Microtasks are microscopic tasks, while macrotasks are macroscopic tasks. Tasks initiated by the host environment like window.setTimeout and JS events are macrotasks, but tasks initiated by JS itself like promises are microtasks
According to the JS event loop and message queue principles, when a macrotask finishes execution, the JS engine checks if there are any unexecuted microtasks within that macrotask. If there are, it continues executing them. Only when all microtasks are completed does it proceed to the next macrotask.
Implementing a Promise from Scratch
Here’s a simplified implementation of a Promise
function Bromise(executor) {
var onResolve_ = null;
var onReject_ = null;
this.then = function (onResolve, onReject) {
onResolve_ = onResolve;
};
function resolve(value) {
window.setTimeout(() => {
onResolve_(value);
}, 0);
}
executor(resolve, null);
}
function executor(resolve, reject) {
resolve(100);
}
var demo = new Bromise(executor);
function onResolve(value) {
console.log(value);
}
demo.then(onResolve);
Async/Await
When discussing Promises, it’s natural to continue with Async/Await. While Promises solved callback hell, they still weren’t simple enough to write. This led to the creation of Async/Await, which allows asynchronous code to be written in a synchronous style, making code more readable and better aligned with human linear thinking.
async/await is built on generators and Promises. Generators implement coroutines, allowing generator functions to be paused and resumed.
Event Loop/Message Queue
JS uses a single-threaded model, meaning it can only execute one thing at a time. However, we sometimes see multiple requests being processed simultaneously in the Networks tab. This is because while JS initiates these requests sequentially, the network process doesn’t have just one thread, and Chrome itself uses a multi-process model, allowing multiple requests to be processed concurrently. This doesn’t contradict JS’s single-threaded model.
Returning to the topic, the single-threaded nature means only one thing can be done at any moment. So what happens at each moment, and how are all tasks guaranteed to execute? This is where the browser’s event loop comes in, which can be simply understood as a while loop. The browser maintains a message queue that stores a bunch of tasks (macrotasks). The queue follows the basic first-in-first-out model, so the main thread of the browser’s rendering process executes tasks one by one.
However, if execution only followed this single task queue, there would be problems. Some asynchronous operations require timeliness, which led to the concept of microtasks. The total number of tasks in the queue remains the same, but individual macrotasks can have additional work added temporarily, hence the concept of microtasks. Microtasks were introduced to address the timeliness requirements of certain operations.
Of course, there isn’t just one message queue. The above describes the most common message queue. There’s also a delay queue - when the program encounters timer functions, it places information like callback time and callback function into the delay queue. When the timer expires, that callback function is pushed as a macrotask into the normal message queue.
Two Examples
Based on the theoretical knowledge above, let’s look at two examples
Q1
async function foo() {
console.log('foo');
}
async function bar() {
console.log('bar start');
await foo();
console.log('bar end');
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
bar();
new Promise(function (resolve) {
console.log('promise executor');
resolve();
}).then(function () {
console.log('promise then');
});
console.log('script end');
Execution process analysis:
- First execute console.log(‘script start’); prints “script start”
- Encounter timer, create a new task and place it in the
delay queue
- Execute bar function. Since bar is marked with async, when entering the function, the JS engine saves the current call stack and other information, then executes console.log(‘bar start’); in bar function, printing “bar start”
- Next execute await foo(); in bar function. Execute foo function. Since foo is also marked with async, when entering the function, the JS engine saves the current call stack and other information, then executes console.log(‘foo’); in foo function, printing “foo”
- When executing await foo(), a Promise object is automatically created
- During Promise creation, the resolve() function is called, and the JS engine places this task in the current macrotask’s microtask queue
- The JS engine then pauses the current coroutine’s execution and returns control of the main thread to the parent coroutine, while also returning the created Promise object to the parent coroutine
- After control returns to the parent coroutine, the parent coroutine calls the then() method of this Promise object to monitor its state changes
- Continue with the parent coroutine’s flow, execute new Promise(), print “promise executor”, call resolve function, and the JS engine adds this task to the end of the microtask queue
- Continue executing the parent coroutine’s flow, execute console.log(‘script end’);, printing “script end”
- The parent coroutine will then finish execution. Before finishing, it enters the microtask checkpoint and executes the microtask queue. There are two microtasks waiting to be executed. Execute the first microtask first, triggering the callback function in the first promise.then(), returning control of the main thread to the bar function’s coroutine. After the bar function’s coroutine is activated, it continues executing subsequent statements, executing console.log(‘bar end’);, printing “bar end”
- After the bar function coroutine finishes execution, execute the second microtask in the microtask queue, triggering the callback function in the second promise.then(). After this callback function is activated, execute console.log(‘promise then’);, printing “promise then”
- After execution completes, return control to the main thread. The current task finishes execution, take the task from the delay queue, execute console.log(‘setTimeout’);, printing “setTimeout”
Q2
Implement a traffic light that changes a circular div’s background color in a cycle: green for 3 seconds, yellow for 1 second, red for 2 seconds
Here’s my implementation code:
var lightEl = document.getElementsByClassName('light')[0];
function sleep(second) {
return new Promise(function (resolve, reject) {
window.setTimeout(function () {
resolve();
}, second * 1000);
});
}
async function trafficLight() {
lightEl.style.backgroundColor = 'green';
await sleep(3);
lightEl.style.backgroundColor = 'yellow';
await sleep(1);
lightEl.style.backgroundColor = 'red';
await sleep(2);
}
async function main() {
while (true) {
await trafficLight();
}
}
main();
Key points:
- Use await for pausing
- Use while loop for continuous execution
References
- Browser Working Principles and Practice - Li Bing
- Re-learning Frontend - winter
- https://github.com/LuckyWinty/blog/blob/master/markdown/Q%26A/Chrome%E6%B5%8F%E8%A7%88%E5%99%A8setTimeout%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E5%92%8C%E4%BD%BF%E7%94%A8%E6%B3%A8%E6%84%8F.md
Final Thoughts
Understanding Promises and the JavaScript event loop is fundamental to writing efficient asynchronous code. While Promises solved the callback hell problem, async/await made asynchronous programming even more intuitive. The key insights are:
- Promises provide a cleaner way to handle asynchronous operations compared to nested callbacks
- The event loop with microtasks and macrotasks ensures proper execution order
- Async/await builds on Promises and generators to make asynchronous code read like synchronous code
- Practical examples like the traffic light demonstrate how these concepts work together in real applications
Mastering these concepts will significantly improve your ability to write clean, efficient JavaScript code.