Do you really understand Promise

Posted by gyoung on Mon, 08 Nov 2021 17:08:57 +0100

preface

Promise plays an important role in asynchronous programming, which is more reasonable and powerful than traditional solutions (callback functions and events). Some small partners may have such questions: Why are you still talking about promise in 2020? In fact, some friends seem to know all about this "old friend" they deal with almost every day, but a little in-depth may be full of questions. This article takes you to deeply understand this familiar stranger promise

Basic Usage

1. Grammar

new Promise( function(resolve, reject) {...} /* executor */  )

   
  • 1
  • When building Promise objects, you need to pass in an executor function, 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 to the executor as parameters. When the resolve and reject functions are called, the state of promise is changed to completed or rejected respectively. Once the state changes, it will not change again. This result can be obtained at any time.
  • Calling the resolve function in the executor function triggers callback function set by promise.then. 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 in itself. When new Promise is used, the executor function will be executed immediately, but we generally deal with an asynchronous operation in the executor function. For example, in the following code, 2 will be printed first.

let p1 = new Promise(()=>{
    setTimeout(()=>{
      console.log(1)
    },1000)
    console.log(2)
  })
console.log(3) // 2 3 1

Promise uses the callback function delay binding technology. When the resolve function is executed, the callback function is not bound, so it can only delay the execution of the callback function. What exactly does that mean? Let's start with the following example:

let p1 = new Promise((resolve,reject)=>{
  console.log(1);
  resolve('Boating in the waves')
  console.log(2)
})
// then: set the processing method after success or failure
p1.then(result=>{
 //p1 delay binding callback function
  console.log('success '+result)
},reason=>{
  console.log('fail '+reason)
})
console.log(3)
// 1
// 2
// 3
// Success boating in the waves

In the case of new Promise, execute the executor function first and print out 1 and 2. When Promise executes resolve, trigger the micro task or continue to execute the synchronization task,
When p1.then is executed, two functions are stored (at this time, the two functions have not been executed), and then 3 is printed. At this time, the synchronization task is completed, and finally the micro task just executed, so as to execute the successful method in. Then.

error handling

The error of Promise object is "bubbling" and will be passed back until it is processed by onReject function or caught by catch statement. With this "bubbling" feature, you don't need to catch exceptions separately in each Promise object.

To encounter a then, execute the successful or failed method, but if this method is not defined in the current then, it will be postponed 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, you can catch the exception through the last object p2.catch. In this way, you can combine the errors of all Promise objects into one function for processing, which solves the problem that each task needs to handle exceptions separately.

In this way, we eliminate nested calls and frequent error handling, which makes our code more elegant and more in line with people's linear thinking.

Promise chained call

We all know that multiple promises can be connected together to represent a series of different steps. The key to this approach lies in the following two Promise inherent behavior characteristics:

  • Every time you call then on Promise, it will create and return a new Promise, which we can link;
  • Regardless of the value returned from the completion callback (first parameter) called by then, it is automatically set to the completion of the linked Promise (in the first point).

First, explain what this paragraph means through the following example, and then introduce the execution process of 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
})
// Connection p1
let p2=p1.then(result=>{
    console.log('Success 1 '+result)
    return Promise.reject(1) 
// A new Promise instance is returned, 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('Failed 1 '+reason)
    return 200
})
// Connection p2 
let p3=p2.then(result=>{
    console.log('Success 2 '+result)
},reason=>{
    console.log('Fail 2 '+reason)
})
// Success 1 100
// Failed 2 1

By returning Promise.reject(1), we completed the first promise p2 created and returned by calling then. The then call of p2 will accept the completion value from the return Promise.reject(1) statement at run time. Of course, p2.then creates another new promise, which can be stored with the variable p3.

The success or failure of the instance generated by new Promise depends on whether the executor function executes resolve or reject, or whether an abnormal error occurs in 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 successful method execution or failed method execution (the two methods in then), if the execution throws an exception, the state of the instance will be changed to failed.
  • 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, which also determines whether the current instance is success or failure.
  • The rest is basically to make the instance become a successful state, and the results returned by the method in the previous then will be passed to the method in the next then.

Let's take another example

new Promise(resolve=>{
    resolve(a) // report errors 
// An unexpected error occurred during the execution of the executor function, which determines that the next then failed method will be executed
}).then(result=>{
    console.log(`success: ${result}`)
    return result*10
},reason=>{
    console.log(`Failed: ${reason}`)
// When executing this sentence, no exception occurs or a failed Promise instance is returned, so the next then successful method will be executed
// There is no return here. undefined will be returned in 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 Promise can well solve the problem of callback hell, this method is full of Promise's then() method. If the processing process is complex, the whole code will be full of then, the semantics is unclear, and the code can not well represent the execution process.

The implementation of async/await, a new asynchronous programming method in ES7, is based on Promise. In short, async function returns Promise object, which is the syntax sugar of generator. Many people think async/await is the ultimate solution for asynchronous operation:

  • The syntax is concise, more like synchronous code, and more in line with ordinary reading habits;
  • Improve the code organization of asynchronous operation serial execution in JS and reduce the nesting of callback;
  • try/catch cannot be customized for error capture in Promise, but Async/await can handle errors 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, performance will be reduced.

async function test() {
  // If the following code has no dependency, Promise.all can be used
  // If there is dependency, it is actually an example of solving callback hell
  await fetch(url1)
  await fetch(url2)
  await fetch(url3)
}

Looking at the following code, can you tell what the printed content is?

let p1 = Promise.resolve(1)
let p2 = new Promise(resolve => {
  setTimeout(() => {
    resolve(2)
  }, 1000)
})
async function fn() {
  console.log(1)
// When the code is executed to this line (put this line first), build an asynchronous micro task
// Wait for 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. The result will be returned only if the returned state is resolved. If the promise is a failed state, await will not receive its return result, and the code below await will 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 execute
}

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()
//About 150ms is required
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:

  • The technologies based on micro tasks include MutationObserver, Promise and many other technologies developed based on Promise. In this topic, resolve() and await fn() are micro tasks.
  • Regardless of the arrival time and placement order of macro tasks, each time the main thread execution stack is empty, the engine will give priority to the micro task queue, process all tasks in the micro task queue, and then process macro tasks.

Next, we analyze step by step:

  • First, execute the synchronization code, output 1, encounter 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. When await fn() is met, put it into the task queue (micro task)
  • When await fn() executes the current line of code, FN function will execute immediately, print 3, encounter the second setTimeout, and put its callback into the task queue (macro task). The code under await fn() needs to wait for the return of Promise success, so 6 will not be printed.
  • Continue to execute. When you encounter the for loop synchronization code, you need to wait 150ms. Although the second setTimeout has reached the time, it will not be executed. When you encounter the third setTimeout, put its callback into the task queue (macro task), and then print out 10. It is worth noting that the delay time of this timer is 0 milliseconds, which is actually not reached. According to the HTML5 standard, setTimeout delays execution for at least 4 milliseconds.
  • After the synchronization code is executed, if there is no micro task at this time, execute the macro task. As mentioned above, the setTimeout that has arrived at the point shall be executed first and printed out 4
  • Then execute the macro task of the next setTimeout, so print out 7 first. When new Promise is executed, the executor function will be executed immediately, print out 8, and then trigger the micro task when resolve is executed, so print out 9
  • Finally, execute the macro task of the first setTimeout and print 2

Common methods

1,Promise.resolve()

The Promise.resolve(value) method returns a Promise object parsed with the given value.
Promise.resolve() is equivalent to the following expression:

Promise.resolve('foo')
// Equivalent to
new Promise(resolve => resolve('foo'))

The parameters of Promise.resolve method are divided into four cases.

(1) Parameter is a Promise instance

If the parameter is a Promise instance, Promise.resolve will return the instance intact 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 and becomes rejected after 3 seconds. The state of p2 changes after 1 second, and the resolve method returns p1. Since p2 returns another Promise, p2's own state is invalid. The state of p2 is determined by the state of p1. Therefore, the following then statements 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 it is not an object at all

Promise.resolve("Success").then(function(value) {
 // The parameters of 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 you to directly return a Promise object in the resolved state without parameters. If you want to get a Promise object, a more convenient method is to call the Promise.resolve() method directly.

Promise.resolve().then(function () {
  console.log('two');
});
console.log('one');
// one two

 
  • 1
  • 2
  • 3
  • 4
  • 5

(4) Parameter is a thenable object

A thenable object refers to an object with a then method. The Promise.resolve method converts this object into a Promise object, and then immediately executes 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 a rejection reason.

new Promise((resolve,reject) => {
    reject(new Error("Error "));
});
// Equivalent to
 Promise.reject(new Error("Error "));  

//Method of use
Promise.reject(new Error("BOOM!")).catch(error => {
console.error(error);
});

It is worth noting that after you call resolve or reject, Promise's mission is completed. Subsequent operations should be placed in the then method, not directly after resolve or reject. Therefore, it is best to add a return statement in front of them so that there will be no accidents.

new Promise((resolve, reject) => {
  return reject(1);
  // Subsequent 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 are written in the Array
    console.log(result) // [ 3, 2, 1 ]
  })
  .catch(reason => {
    console.log("fail:reason")
  })

Promise.all generates and returns a new promise object, so it can use all the methods of the promise instance. When all promise objects in the promise array are changed to resolve, the method will return, and the newly created promise will use the values of these promises.

If any Promise in the parameter is reject, the whole Promise.all call will terminate immediately and return a new Promise object of reject.

4,Promise.allSettled()

Sometimes, we don't care about the results of asynchronous operations, only whether these operations have ended. At this time, it is useful for ES2020 to introduce Promise.allSettled() method. Without this method, it is troublesome to ensure that all operations are completed. The Promise.all() method cannot do this.

If there is such a scenario: a page has three areas corresponding to three independent interface data, and Promise.all is used to request three interfaces concurrently. If any of the interfaces is abnormal and the status is reject, it will lead to the failure of all the data in the three areas on the page. Obviously, this situation is unacceptable, 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 the data rendering of the page area as much as possible
  RenderContent(
    res.filter(el => {
      return el.status !== 'rejected'
    })
  )
})

Promise.allSettled is similar to Promise.all. Its parameters accept an array of promises and return a new promise. The only difference is that it will not be short circuited. That is, after all promises are processed, we can get the status of each promise, regardless of whether the processing is successful or not.

5,Promise.race()

The effect of Promise.all() method is "Whoever runs slowly will execute the callback according to who", so there is another method "Whoever runs fast will execute the callback according to who", which is the Promise.race() method. The word originally means race. The usage of race is the same as that of all. It receives an array of promise objects as parameters.

Promise.all will continue the following processing only after all the received objects are in the state of FulFilled or Rejected. In contrast, Promise.race will continue the following processing as long as a promise object enters the state of FulFilled or Rejected.

// `delay ` resolve in milliseconds
function timerPromisefy(delay) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(delay);
        }, delay);
    });
}
// If any promise changes to 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 three promise objects. These promise objects will change to the determined state after 1ms, 32ms and 64ms respectively, that is, FulFilled. After the first 1ms, the callback function registered by. then will be called.

6,Promise.prototype.finally()

ES9 adds a finally() method to return a promise. At the end of the promise, the specified callback function will be executed regardless of whether the result is fully or rejected. This provides a way for code to be executed after promise is successfully completed. This avoids the situation where the same statement needs to be written once in then() and once in catch().

For example, a loading will appear before we send the request. After we send the request, we want to turn off the loading regardless of whether there is an error in the request.

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 do not depend on the Promise execution result.

practical application

Suppose there is such a demand: the red light is on once in 3s, the green light is on once in 1s, and the yellow light is on once in 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 complexity of this problem lies in the need for "alternating and repeated" lights, rather than a one hammer deal that ends after one light. We can achieve it by passing it back:

// Implementation 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()

You can also implement async/await:

//  async/await implementation
let step = async () => {
  await task(3000, 'red')
  await task(1000, 'green')
  await task(2000, 'yellow')
  step()
}
step()

async/await can be used to write asynchronous code in the style of synchronous code. There is no doubt that the async/await scheme is more intuitive, but an in-depth understanding of Promise is the basis for mastering async/await.! Recommend an easy-to-use BUG monitoring tool Fundebug , welcome to try for free!

Welcome to the official account: front-end craftsman, your growth, witness with us!

reference material