Six options for JS asynchronous programming
Foreword
We know that the execution environment of the Javascript language is "single thread". This means that only one task can be completed at a time. If there are multiple tasks, they must be queued, the previous task is completed, and then the next task is executed.
Although this mode is relatively simple to implement and the execution environment is relatively simple, as long as one task takes a long time, the following tasks must be queued up, which will delay the execution of the entire program. Common browsers are unresponsive (feigned death), often because a certain piece of Javascript code runs for a long time (such as an infinite loop), causing the entire page to be stuck in this place, and other tasks cannot be performed.
To solve this problem, the Javascript language divides the execution modes of tasks into two types: synchronous and asynchronous. This article mainly introduces several methods of asynchronous programming, and through comparison, get the best asynchronous programming solution!
1. Synchronous and Asynchronous
We can generally understand that asynchrony means that a task is divided into two sections, the first section is executed first, and then other tasks are executed. When you are ready, you can go back and execute the second section. The code after the asynchronous task will run immediately without waiting for the asynchronous task to finish, that is, the asynchronous task does not have a "blocking" effect . For example, a task is to read a file for processing, and the asynchronous execution process is as follows
This discontinuous execution is called asynchronous. Correspondingly, continuous execution is called synchronization
"Asynchronous mode" is very important. On the browser side, operations that take a long time should be performed asynchronously to avoid the browser becoming unresponsive. The best example is Ajax operations. On the server side, "asynchronous mode" is even the only mode, because the execution environment is single-threaded, and if all http requests are allowed to be executed synchronously, the server performance will drop drastically and will quickly become unresponsive. Next, we will introduce six methods of asynchronous programming.
2. Callback function (Callback)
The callback function is the most basic method of asynchronous operation. The following code is an example of a callback function:
ajax(url, () => {
// processing logic
})
But the callback function has a fatal weakness, that is, it is easy to write callback hell (Callback hell) . Assuming multiple requests have dependencies, you might write code like this:
ajax(url, () => {
// processing logic
ajax(url1, () => {
// processing logic
ajax(url2, () => {
// processing logic
})
})
})
The advantage of the callback function is that it is simple, easy to understand and implement, but the disadvantage is that it is not conducive to the reading and maintenance of the code, and the various parts are highly coupled, which makes the program structure chaotic and the process difficult to track (especially when multiple callback functions are nested). And each task can only specify one callback function. In addition, it cannot use try catch to catch errors, and it cannot return directly.
3. Event monitoring
In this way, the execution of asynchronous tasks does not depend on the order of the code, but on whether an event occurs .
The following are two functions f1 and f2. The intention of programming is that f2 must wait until f1 is executed before it can be executed. First, bind an event to f1 (jQuery's writing method is used here)
f1.on('done', f2);
The above line of code means that when a done event occurs in f1, f2 is executed. Then, rewrite f1:
function f1() {
setTimeout(function () {
// ...
f1.trigger('done');
}, 1000);
}
In the above code, f1.trigger('done') means that after the execution is completed, the done event is triggered immediately, thereby starting to execute f2.
The advantage of this method is that it is easier to understand, multiple events can be bound, multiple callback functions can be specified for each event, and it can be "decoupled", which is conducive to the realization of modularization. The disadvantage is that the entire program has to become event-driven, and the running process will become very unclear. When reading the code, it's hard to see the main flow.
4. Publish and Subscribe
We assume that there is a "signal center", and when a task is completed, it "publishes" a signal to the signal center, and other tasks can "subscribe" to the signal center to know when they can start execution. This is called "publish/subscribe pattern" (publish-subscribe pattern), also known as "observer pattern" (observer pattern).
First, f2 subscribes the done signal to the signal center jQuery.
jQuery.subscribe('done', f2);
Then, f1 is rewritten as follows:
function f1() {
setTimeout(function () {
// ...
jQuery.publish('done');
}, 1000);
}
In the above code, jQuery.publish('done') means that after the execution of f1 is completed, the done signal is released to the signal center jQuery, thereby triggering the execution of f2.
After f2 finishes executing, you can unsubscribe (unsubscribe)
jQuery.unsubscribe('done', f2);
This method is similar in nature to "event listener", but is significantly better than the latter. Because you can monitor the operation of the program by looking at the "message center" to see how many signals exist and how many subscribers each signal has.
5. Promise/A+
Promise means promise, in the program it means promise that I will give you a result after a period of time. When will it be used for a while? The answer is asynchronous operation. Asynchronous means that it may take a long time to have results, such as network requests, reading local files, etc.
1. Three states of Promise
- Pending----The initial state of the Promise object instance when it is created
- Fulfilled---- can be understood as a successful state
- Rejected---- can be understood as a failed state
Once the promise changes from the waiting state to another state, it can never change the state . For example, once the state becomes resolved, it cannot be changed to Fulfilled again.
let p = new Promise((resolve, reject) => {
reject('reject')
resolve('success')//Invalid code will not be executed
})
p.then(
value => {
console.log(value)
},
reason => {
console.log(reason)//reject
}
)
When we construct a Promise, the code inside the constructor is executed immediately
new Promise((resolve, reject) => {
console.log('new Promise')
resolve('success')
})
console.log('end')
// new Promise => end
2. Chained calls of promises
Each call returns a new Promise instance (this is why then can be chained)
If the result returned in then is a result, the result will be passed to the next success callback in then
If there is an exception in then, it will go to the next then's failure callback
If return is used in then, the value of return will be wrapped by Promise.resolve() (see example 1, 2)
No parameters can be passed in then, if not, it will be passed to the next then (see example 3)
catch catches uncaught exceptions
Next we look at a few examples:
// example 1
Promise.resolve(1)
.then(res => {
console.log(res)
return 2 //wrapped into Promise.resolve(2)
})
.catch(err => 3)
.then(res => console.log(res))
// Example 2
Promise.resolve(1)
.then(x => x + 1)
.then(x => {
throw new Error('My Error')
})
.catch(() => 1)
.then(x => x + 1)
.then(x => console.log(x)) //2
.catch(console.error)
// Example 3
let fs = require('fs')
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
read('./name.txt')
.then(function(data) {
throw new Error() //An exception occurs in then, and the failure callback of the next then will be taken
}) //Since there is no failure callback for the next then, it will continue to look down. If there is none, it will be caught by catch
.then(function(data) {
console.log('data')
})
.then()
.then(null, function(err) {
console.log('then', err)// then error
})
.catch(function(err) {
console.log('error')
})
Promise can not only catch errors, but also solve the problem of callback hell very well. The previous example of callback hell can be rewritten as the following code:
ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))
It also has some shortcomings, such as the inability to cancel the Promise, and the error needs to be caught by the callback function.
6. Generators/ yield
Generator function is an asynchronous programming solution provided by ES6. Its syntax and behavior are completely different from traditional functions. The biggest feature of Generator is that it can control the execution of functions.
- Syntactically, it can first be understood that the Generator function is a state machine that encapsulates multiple internal states.
- In addition to the state machine, the Generator function is also a traverser object generation function .
- The function can be paused, the yield can be paused, the next method can be started, and the result of the expression after yield is returned each time .
The yield expression itself does not return a value, or it always returns undefined. The next method can take one parameter, which will be used as the return value of the previous yield expression .
Let's look at an example first:
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
The results may be inconsistent with your imagination. Next, we analyze the code line by line:
- First of all, the Generator function call is different from ordinary functions, it will return an iterator
- When executing next for the first time, the parameters passed will be ignored, and the function will pause at yield (x + 1), so return 5 + 1 = 6
- When executing next for the second time, the incoming parameter 12 will be used as the return value of the previous yield expression. If you do not pass parameters, yield will always return undefined. Now let y = 2 * 12, so the second yield equals 2 * 12 / 3 = 8
- When executing next for the third time, the incoming parameter 13 will be used as the return value of the previous yield expression, so z = 13, x = 5, y = 24, adding up to 42
Let's look at another example: there are three local files, 1.txt, 2.txt and 3.txt, each with only one sentence, the next request depends on the result of the previous request, and I want to call the three files in turn through the Generator function
//1.txt file
2.txt
//2.txt file
3.txt
//3.txt file
Finish
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
function* r() {
let r1 = yield read('./1.txt')
let r2 = yield read(r1)
let r3 = yield read(r2)
console.log(r1)
console.log(r2)
console.log(r3)
}
let it = r()
let { value, done } = it.next()
value.then(function(data) { // value is a promise
console.log(data) //data=>2.txt
let { value, done } = it. next(data)
value.then(function(data) {
console.log(data) //data=>3.txt
let { value, done } = it.next(data)
value.then(function(data) {
console.log(data) //data=>end
})
})
})
// 2.txt=>3.txt=>end
From the above example, we can see that the manual iteration Generator
function is very troublesome, and the implementation logic is a bit confusing, but the actual development is generally used in conjunction with the co
library . coIt is a generator-based flow control tool for Node.js and browsers. With the help of Promises, you can write non-blocking code in a more elegant way .
To install co
the library just:npm install co
The above example can be easily implemented in just two sentences
function* r() {
let r1 = yield read('./1.txt')
let r2 = yield read(r1)
let r3 = yield read(r2)
console.log(r1)
console.log(r2)
console.log(r3)
}
let co = require('co')
co(r()).then(function(data) {
console.log(data)
})
// 2.txt=>3.txt=>end=>undefined
We can solve the problem of callback hell through the Generator function. We can rewrite the previous example of callback hell into the following code:
function *fetch() {
yield ajax(url, () => {})
yield ajax(url1, () => {})
yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
7. async/await
1. Introduction to Async/Await
With async/await, you can easily achieve what you have done with generators and co functions. It has the following characteristics:
- async/await is implemented based on Promise, it cannot be used for ordinary callback functions.
- async/await, like Promises, is non-blocking.
- async/await makes asynchronous code look like synchronous code, which is where its magic happens.
If a function is added with async, then the function will return a Promise
async function async1() {
return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}
The generator function calls three files in turn. The example is written in async/await, which can be implemented in just a few words.
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
async function readResult(params) {
try {
let p1 = await read(params, 'utf8')//await is followed by a Promise instance
let p2 = await read(p1, 'utf8')
let p3 = await read(p2, 'utf8')
console.log('p1', p1)
console.log('p2', p2)
console.log('p3', p3)
return p3
} catch (error) {
console.log(error)
}
}
readResult('1.txt').then( // the async function also returns a promise
data => {
console.log(data)
},
err => console.log(err)
)
// p1 2.txt
// p2 3.txt
// end of p3
// Finish
2. Async/Await concurrent requests
If you request two files, it doesn't matter, you can pass concurrent requests
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
function readAll() {
read1()
read2()//This function is executed synchronously
}
async function read1() {
let r = await read('1.txt','utf8')
console.log(r)
}
async function read2() {
let r = await read('2.txt','utf8')
console.log(r)
}
readAll() // 2.txt 3.txt
8. Summary
1. The evolutionary history of JS asynchronous programming: callback -> promise -> generator -> async + await
2. The implementation of the async/await function is to wrap the Generator function and the automatic executor in a function.
3. async/await can be said to be the ultimate asynchronous solution.
(1) Compared with Promise, the advantages of async/await function are:
- Handling the call chain of then, can write code more clearly and accurately
- And it also gracefully solves the callback hell problem.
Of course, the async/await function also has some shortcomings, because await transforms asynchronous code into synchronous code. If multiple asynchronous codes have no dependencies but use await, the performance will be degraded. If the code has no dependencies, Promise can be used. .all way.
(2) The improvement of the Generator function by the async/await function is reflected in the following three points:
- Built-in actuator.
The execution of the Generator function must rely on the executor, so there is the co function library, and the async function has its own executor. That is to say, the execution of async functions is exactly the same as that of ordinary functions, with only one line .
- wider applicability.
Co function library convention, yield command can only be followed by Thunk function or Promise object, while await command of async function can be followed by Promise object and primitive type value (numeric, string and boolean value, but this is equivalent to synchronous operation ) .
- better semantics.
async and await have clearer semantics than asterisk and yield. async means that there is an asynchronous operation in the function, and await means that the following expression needs to wait for the result.