preface

Promise plays an important role in asynchronous programming, making it more reasonable and powerful than traditional solutions (callback functions and events). Some people may wonder: In 2020, why are we still talking about Promise? In fact, some friends seem to know everything about this “old friend” they are dealing with almost every day. However, if you go deeper, you may have many questions. This article takes you to understand this familiar stranger in depth — Promise.

Basic usage

1. Grammar

new Promise( function(resolve, reject) {... }/* executor */  )
Copy the code
  • When you build a Promise object, you pass in an Executor function, where the main business process is executed.
  • The Promise constructor calls the executor function immediately upon execution. Resolve and reject are passed as arguments to the executor. Resolve and reject are called. This is a big pity or promise (fulfilled) or rejected (failed). Once the state changes, it never changes again, and you can get this result at any time.
  • A call to resolve from the executor function fires the promise. Then callback; Calling reject triggers the callback set by promise.catch.

It is important to note that promises are used to manage asynchronous programming. They are not asynchronous per se. New Promises will execute executor functions immediately, but we will normally handle an asynchronous operation within executor functions. For example, in the following code, the initial print is 2.

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

Promise uses the callback function deferred binding technique. If the resolve function is not bound at the time of execution, the callback function can only be deferred. What exactly does that mean? Let’s start with the following example:

let p1 = new Promise((resolve,reject) = >{
  console.log(1);
  resolve('Boat on the waves')
  console.log(2)})// then: Sets the processing method 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
// He who swims through the waves sails
Copy the code

A new Promise executes the executor function first, printing 1 and 2. A Promise executes resolve, firing the microtask, or continuing the synchronization task. P1. then stores two functions (which have not yet been executed), and prints 3. At this point, the synchronization task is completed, and finally the microtask is executed, thus executing the successful method in. Then.

Error handling

Errors on the Promise object are “bubbling” and are passed backwards until they are processed 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, a successful or failed method is executed, but if the method is not defined in the current THEN, the next corresponding function is continued

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)
Copy the code

This code has three Promise objects: P0 to P2. No matter which object throws an exception, the last object, p2.catch, can be caught. This way, all Promise object errors can be consolidated into a single function, eliminating the need for each task to handle exceptions separately.

This way, we eliminate nested calls and frequent error handling, which makes our code more elegant and linear.

Promise chain call

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

  • Every time you call THEN on a Promise, it creates and returns a new Promise, which we can link together;
  • Whatever value is returned from the completion callback (the first argument) called from then is automatically set to the completion of the linked Promise (in point 1).

First, use the following example to explain the meaning of this paragraph, and then introduce the execution flow of the next chain call in detail

let p1=new Promise((resolve,reject) = >{
    resolve(100) // Determines which successful methods 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, determining that the current instance failed, so determining that the failed method in the next THEN will be executed
},reason=>{
    console.log('Failure 1'+reason)
    return 200
})
/ / connect the p2
let p3=p2.then(result= >{
    console.log('Success 2'+result)
},reason=>{
    console.log('Failure 2'+reason)
})
// Success 1 100
// fail 2 1
Copy the code

We complete the Promise P2 created by the first call to THEN and return it by returning promise.reject (1). P2’s then call accepts completion values at run time from the return Promise.reject(1) statement. Of course, p2.then creates another new promise, which can be stored in the variable P3.

An instance of the new Promise will succeed or fail, depending on whether the executor executes resolve or Reject, or if the executor executes an exception, both of which will change the state of the instance to fail.

P2 The state of the new instance returned by executing then determines which method will be executed in the next THEN.

  • Any execution that throws an exception, whether it is a successful method execution or a failed method execution (both methods in then), changes the state of the instance to failed.
  • Method returns a new Promise instance (such as promise.reject (1) in the previous example), and whether that instance succeeds or fails determines whether the current instance succeeds or fails.
  • The rest is basically to get the instance to a successful state, and the results returned by the methods in the previous THEN are passed to the methods in the next THEN.

Let’s look at another example

new Promise(resolve= >{
    resolve(a) / / an error
// This executor function has an exception that determines that the next then failed method will be executed
}).then(result= >{
    console.log(` success:${result}`)
    return result*10
},reason=>{
    console.log(` failure:${reason}`)
// No exception occurs or a failed Promise instance is returned, so the next THEN successful method is executed
// There is no return. Undefined is returned
}).then(result= >{
    console.log(` success:${result}`)
},reason=>{
    console.log(` failure:${reason}`)})// ReferenceError: A is not defined
// success: undefined
Copy the code

async & await

As you can see from some of the examples above, while using Promise is a good way to solve callback hell, it’s full of Promise’s THEN () method, and if the process is complicated, the whole code will be filled with THEN, which is not semantically obvious. The code does not represent the flow of execution well.

The new asynchronous programming method in ES7, async/await implementation is based on Promise. In short, async functions return Promise objects, which are syntactic sugar of generator. Many people think async/await is the ultimate solution for asynchronous operations:

  • The syntax is concise, more like synchronous code, and more in line with common reading habits;
  • Improve the code organization of serial execution of asynchronous operation in JS, reduce the nesting of callback;
  • The use of try/catch for error catching cannot be customized in promises, but can be handled in Async/await as in synchronous code.

However, there are some disadvantages, because await transforms asynchronous code into synchronous code, and if multiple asynchronous code uses await without dependencies, performance will suffer.

async function test() {
  // If the following code does not have dependencies, you can use promise.all
  // If there is a dependency, it is an example of solving callback hell
  await fetch(url1)
  await fetch(url2)
  await fetch(url3)
}
Copy the code

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

let p1 = Promise.resolve(1)
let p2 = new Promise(resolve= > {
  setTimeout((a)= > {
    resolve(2)},1000)})async function fn() {
  console.log(1)
// When the code executes to this line (first this line), build an asynchronous microtask
// Wait for the promise to return the result and await the code below the await to be queued
  let result1 = await p2
  console.log(3)
  let result2 = await p1
  console.log(4)
}
fn()
console.log(2)
// 1, 2, 3, 4
Copy the code

If the logic on the right side of ‘await’ is a promise, ‘await’ will wait for the return result of the promise. ‘Await’ will only return the result if the return state is’ resolved ‘. If the promise is in a failed state, ‘await’ will not receive the return result. The code under 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
}
Copy the code

Let’s move on to a more complicated 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(for(leti=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
Copy the code

To solve this problem, the reader needs to know:

  • Microtask-based technologies include MutationObserver, Promise, and many others developed based on promises. Resolve () and await fn() are microtasks in this case.
  • Every time the main thread execution stack is empty, the engine processes the microtask queue first, regardless of the arrival time and the order in which the macro tasks are placed.

Next, let’s analyze it step by step:

  • First execute the synchronization code, print 1, encounter the first setTimeout, put its callback into the task queue (macro task), and continue to execute
  • Run run(), print 5 and go down, meet await fn(), put it in the task queue (microtask)
  • Await fn() the fn function will execute immediately when the current line of code is executed, print 3, meet the second setTimeout, put its callback into the task queue (macro task), await fn() the code below will wait to return the Promise state before executing, so 6 will not be printed.
  • The second setTimeout has reached the time limit, but it will not be executed. The third setTimeout is encountered, its callback is put into the task queue (macro task), and 10 is printed. Note that the timer delay of 0 milliseconds is not actually achievable. According to the HTML5 standard, setTimeOut delays execution by at least 4 milliseconds.
  • When the synchronization code is finished and there is no microtask at this time, the macro task will be executed. As mentioned above, setTimeout that has reached the point will be executed first and 4 will be printed
  • The next setTimeout macro task is then executed, so 7 is printed, the Executor function is immediately executed on New Promise, 8 is printed, and the resolve microtask is triggered, 9 is printed
  • Finally, execute the first setTimeout macro and print out 2

Common methods

1, Promise. Resolve ()

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

Promise.resolve('foo')
/ / equivalent to the
new Promise(resolve= > resolve('foo'))
Copy the code

The parameters of the promise.resolve method are divided into four cases.

(1) The argument is an instance of Promise

If the argument is a Promise instance, promise.resolve will return the instance unchanged.

const p1 = new Promise(function (resolve, reject) {
  setTimeout((a)= > reject(new Error('fail')), 3000)})const p2 = new Promise(function (resolve, reject) {
  setTimeout((a)= > resolve(p1), 1000)
})
p2
  .then(result= > console.log(result))
  .catch(error= > console.log(error))
// Error: fail
Copy the code

In the code above, P1 is a Promise. After 3 seconds, it changes to Rejected. The state of p2 changes after 1 second, and the resolve method returns P1. Since P2 returned another Promise, p2’s own state became invalid, and p1’s state determined P2’s state. Therefore, subsequent then statements become for the latter (P1). After another 2 seconds, P1 changes to Rejected, triggering the callback specified by the catch method.

(2) Arguments are not objects with then methods, or are not objects at all

Promise.resolve("Success").then(function(value) {
 // Promise.resolve is passed to the callback as well.
  console.log(value); // "Success"
}, function(value) {
  // will not be called
});
Copy the code

(3) Without any parameters

The promise.resolve () method allows a call with no arguments to return a Resolved Promise object. If you want to get a Promise object, it’s convenient to call the promise.resolve () method directly.

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

(4) Argument is a Thenable object

The thenable object refers to an object that has a THEN method. The Promise.resolve method converts this object to a Promise and immediately executes the THenable object’s THEN method.

let thenable = {
  then: function(resolve, reject) {
    resolve(42); }};let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  / / 42
});
Copy the code

2, Promise. Reject ()

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

new Promise((resolve,reject) = > {
    reject(new Error("Something went wrong."));
});
/ / equivalent to the
 Promise.reject(new Error("Something went wrong."));  

// How to use it
Promise.reject(new Error("BOOM!")).catch(error= > {
    console.error(error);
});
Copy the code

It’s worth noting that once you call resolve or Reject, the Promise’s mission is complete, and subsequent actions should be placed in the then method, not directly after resolve or Reject. Therefore, it is best to precede them with a return statement so there are no surprises.

new Promise((resolve, reject) = > {
  return reject(1);
  // The following statement will not be executed
  console.log(2);
})
Copy the code

3, Promise. All ()

let p1 = Promise.resolve(1)
let p2 = new Promise(resolve= > {
  setTimeout((a)= > {
    resolve(2)},1000)})let p3 = Promise.resolve(3)
Promise.all([p3, p2, p1])
  .then(result= > {
 // Returns the order in which instances are written to Array
    console.log(result) // [3, 2, 1]
  })
  .catch(reason= > {
    console.log("Failure:" reason")})Copy the code

Promise.all generates and returns a new Promise object, so it can use all the methods of a Promise instance. The method returns when all the promise objects in the promise array become resolve, and newly created promises use those promise values.

If any of the promises in the arguments is reject, the entire promise. all call terminates immediately and returns a new Reject Promise object.

4, Promise. AllSettled ()

Sometimes we don’t care about the results of asynchronous operations, only whether they end or not. At this point, it is useful for ES2020 to introduce the promise.allSettled () method. Without this method, it would be a hassle to make sure everything was done. The promise.all () method doesn’t do this.

Imagine a scenario like this: A page has three areas corresponding to three independent interface data. Use Promise. All to request three interfaces concurrently. Promise. AllSettled 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: "depressing ", value: {... This is a big pity. 1: {status: "depressing ", value: {... }} * /
  // Filter out the Rejected state to ensure as much page area data rendering as possible
  RenderContent(
    res.filter(el= > {
      returnel.status ! = ='rejected'}})))Copy the code

Promise.allsettled is similar to promise.all, in that it accepts an array of promises and returns a new Promise, except that it will not be short-circuited. This means that when all promises are processed, we can get the state of each Promise, regardless of whether they were processed successfully or not.

5, Promise. Race ()

The promise.all () method has the effect of “the slower runner executes the callback,” as opposed to the “faster runner executes the callback,” which is the promise.race () method, which originally means a race. Race is used like all, taking an array of Promise objects as arguments.

Promise. All will go on with the subsequent processing after all the objects received will become a pity or the Rejected state. This is a pity or Rejected state. Race will continue to process the Promise as long as one of the Promise objects enters the FulFilled or Rejected state.

// execute resolve after 'delay' milliseconds
function timerPromisefy(delay) {
    return new Promise(resolve= > {
        setTimeout((a)= > {
            resolve(delay);
        }, delay);
    });
}
// If either 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
});
Copy the code

The code above creates three Promise objects, which will become a certain state after 1ms, 32ms and 64ms respectively, namely depressing. After the first state becomes a certain state for 1ms, the registered callback function of.then will be called.

6, Promise. Prototype. Finally ()

The new finally() method in ES9 returns a Promise. At the end of the promise, the specified callback function will be executed, whether the result is fulfilled or Rejected. This provides a way for code to be executed after a Promise is successfully completed or not. This avoids the need to write the same statement once in both then() and catch().

For example, before sending a request, there will be a loading. When the request is sent, we want to stop this loading no matter whether the request is wrong or not.

this.loading = true
request()
  .then((res) = > {
    // do something
  })
  .catch((a)= > {
    // log err
  })
  .finally((a)= > {
    this.loading = false
  })
Copy the code

The finally callback takes no arguments, indicating that operations in the finally method should be state independent and not depend on the result of a Promise execution.

The practical application

Suppose there is such a requirement: red light 3s on once, green light 1s on once, yellow light 2s on once; How do I make three lights turn on again and again? Three lighting functions already exist:

function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}
Copy the code

The complexity of this problem lies in the fact that the lights need to be turned on “alternately and repeatedly”, rather than a one-shot deal that ends after the light is turned on once. We can achieve this by recursion:

// Use promise
let task = (timer, light) = > {
  return new Promise((resolve, reject) = > {
    setTimeout((a)= > {
      if (light === 'red') {
        red()
      }
      if (light === 'green') {
        green()
      }
      if (light === 'yellow') { yellow() } resolve() }, timer); })}let step = (a)= > {
  task(3000.'red')
    .then((a)= > task(1000.'green'))
    .then((a)= > task(2000.'yellow'))
    .then(step)
}
step()
Copy the code

It can also be implemented with async/await:

/ / async/await
let step = async() = > {await task(3000.'red')
  await task(1000.'green')
  await task(2000.'yellow')
  step()
}
step()
Copy the code

With async/await, asynchronous code can be written in the style of synchronous code. There is no doubt that async/await scheme is more intuitive, but a deep understanding of Promise is the foundation to master async/await.

Welcome to pay attention to the public number: front-end craftsmen, we will witness your growth together!

The resources

  • MDN document
  • Do you know Promise?
  • Working principle and practice of browser
  • Web front-end development Senior engineer
  • Advanced core knowledge of front-end development
  • Learn JavaScript ES(6-10) full version of the syntax
  • ECMAScript 6 Introduction
  • Javascript You Don’t Know (Middle Volume)