1. JavaScript asynchrony
(1) Why is JavaScript single threaded?
JavaScript’s primary purpose is to interact with users and manipulate the DOM, and if JavaScript is designed to be asynchronous, it can lead to complex synchronization issues
So to avoid complexity, JavaScript is designed to be single-threaded
(2) Why does JavaScript need to be asynchronous?
Single-threaded means that all tasks are queued, meaning that the next task cannot be executed until the previous one has completed
If the former task takes a long time, then the latter task can only wait forever, resulting in a poor user experience
In many cases, long time consuming tasks are usually IO operations rather than CPU calculations, wasting CPU resources
An asynchronous operation suspends the pending task, executes the next task, and waits for the result to return before resuming the pending task
(3) How can single-threaded JavaScript achieve asynchrony?
JavaScript implements asynchrony through event loops, which are executed as follows:
- In the case of synchronous tasks, the execution stack is directly entered and executed by the main thread
- For asynchronous tasks, an event (callback function) is placed in the task queue only when the asynchronous task returns a result
- When all synchronization tasks in the execution stack are completed, it reads the next event in the task queue and adds it to the execution stack to start execution
- Repeat the above steps until the end
In ES6, we have a more nuanced understanding of the event loop
First, we divide tasks into two categories, one is MacroTask (also known as Task) and the other is MicroTask (also known as Jobs).
- MacroTask: setTimeout, setInterval, I/O, UI Rendering, etc
- MicroTask: process.nextTick(node), Promise, etc
In an event loop, the mechanism is as follows:
- Extract a MacroTask from the MacroTask Queue to start execution (or from the task Queue if the execution stack is empty)
- If a MicroTask is encountered during MacroTask execution, add it to the MicroTask Queue
- Extract all MicroTask executions from the MicroTask Queue until the MicroTask Queue is empty
- Repeat the above steps until the end
Combining these two understandings, the final conclusion is as follows:
-
Put the code on the execution stack and execute it sequentially
-
If you encounter a synchronization task, execute it immediately
-
If an asynchronous task is encountered, the callback function is registered in the Event Table first, and then the callback function is put into the corresponding queue after the asynchronous task is completed
- If the asynchronous task is a macro task, the corresponding callback function should be placed in the macro task queue after the task is completed
- If the asynchronous task is a microtask, the corresponding callback function is put into the microtask queue after the task is completed
-
When the stack is empty, all the callback functions in the microtask queue are executed, thus completing an event loop
-
Then, extract the first callback function from the macro task queue and place it on the execution stack, and repeat the process
Here to give you a problem, in the Node running environment, please write the following program execution results
setTimeout(function() {
console.log(1)},0)
new Promise(function(resolve,reject){
console.log(2)
resolve()
}).then(function() {
console.log(3)
}).then(function() {
console.log(4)
})
process.nextTick(function(){
console.log(5)})console.log(6)
Copy the code
The output is 2, 6, 5, 3, 4, 1, everyone right? The complete execution logic is as follows:
-
Put this code on the execution stack and execute it sequentially
-
When setTimeout is encountered, the callback function is registered in the Event Table. After the task is completed, the callback function is put into the macro task queue
Since the wait time is 0, the task is complete and can be immediately put into the macro task queue
-
When a Promise is encountered, the constructor logic is to synchronize the task, execute it immediately, and output 2
For THEN, the callback function is registered in the Event Table and placed on the microtask queue when the task is complete
Because resolve() has been called in the constructor, the task is complete and can be immediately put on the microtask queue
-
When process.nextTick is encountered, register the callback function in the Event Table and put it on the microtask queue
-
When console.log is encountered, it is a synchronization task and is executed immediately, printing 6
-
At this point stack clearing is performed, and then we will execute all the callback functions in the microtask queue
Because process.nextTick has a higher priority than Promise in the microtask queue, 5 is printed, followed by 3 and 4
-
This is the end of the first event loop, and then the second event loop
-
Take the first callback function from the macro task queue and put it on the execution stack and execute it in order
The first callback in the macro task queue is the setTimeout callback, which prints 1
2. Solutions to asynchronous problems
In JavaScript programming, asynchrony is always a topic that cannot be avoided. How to solve the asynchrony problem gracefully is worth discussing
(1) Callback function
The essence of this scenario is to pass in a function as an argument to an asynchronous function and execute the function when the asynchronous logic completes
Let’s start with an example of how callbacks can be used to handle asynchronous problems
// The callback function
function callback() {
console.log("Execute callback function")}// Asynchronous functions
function asynchronous(callback){
console.log("Perform asynchronous logic")
callback()
}
// Call the function to ensure that the callback function is executed after the asynchronous logic is complete
asynchronous(callback)
Copy the code
For example, the setTimeout function we often use
// The callback function
function sayHello() {
console.log("Hello World")}// Asynchronous functions
// Pass the callback function as an argument to the asynchronous function and wait 1000ms before executing
setTimeout(sayHello, 1000)
Copy the code
JQuery, for example, makes a lot of use of callback techniques
$.post("http://www.httpbin.org/post", {
"username": "admin"."password": "123456"
}, function(data, status) { // The callback function is guaranteed to be executed after the data and status results are obtained
console.log(status)
console.log(data)
})
Copy the code
Heavy use of callback functions leads to callback hell, which is simply too much nesting that makes the program hard to read and understand
(2) the Promise
What is Promise? A Promise is simply an object that represents the state and result of an asynchronous operation
Promise states are “Pending,” “Resolved” and “Rejected.”
Promise objects start out in a pending state and have one and only one state transition throughout their life cycle, to Resolved or Rejected
1) create a Promise
Promise()
We can create a Promise object using the Promise constructor
The constructor takes a function as an argument, which takes resolve and reject, both function types
- Resolve: Called when the operation succeeds, changing the state of the Promise object to Resolved and passing the result of the operation as an argument
- Reject: Called when the operation fails, changing the state of the Promise object to Rejected and passing the error message as an argument
/ / pseudo code
let promise = new Promise(function(resolve, reject) { // Resolve and reject are callbacks
// Asynchronous operation
if (/* The operation succeeded */) {
return resolve(value) // Use the resolve callback to pass value as the result of the operation
} else { // The operation failed
return reject(error) // Use the reject callback, passing the error message as an argument}})Copy the code
Promise.resolve()
Used to convert existing objects into Promise objects, there are four cases based on parameter types
- Promise object: Returns this Promise object without doing anything
- Thenable object: Become a Promise object and immediately execute the thenable object’s then method
- Normal value: Returns an Resolved Promise object that is passed as an argument to the callback function
- No arguments: Returns a Promise object in the Resolved state
console.log(1)
let obj = {
then: function(resolve, reject) {
console.log("obj then")
resolve()
}
}
console.log(2)
let pro = Promise.resolve(obj)
console.log(3)
pro.then(function() {
console.log("pro then")
}).catch(function() {
console.log("pro catch")})console.log(4)
* 1 * 2 * 3 * 4 * obj then * pro then **/
Copy the code
Promise.reject()
Used to convert existing objects into Promise objects
No matter what type the argument is, always return a Promise object with the state Rejected, and the argument is passed directly to the callback function
console.log(1)
let obj = {
then: function(resolve, reject) {
console.log("obj then")
reject()
}
}
console.log(2)
let pro = Promise.reject(obj)
console.log(3)
pro.then(function(value) {
console.log("pro then")
console.log(value === obj)
}).catch(function(error) {
console.log("pro catch")
console.log(error === obj)
})
console.log(4)
* 1 * 2 * 3 * 4 * pro catch * true **/
Copy the code
(2) using a Promise
Promise.prototype.then()
This method takes two functions as parameters that specify the resolved state and the rejected state callback functions
The first function is called when the state becomes Resolved, and the second function is called when the state becomes Rejected, where the second function is optional
/ / pseudo code
promise.then(function(value){ // Resolved callback function
// If the operation succeeds, process value
}, function(error){ // The callback function of the Rejected state
// If the operation fails, handle error
})
Copy the code
The Promise is executed immediately after creation, while the callback function specified by the then method is executed at the end of the current event loop
console.log(1)
let promise = new Promise(function(resolve, reject) {
console.log("Promise begin")
let error = "fail"
reject(error)
console.log("Promise end")})console.log(2)
promise.then(function(value) {
console.log(value)
}, function(error) {
console.log(error)
})
console.log(3)
/* * Result: * 1 * Promise begin * Promise end * 2 * 3 * fail **/
Copy the code
The then method of a Promise object can be called multiple times, which is quite special
let promise = new Promise(function(resolve, reject) {
let value = "success"
resolve(value)
})
promise.then(function(value){
console.log(value)
})
promise.then(function(value){
console.log(value)
})
/* * Result: * success * success **/
Copy the code
The then method returns a new Promise object (not the original Promise object), so it can be called chained
let promise = new Promise(function(resolve, reject) {
let data = 2
resolve(data)
})
promise.then(function(value) {
console.log(value)
return value*2
}).then(function(value) {
console.log(value)
})
/* * Result: * Promise * 2 * 4 **/
Copy the code
Promise.prototype.catch()
This function specifies the callback when an error occurs, equivalent to then(null, reject)
If a reject() or throw new Error() occurs inside a Promise, the Error will be passed on
Until then(resolve, reject) or catch(reject) is encountered that can process it
If there is no subsequent statement to handle it, the error is not passed to the outer code
let promise = new Promise(function(resolve, reject) {
reject("fail")
})
promise.then(function(value) {
console.log(value)
}).catch(function(error) {
console.log(error)
})
/*
* 执行结果:
* fail
**/
Copy the code
Promise.prototype.finally()
This function is used to specify that the callback will be executed regardless of the final state
let promise = new Promise(function(resolve, reject) {
let success = (Math.random() >= 0.5)
if (success) {
resolve("success")}else {
reject("fail")
}
})
promise.then(function(value) {
console.log(value)
}).catch(function(error) {
console.log(error)
}).finally(function() {
console.log("finally")})/* * Result: * success/fail * finally **/
Copy the code
Promise.all()
Takes an array, each element of which is a Promise instance, and returns a new Promise instance
The new Promise instance will become Resolved only when the state of all incoming Promise instances becomes Resolved
If the state of any incoming Promise instance changes to Rejected, the new Promise instance will also change to Rejected
console.time("all")
let pro1 = new Promise(function(resolve, reject) {
setTimeout(function() { resolve() }, 2000)})let pro2 = new Promise(function(resolve, reject) {
setTimeout(function() { reject() }, 5000)})let pro = Promise.all([pro1, pro2])
pro.then(function() {
console.log("Promise resolve")
}).catch(function() {
console.log("Promise reject")
}).finally(function() {
console.timeEnd("all")})/* * Result: * Promise reject * all: 5002.387939453125ms **/
Copy the code
Promise.race()
This method also takes an array, each element of which is a Promise instance, and returns a new Promise instance
The difference is that whenever the state of any incoming Promise instance changes, the state of the new Promise instance changes
That is, the state of the new Promise instance is determined by the state of the Promise instance that gets the result the fastest
console.time("race")
let pro1 = new Promise(function(resolve, reject) {
setTimeout(function() { resolve() }, 2000)})let pro2 = new Promise(function(resolve, reject) {
setTimeout(function() { reject() }, 5000)})let pro = Promise.race([pro1, pro2])
pro.then(function() {
console.log("Promise resolve")
}).catch(function() {
console.log("Promise reject")
}).finally(function() {
console.timeEnd("race")})/* * Result: * Promise resolve * race: 2001.555908203125ms **/
Copy the code
(3) the async/await
What is the async keyword used for?
A function defined by async returns a Promise object, which can then be processed using the properties and methods of the Promise object
async function asynchronous() {
let data = "success"
return data // Convert to a Promise object and return
}
let promise = asynchronous()
console.log(promise)
promise.then(function(value) {
console.log(value)
})
/* * Promise {
: "success"} * success **/
Copy the code
Async provides syntactic sugar for making promises, while await provides syntactic sugar for using promises, allowing us to simplify our code even further
The await keyword is used to wait for the result of an asynchronous operation, and only after the asynchronous operation has completed and returned the result can the following code continue
Note that await can only be used inside functions defined by async
async function asynchronous() {
let data = "success"
return data
}
async function main() {
// Return a Promise without await
let promise = asynchronous()
console.log(promise)
// Return the result of a Promise with await
let result = await asynchronous()
console.log(result)
}
main()
/* * Promise {
: "success"} * success **/
Copy the code
The state of the Promise object after await may change to Rejected, in which case we need to use try/catch
async function asynchronous() {
return new Promise(function(resolve, reject) {
let data = "fail"
reject(data)
})
}
async function main() {
try {
let result = await asynchronous()
} catch(error) {
console.log(error)
}
}
main()
/*
* 执行结果:
* fail
**/
Copy the code