This article wants to talk about the application of synchronous and asynchronous operations in JavaScript in a simple example. We'll use the example of making a burger and show how to do it using synchronous methods, callbacks, and Promises with async/await.
Let’s imagine we’re trying to make a burger:
1. Get ingredients to get raw materials (such as beef)
2. Cook the beef
3. Get burger buns
4. Put the cooked beef between the buns
5. Serve the burger
Next, we use different methods to demonstrate the implementation of these steps.
1. Synchronization method
First, let's look at an example that implements a hamburger-making process using synchronous methods:
function getBeef() {
console.log("Step 1: Getting beef");
return "beef";
}
function cookBeef(beef) {
console.log("Step 2: Cooking beef");
if (beef === "beef")
return "patty";
}
function getBuns() {
console.log("Step 3: Getting buns");
return "buns";
}
function putBeefBetweenBuns(buns, patty){
if (buns === "buns" && patty === "patty") {
console.log("Step 4: Putting beef patty between buns");
return "burger"
}
}
function makeBurger() {
const beef = getBeef();
const patty = cookBeef(beef);
const buns = getBuns();
const burger = putBeefBetweenBuns(buns, patty);
return burger;
}
function serve(burger){
console.log("Finally: " + burger + " is served!")
}
const burger = makeBurger();
serve(burger);
In this example, we've used synchronous methods to implement the steps involved in making a hamburger. This method is very simple, there is nothing to talk about, but it may be limited when dealing with complex tasks, because it does not support asynchronous operations.
2. Callbacks
Next, let's look at an example of using a callback function to implement the hamburger making process:
function getBeef(cb) {
setTimeout(() => {
console.log("Step 1: Getting beef");
const beef = "beef";
cb(beef);
}, 1000);
}
function cookBeef(beef, cb) {
setTimeout(() => {
if(beef === "beef"){
console.log("Step 2: Cooking beef");
const patty = "patty"
cb(patty)
}
}, 1000)
}
function getBuns(cb) {
setTimeout(() => {
console.log("Step 3: Getting buns");
const buns = "buns";
cb(buns)
}, 1000)
}
function putBeefBetweenBuns(buns, patty, cb) {
setTimeout(() => {
if (buns === "buns" && patty === "patty") {
console.log("Step 4: Putting beef patty between buns");
const burger = "burger"
cb(burger)
}
}, 1000);
}
//关键部分
function makeBurger(cb) {
getBeef(function(beef) {
cookBeef(beef, function(patty)
getBuns(function(buns) {
putBeefBetweenBuns(buns, patty, function(burger) {
cb(burger);
});
});
});
});
}
function serve(burger){
console.log("Finally: Burger is served!")
}
// Make and serve the burger
makeBurger((burger) => {
serve(burger)
})
In order to understand the above example, let's talk about setTimeout
a function first, setTimeout
which is a JavaScript function that is used to execute a certain function or code fragment after a specified time delay. setTimeout
It will not block code execution itself, it will put the provided callback function (code that needs to be delayed) into a queue, and put it into the event loop after the timer ends, waiting for execution. Let me give you a simple example to explain setTimeout
how works:
console.log("Before setTimeout");
setTimeout(() => {
console.log("Inside setTimeout");
}, 2000);
console.log("After setTimeout");
When executing this code, you will find that the output sequence is as follows:
Before setTimeout
After setTimeout
大约 2 秒后,会出现:Inside setTimeout
In the second burger-making example, we use callback functions to handle asynchronous operations.
A callback function is simply a function that is passed as an argument to another function. When the called function completes its operation, it executes the passed callback function. In this burger making example, we use callback functions for each step's completion notification.
Here is a detailed explanation of the callback function example:
makeBurger()
The function is called, which first callsgetBeef()
the function, passing an anonymous callback function as an argument. This callback function receives one parameterbeef
.getBeef()
The function performs an asynchronous operation (usingsetTimeout()
a mock), and when the operation is complete, it calls the passed callback function, passedbeef
as an argument.- The callback function executes and
beef
calls the function with the parametercookBeef()
. Also, wecookBeef()
pass an anonymous callback function to the function, which receives a parameterpatty
. cookBeef()
The function performs an asynchronous operation, and when the operation is complete, it calls the passed callback function,patty
passing it as a parameter.- The callback function executes and
patty
calls the function with the parametergetBuns()
. WegetBuns()
pass an anonymous callback function to the function, receiving one parameterbuns
. getBuns()
The function performs an asynchronous operation, and when the operation is complete, it calls the passed callback function,buns
passing it as a parameter.- The callback function executes and calls the function with the
buns
and parameters . We pass an anonymous callback function to the function, receiving one parameter .patty
putBeefBetweenBuns()
putBeefBetweenBuns()
burger
putBeefBetweenBuns()
The function performs an asynchronous operation, and when the operation is complete, it calls the passed callback function,burger
passing it as a parameter.- The callback function executes,
burger
passing the parameter toserve()
the function. serve()
The function prints a message that the burger is ready and served.
In the process, we can see that the callback function is executed after each asynchronous operation is completed, and the result is passed to the next operation. This allows us to handle the entire burger making process asynchronously. However, the disadvantage of this approach is that callback functions may lead to too many layers of nesting, resulting in less readable code. Even if you don’t look carefully, you won’t be able to understand it for a long time, so don’t worry if you can’t understand this code. I think JavaScript’s own syntax and readability and comprehension are extremely rubbish. Don’t worry if you don’t understand it. Anyway, we have to To avoid Callback Hell like the one above, the most commonly used method is the following:
3. Promises and async/await
function getBeef() {
return new Promise((res) => {
setTimeout(() => {
console.log("Step 1: Getting beef");
res("beef");
}, 1000);
});
}
function cookBeef(beef) {
return new Promise((res, rej) => {
setTimeout(() => {
if (beef === "beef"){
console.log("Step 2: Cooking beef");
res("patty");
}
else
rej("no beef available");
}, 1000);
});
}
function getBuns() {
return new Promise((res) => {
setTimeout(() => {
console.log("Step 3: Getting buns");
res("buns");
}, 1000);
});
}
function putBeefBetweenBuns(buns, patty) {
return new Promise((res, rej) => {
setTimeout(() => {
if (buns !== "buns")
rej("no buns");
else if (patty !== "patty")
rej("no patty");
else{
console.log("Step 4: Putting beef patty between buns");
res("burger");
}
}, 1000);
});
}
//Promise链式调用
getBeef().then(beef => {
return Promise.all([
cookBeef(beef),
getBuns()
])
}).then(ingredients => {
const [patty, buns] = ingredients;
return putBeefBetweenBuns(buns, patty)
}).then(burger => {
console.log("Finally: " + burger + " is served!")
})
//async/await
async function makeBurger(){
const beef = await getBeef();
const patty = await cookBeef(beef);
const buns = await getBuns();
const burger = await putBeefBetweenBuns(buns, patty);
return burger
}
makeBurger()
In this example, we use Promise chaining (using the then keyword) and async/await to handle asynchronous operations.
1. For Promise chain calls, use .then()
the method to link asynchronous operations together. In this example, first call getBeef()
, and then .then()
process the obtained in beef
. Next, we use Promise.all()
to execute cookBeef(beef)
and getBuns()
together, wait for them both to complete, and then process their results. Finally, we put patty
and buns
together to compose burger
. The advantage of this notation is that it allows you to organize asynchronous operations in a much cleaner way. However, when there are many asynchronous operations, this way of writing may lead to .then()
too long chains, making the code difficult to read and maintain.
2. For async/await, it allows you to write asynchronous operations in a way that is closer to synchronous code. In this example, we use await
to wait for each asynchronous operation to complete and assign the result to the corresponding variable. This way, the code looks like it's executing synchronously, but is actually still asynchronous. The advantage of this way of writing is: it makes the code more concise and easier to read. Also, it allows you to handle errors more easily, since you can try-catch
catch exceptions in asynchronous operations directly using the statement.
Both of these writing methods are used to handle asynchronous operations, and their main difference lies in writing style and readability. Promise chaining focuses more on chaining operations together, while async/await focuses more on making code look like it executes synchronously.
Summarize
In this blog, we compare three ways to implement synchronous and asynchronous operations in JavaScript: synchronous methods, callback functions in asynchronous and Promise chaining calls with async/await. Each method has its pros and cons. Synchronous methods are easy to understand, but do not support asynchronous operations; callback functions support asynchronous operations, but are poor in readability; and Promise and async/await both support asynchronous operations and have good readability, so these two writing methods are recommended.