Do you really understand Promise

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 we 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 */ )
  • 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.
  • After the resolve function is called in the executor function, promise Callback function set by then; After the reject function is called, promise will be triggered Callback function set by catch.

It is worth noting that Promise is used to manage asynchronous programming. It is not asynchronous in itself. new Promise will immediately execute the executor function, but we generally deal with an asynchronous operation in the executor function. For example, in the following code, 2 will be printed 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 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
// Successful 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,
Execute P1 When then, two functions are stored (at this time, these two functions have not been executed), and then 3 is printed. At this time, the synchronization task is completed, and finally the micro task is executed to execute Successful methods 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 use the last object p2 Catch to catch exceptions. In this way, the errors of all Promise objects can be combined 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 the code we write 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 inherent behavior characteristics of Promise:

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

First, explain the meaning of this paragraph through the following examples, 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

We return promise Reject (1), completing the first promise p2 created and returned by calling then. The then call of p2 will start from return promise The reject (1) statement accepts the completion value. 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), whether the result of returning this instance is success or failure 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 
// The executor function has an abnormal error during execution, 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

It can be seen from the example above that Promise () can significantly solve the problem that the whole process is full of ambiguous code.

async/await, a new asynchronous programming method in ES7, is based on Promise. In short, async function returns Promise object and 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 in Promise for error capture, 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() {
  // Promise can be used if the following code has no dependency All way
  // 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 judge 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 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.
  • No matter whether the macro task arrives at the time or not and the order in which it is placed, every time the main thread execution stack is empty, the engine will give priority to processing the micro task queue, process all the tasks in the micro task queue, and then process the macro task.

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 out 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 encountering the for loop synchronization code, you need to wait 150ms. Although the second setTimeout has reached the time, it will not be executed. When encountering 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()

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

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

Promise. The parameters of the 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) {
 // Promise. The parameters of the 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

Promise. The 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 promise directly Resolve() method.

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

(4) Parameter is a thenable object

Thenable object refers to the object with then method, Promise The resolve method turns 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()

Promise. The reject () method returns a promise object with a reject reason.

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

// usage method
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 The all call terminates immediately and returns 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, promise is introduced into ES2020 The allsettled () method is useful. Without this method, it is troublesome to ensure that all operations are completed. Promise. The 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 request three interfaces concurrently. If any one of the interfaces is abnormal, the status is reject, which will cause the data of the three areas in the page to be unable to come out. Obviously, this situation is unacceptable. Promise The emergence of 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 and promise All is similar. 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()

Promise. The effect of the 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 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 not proceed with the following processing until all the received object promises have changed to FulFilled or Rejected state. In contrast, promise As long as a promise object in race enters the state of FulFilled or Rejected, it will continue the subsequent processing.

// `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 will stop 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, and after 1ms of the first change to the determined state The callback function registered by then will be called.

6,Promise.prototype.finally()

ES9 adds a finally() method to return a promise. When the function is executed, the callback will end, regardless of whether the function is fired or fired. 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, there will be a loading 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 operation in the finally method should be state independent and independent of 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:

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

It can also be realized through 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 scheme of async/await is more intuitive, but an in-depth understanding of Promise is the basis for mastering async/await.

Pea resource search websitehttps://55wd.com Guangzhou vi design companyhttp://www.maiqicn.com

Posted by ruano84 on Sun, 15 May 2022 22:27:05 +0300