MEMEPh. ideas that are worth sharing...

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 */  )

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:

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:

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:

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:

Next, we analyze step by step:

 

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.