Do you really understand Promises
Foreword
In asynchronous programming, Promises play a pivotal role, more reasonable and more powerful than traditional solutions (callbacks and events). Some friends seem to know everything about this "old friend" that they deal with almost every day, but they may be full of doubts if they go deeper. This article will take you to understand this familiar stranger - Promise.
Basic usage
1. Grammar
new Promise( function(resolve, reject) {...} /* executor */ )
- When constructing a Promise object, an executor function needs to be passed in, and the main business processes are executed in the executor function.
- When the Promise constructor is executed, the executor function is called immediately. The resolve and reject functions are passed as parameters to the executor. When the resolve and reject functions are called, the state of the promise is changed to fulfilled (completed) or rejected (failed). Once the state changes, it will not change again , and this result can be obtained at any time.
- After calling the resolve function in the executor function, the callback function set by promise.then will be triggered; after calling the reject function, the callback function set by promise.catch will be triggered.
It is worth noting that Promise is used to manage asynchronous programming. It is not asynchronous itself . When a new Promise is used, the executor function will be executed immediately, but we generally process an asynchronous operation in the executor function. For example, in the following code, 2 will be printed first at the beginning.
let p1 = new Promise(()=>{
setTimeout(()=>{
console.log(1)
},1000)
console.log(2)
})
console.log(3) // 2 3 1
Promise adopts the callback function delay binding technology. When the resolve function is executed, the callback function has not been bound, so the execution of the callback function can only be postponed . What exactly does this mean? Let's first look at the following example:
let p1 = new Promise((resolve,reject)=>{
console.log(1);
resolve('boating in the waves')
console.log(2)
})
// then: Set the method to process after success or failure
p1.then(result=>{
//p1 delay binding callback function
console.log('success'+result)
},reason=>{
console.log('failure'+reason)
})
console.log(3)
// 1
// 2
// 3
// successful boating in the waves
When a new Promise is executed, the executor function is executed first, and 1 and 2 are printed out. When the Promise executes resolve, it triggers the micro-task, or continues to execute the synchronization task.
When p1.then is executed, two functions are stored (at this time, these two The function has not been executed yet), and then print out 3. At this time, the execution of the synchronization task is completed, and finally the microtask just now is executed, thereby executing the successful method in .then.
error handling
Errors from Promise objects are "bubble" and are passed backwards until they are handled by the onReject function or caught by a catch statement. With this "bubbling" feature, there is no need to catch exceptions individually in each Promise object.
To encounter a then, the method of success or failure must be executed, but if this method is not defined in the current then, it will be deferred to the next corresponding function
function executor (resolve, reject) {
let rand = Math.random()
console.log(1)
console.log(rand)
if (rand > 0.5) {
resolve()
} else {
reject()
}
}
var p0 = new Promise(executor)
var p1 = p0.then((value) => {
console.log('succeed-1')
return new Promise(executor)
})
var p2 = p1.then((value) => {
console.log('succeed-2')
return new Promise(executor)
})
p2.catch((error) => {
console.log('error', error)
})
console.log(2)
This code has three Promise objects: p0~p2. No matter which object throws an exception, it can be caught by the last object p2.catch. In this way, the errors of all Promise objects can be merged into one function for processing, so that each task needs to be processed separately. unusual question.
In this way, we eliminate nested calls and frequent error handling, which makes the code we write more elegant and more in line with human linear thinking.
Promise chaining
We all know that multiple Promises can be chained together to represent a series of different steps. The key to this approach lies in the following two inherent behavioral properties of Promises:
- Every time you call then on a Promise, it creates and returns a new Promise, which we can chain;
- Regardless of the value returned from the completion callback (the first argument) called by then, it is automatically set to the completion of the chained Promise (in the first point).
First, let's use the following example to explain what this sentence just means, and then introduce the execution process of the down-chain call in detail.
let p1=new Promise((resolve,reject)=>{
resolve(100) // Determines that the successful method in the next then will be executed
})
// connect p1
let p2=p1.then(result=>{
console.log('success 1'+result)
return Promise.reject(1)
// Returns a new Promise instance, which determines that the current instance is failed, so it is determined that the failed method in the next then will be executed
},reason=>{
console.log('Failure 1'+reason)
return 200
})
// connect p2
let p3=p2.then(result=>{
console.log('success 2'+result)
},reason=>{
console.log('Failure 2'+reason)
})
// success 1 100
// fail 2 1
We complete the promise p2 created and returned by the first call to then by returning Promise.reject(1). The then call of p2 accepts the completion value from the return Promise.reject(1) statement at runtime. Of course, p2.then creates another new promise, which can be stored in the variable p3.
The success or failure of the instance from the new Promise depends on whether the executor function executes resolve or reject , or an abnormal error occurs during the execution of the executor function . In both cases, the instance state will be changed to failed.
p2 executes the state of the new instance returned by then, and determines which method in the next then will be executed. There are the following situations:
- Whether it is a successful method execution or a failed method execution (the two methods in then), if the execution throws an exception, the status of the instance will be changed to failure.
- If a new Promise instance is returned in the method (such as Promise.reject(1) in the above example), the result of returning this instance is success or failure, and it also determines whether the current instance succeeds or fails.
- The remaining cases are basically to make the instance into a successful state, and the result returned by the method in the previous then will be passed to the method of the next then.
Let's look at another example
new Promise(resolve=>{
resolve(a) // error
// An exception error occurs in the execution of this executor function, which determines that the next then failure method will be executed
}).then(result=>{
console.log(`success: ${result}`)
return result*10
},reason=>{
console.log(`Failed: ${reason}`)
// When this sentence is executed, no exception occurs or a failed Promise instance is returned, so the next successful method of then will be executed
// There is no return here, it will return undefined at the end
}).then(result=>{
console.log(`success: ${result}`)
},reason=>{
console.log(`Failed: ${reason}`)
})
// Failed: ReferenceError: a is not defined
// success: undefined
async & await
From the above examples, we can see that although using Promise can solve the problem of callback hell well, this method is full of Promise's then() method. If the processing flow is more complicated, then the whole code will be filled with Then, the semantics are not obvious, and the code cannot represent the execution flow well.
The new asynchronous programming method in ES7, the implementation of async/await is based on Promise. In short, the async function returns a Promise object, which is the syntactic sugar of the generator. Many people consider async/await to be the ultimate solution for asynchronous operations:
- The syntax is concise, more like synchronous code, and more in line with ordinary reading habits;
- Improve the code organization of serial execution of asynchronous operations in JS, and reduce the nesting of callbacks;
- In Promise, try/catch cannot be used to capture errors, but in Async/await, errors can be handled like synchronous code.
However, there are also some disadvantages, because await transforms asynchronous code into synchronous code. If multiple asynchronous codes have no dependencies but use await, it will lead to performance degradation.
async function test() {
// If the following code has no dependencies, the Promise.all method can be used completely
// If there are dependencies, it is actually an example of solving callback hell
await fetch(url1)
await fetch(url2)
await fetch(url3)
}
Looking at the code below, can you tell what is printed out?
let p1 = Promise.resolve(1)
let p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2)
}, 1000)
})
async function fn() {
console.log(1)
// When the code executes to this line (put this line first), build an asynchronous microtask
// Wait for the promise to return the result, and the code below await is also listed in the task queue
let result1 = await p2
console.log(3)
let result2 = await p1
console.log(4)
}
fn()
console.log(2)
// 1 2 3 4
If the expression logic on the right side of await is a promise, await will wait for the return result of this promise. Only the returned state is resolved , the result will be returned. If the promise is in the failed state, await will not receive its return result. The following await The code will also not continue to execute.
let p1 = Promise.reject(100)
async function fn1() {
let result = await p1
console.log(1) //This line of code will not be executed
}
Let's look at a more complex topic:
console.log(1)
setTimeout(()=>{console.log(2)},1000)
async function fn(){
console.log(3)
setTimeout(()=>{console.log(4)},20)
return Promise.reject()
}
async function run(){
console.log(5)
await fn()
console.log(6)
}
run()
//It takes about 150ms to execute
for(let i=0;i<90000000;i++){}
setTimeout(()=>{
console.log(7)
new Promise(resolve=>{
console.log(8)
resolve()
}).then(()=>{console.log(9)})
},0)
console.log(10)
// 1 5 3 10 4 7 8 9 2
Before doing this question, the reader needs to understand:
- Microtask-based technologies include MutationObserver, Promise, and many other technologies developed based on Promise. In this question, resolve() and await fn() are all microtasks.
- Regardless of the arrival time of the macro tasks and the order in which they are placed, every time the main thread execution stack is empty, the engine will prioritize the processing of the micro-task queue, process all the tasks in the micro-task queue, and then process the macro tasks.
Next, we analyze step by step:
- First execute the synchronization code, output 1, meet the first setTimeout, put its callback into the task queue (macro task), and continue to execute
- Run run(), print out 5, and execute it down, meet await fn(), put it into the task queue (microtask)
- await fn() When the current line of code is executed, the fn function will be executed immediately, print out 3, meet the second setTimeout, put its callback into the task queue (macro task), await fn() The code below needs to wait for the return Promise will only be executed if it succeeds, so 6 will not be printed.
- Continue to execute, encounter the for loop synchronization code, need to wait 150ms, although the second setTimeout has reached the time, it will not be executed, encounter the third setTimeout, put its callback into the task queue (macro task), and then print out 10. It is worth noting that this timer delay time of 0 milliseconds is actually not reached. According to the HTML5 standard, setTimeOut delays the execution time, at least 4 milliseconds.
- After the synchronization code is executed, if there is no micro task at this time, the macro task is executed. The setTimeout mentioned above is executed first, and 4 is printed.
- Then execute the next macro task of setTimeout, so print 7 first, when the new Promise executes the executor function immediately, print 8, and then trigger the micro-task when resolve is executed, so print 9
- Finally, execute the macro task of the first setTimeout and print out 2
Commonly used methods
1. Promise.resolve()
The Promise.resolve(value) method returns a Promise object resolved with the given value.
Promise.resolve() is equivalent to the following:
Promise.resolve('foo')
// Equivalent to
new Promise(resolve => resolve('foo'))
The parameters of the Promise.resolve method are divided into four cases.
(1) The parameter is a Promise instance
If the parameter is a Promise instance, Promise.resolve will return the instance unchanged without any modification .
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
In the above code, p1 is a Promise, which becomes rejected after 3 seconds. The state of p2 changes after 1 second, and the resolve method returns p1. Since p2 returns another Promise, the state of p2 itself is invalid, and the state of p1 determines the state of p2. Therefore, the following then statements all become for the latter (p1). After another 2 seconds, p1 becomes rejected, causing the callback function specified by the catch method to be triggered.
(2) The parameter is not an object with a then method, or is not an object at all
Promise.resolve("Success").then(function(value) {
// The parameters of the Promise.resolve method will be passed to the callback function at the same time.
console.log(value); // "Success"
}, function(value) {
// will not be called
});
(3) without any parameters
The Promise.resolve() method allows calling without parameters and directly returns a resolved Promise object. If you want to get a Promise object, the more convenient way is to call the Promise.resolve() method directly.
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one two
(4) The parameter is a thenable object
The thenable object refers to an object with a then method. The Promise.resolve method will convert this object into a Promise object, and then immediately execute the then method of the thenable object.
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
2. Promise.reject()
The Promise.reject() method returns a Promise object with the reason for rejection.
new Promise((resolve, reject) => {
reject(new Error("Error"));
});
// Equivalent to
Promise.reject(new Error("Error"));
// Instructions
Promise.reject(new Error("BOOM!")).catch(error => {
console.error(error);
});
It is worth noting that after calling resolve or reject, the mission of Promise is completed, and subsequent operations should be placed in the then method, not directly behind resolve or reject . So, it's better to prefix them with return statements so there are no surprises.
new Promise((resolve, reject) => {
return reject(1);
// The following statements will not be executed
console.log(2);
})
3. Promise.all()
let p1 = Promise.resolve(1)
let p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2)
}, 1000)
})
let p3 = Promise.resolve(3)
Promise.all([p3, p2, p1])
.then(result => {
// The returned results are in the order in which the instances were written in the Array
console.log(result) // [ 3, 2, 1 ]
})
.catch(reason => {
console.log("Failure:reason")
})
Promise.all generates and returns a new Promise object, so it can use all the methods of the Promise instance. This method will only return when all the Promise objects in the promise array passed as a parameter have become resolved , and the newly created Promise will use the values of these promises.
If any of the parameters in the promise is rejected, the entire Promise.all call will terminate immediately and return a new Promise object that rejects.
4. Promise.allSettled()
Sometimes, we don't care about the results of asynchronous operations, only whether these operations have ended. At this time, the introduction of the Promise.allSettled() method in ES2020 is very useful. Without this method, it would be cumbersome to ensure that all operations end. The Promise.all() method cannot do this.
If there is such a scenario: a page has three areas, corresponding to three independent interface data, use Promise.all to concurrently request three interfaces, if any one of the interfaces is abnormal, the status is reject, which will cause the page All the data in the three areas cannot be obtained. Obviously, we cannot accept this situation. The appearance of Promise.allSettled can solve this pain point:
Promise.allSettled([
Promise.reject({ code: 500, msg: 'Service exception' }),
Promise.resolve({ code: 200, list: [] }),
Promise.resolve({ code: 200, list: [] })
]).then(res => {
console.log(res)
/*
0: {status: "rejected", reason: {…}}
1: {status: "fulfilled", value: {…}}
2: {status: "fulfilled", value: {…}}
*/
// Filter out the rejected state and ensure as much data rendering in the page area as possible
RenderContent(
res.filter(el => {
return el.status !== 'rejected'
})
)
})
Promise.allSettled is similar to Promise.all. Its parameter accepts an array of Promises and returns a new Promise. The only difference is that it does not short-circuit , that is to say, when all Promises are processed, we can get each Promise. The state of the Promise, regardless of whether it was successfully processed or not.
5. Promise.race()
The effect of the Promise.all() method is "whoever runs slower, executes the callback based on whoever runs", then there is another method "whoever runs faster, executes the callback based on whoever runs faster", which is Promise.race () method, the word originally means race. The usage of race is the same as that of all, receiving an array of promise objects as a parameter.
Promise.all will continue the subsequent processing after all received object promises become FulFilled or Rejected. In contrast, Promise.race will continue as long as one promise object enters the FulFilled or Rejected state. Perform subsequent processing.
// execute resolve after `delay` milliseconds
function timerPromisefy(delay) {
return new Promise(resolve => {
setTimeout(() => {
resolve(delay);
} }, delay);
});
}
// If any promise becomes resolve or reject, the program stops running
Promise.race([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64)
]).then(function (value) {
console.log(value); // => 1
});
The above code creates 3 promise objects. These promise objects will become a definite state after 1ms, 32ms and 64ms respectively, that is, FulFilled, and after the first 1ms to become a definite state, the callback function registered by .then will be will be called.
6. Promise.prototype.finally()
ES9 adds a finally() method that returns a Promise. At the end of the promise, whether the result is fulfilled or rejected, the specified callback function will be executed. This provides a way for code that needs to execute whether the Promise completes successfully or not . This avoids the situation where the same statement needs to be written once each in then() and catch().
For example, before we send a request, there will be a loading. When our request is sent, no matter whether there is an error in the request, we want to turn off this loading.
this.loading = true
request()
.then((res) => {
// do something
})
.catch(() => {
// log err
})
.finally(() => {
this.loading = false
})
The callback function of the finally method does not accept any parameters, which indicates that the operations in the finally method should be independent of the state and not dependent on the execution result of the Promise.
Practical Application
Suppose there is such a requirement: the red light turns on once every 3s, the green light turns on once for 1s, and the yellow light turns on once for 2s; how to make the three lights turn on alternately and repeatedly?
Three lighting functions already exist:
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
The complicated part of this question is that it needs to "alternately repeat" the lights , rather than a one-shot deal that ends after one time. We can achieve this through recursion:
// implement with promise
let task = (timer, light) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
}
if (light === 'green') {
green()
}
if (light === 'yellow') {
yellow()
}
resolve()
}, timer);
})
}
let step = () => {
task(3000, 'red')
.then(() => task(1000, 'green'))
.then(() => task(2000, 'yellow'))
.then(step)
}
step()
The same can also be achieved through async/await:
// async/await
let step = async () => {
await task(3000, 'red')
await task(1000, 'green')
await task(2000, 'yellow')
step()
}
step()
Using async/await can write asynchronous code in the style of synchronous code. There is no doubt that the async/await solution is more intuitive, but a deep understanding of Promise is the basis for mastering async/await.