The basic concept

MDN definition of Promise:

A Promise is an object that represents the final completion or failure of an asynchronous operation.

Promise addresses primarily the problem of asynchronous coding styles, the problem of callback hell. As you develop more complex projects, it’s easy to get stuck in callback hell if you have too many nested callback functions. You can refer to the messy code below:

XFetch(makeRequest('https://xxx'),
      function resolve(response) {
          console.log(response)
          XFetch(makeRequest('https://xxx'),
              function resolve(response) {
                  console.log(response)
                  XFetch(makeRequest('https://xxx')
                      function resolve(response) {
                          console.log(response)
                      }, function reject(e) {
                          console.log(e)
                      })
              }, function reject(e) {
                  console.log(e)
              })
      }, function reject(e) {
          console.log(e)
      })
Copy the code

This code requests the first interface, then the second interface if successful, and then the last interface if successful. In other words, the code is already cluttered with three layers of nested requests, which can become a disaster when a fourth (or more) request in the above requirement still relies on the previous one.

This code looks messy for two reasons:

  • The first is nested calls, where the following tasks rely on the result of the previous task’s request and execute the new business logic inside the callback function of the previous task, so that the code becomes very unreadable as the nesting level increases.
  • The second is the uncertainty of the task. There are two possible results (success or failure) for each task, so it is reflected in the code that the execution result of each task needs to be judged twice. Such an extra error handling for each task significantly increases the level of confusion in the code.

At this point, we might want to:

  • The first is to eliminate nested calls;
  • The second is error handling for merging multiple tasks.

Promise solves the nested callback problem in two main steps.

  1. First, Promise implements delayed binding of callback functions. The delayed binding of the callback is code that creates the Promise object promise1 and uses the Promise constructor executor to execute the business logic. After creating the Promise object promise1, use promise1. Then to set the callback function.

  2. Second, the return value of the callback function onResolve needs to be penetrated to the outermost layer. Because we decide what type of Promise task to create based on the incoming value of the onResolve function, the created Promise object needs to be returned to the outermost layer to get rid of the nested loop. The code is as follows:

let promise1 = new Promise((resolve, reject) => {
    console.log(1)
    resolve('success')
})

// thenMethod delayed binding returns a value through assignment to P2let p2 = promise1.then(res => {
    console.log(res)
    return2})setTimeout(() => {console.log(p2)}Copy the code

Promise solves loop nesting with callback function delayed binding and callback function return value penetration. Let’s look at how promises handle exceptions:


function executor(resolve, reject) {
    let rand = Math.random();
    console.log(1)
    console.log(rand)
    ifRand (> 0.5) resolve ()else
        reject()
}
var p0 = new Promise(executor);

var p1 = p0.then((value) => {
    console.log("succeed-1")
    return new Promise(executor)
})
p1.catch((error) => {
    console.log("error")
})
console.log(2)
Copy the code

Any error reported by p0, P1 can be caught in the last catch. The reason you can use the last object to catch all exceptions is because of the “bubbling” nature of errors in the Promise object, which 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.

The principle of analyzing

We’ve seen that Promise is implemented primarily through delayed callbacks, return value penetration, and error ‘bubbling’ capture.

Next, let’s take a closer look at these technical implementations through source code implementations, and dig deeper into Promise.

Minimalist Promise prototype

functionPromise(executor) {this.value = null // Store resolved value this.reason = null // store rejected value this. onelet = [] This. OnRejectedArray = [] Use an array to store const resolve = value = > {. This value this value =. OnFulfilledArray. ForEach (func = > {func (value)})} const reject = reason => { this.reason = reason this.onRejectedArray.forEach(func => { func(reason) }) } executor(resolve, reject) } Promise.prototype.then =function(onfulfilled, onrejected) {
    this.onFulfilledArray.push(onfulfilled)
    this.onRejectedArray.push(onrejected)
}
Copy the code

The above code is relatively simple, and the general logic looks like this:

  1. The then method is called to put the callback you want to execute on the success of the Promise asynchronous operation into the onRejectedArray queue and the callback you want to execute on the failure into the onRejectedArray queue.
  2. The functions passed in when a Promise instance is created are given parameters of the function type, resolve and Reject, which receive a parameter representing the result returned by the asynchronous operation, and the user calls the resolve method when the step succeeds. On failure, the reject method is executed. What you’re really doing is executing the callbacks in the queue one by one.

Add a delay binding mechanism

As the observant eye might notice, there are a few problems with this code: Resolve and reject are executed before the THEN callback. Therefore, the resolve and Reject executions need to be placed on the task queue. (This is not strictly done. To ensure that promises belong to microtasks, many Promise implementation libraries use MutationObserver to mimic nextTick.)

 const resolve = value => {
    // setTimeout simulates asynchronous executionsetTimeout(() => { 
      this.value = value
      this.onFulfilledArray.forEach(func => {
        func(value)
      })
    })
  }

  const reject = reason => {
    // setTimeout simulates asynchronous executionsetTimeout(() => { 
      this.reason = reason
      this.onRejectedArray.forEach(func => {
        func(reason)
      })
    })
  }
Copy the code

Join the state

Promises/A+ Promise States clearly specify that pending can be changed to A pity or rejected and can only be changed once. That is to say, if pending is changed to A pity state, So you can’t convert to Rejected again. Moreover, the depressing and Rejected states can only be converted by pending, and they cannot be converted to each other. A picture is worth a thousand words:

function Promise(executor) {
  this.status = 'pending'
  this.value = null
  this.reason = null
  this.onFulfilledArray = []
  this.onRejectedArray = []

  const resolve = value => {
    // setTimeout simulates asynchronous executionsetTimeout(() => { 
      if (this.status === 'pending') {
        this.value = value
        this.status = 'fulfilled'

        this.onFulfilledArray.forEach(func => {
          func(value)
        })
      }
    })
  }

  const reject = reason => {
    // setTimeout simulates asynchronous executionsetTimeout(() => { 
      if (this.status === 'pending') {
        this.reason = reason
        this.status = 'rejected'
        
        this.onRejectedArray.forEach(func => {
          func(reason)
        })
      }
    })
  }
  executor(resolve, reject)
}

Promise.prototype.then = function(onfulfilled, onrejected) {
  if (this.status === 'fulfilled') {
    onfulfilled(this.value)
  }
  if (this.status === 'rejected') {
    onrejected(this.reason)
  }
  if (this.status === 'pending') {
    this.onFulfilledArray.push(onfulfilled)
    this.onRejectedArray.push(onrejected)
  }
}
Copy the code

This code goes like this: When resolve and Reject are executed, the state is set to fulfilled and Rejected, and any new callbacks added by then are immediately executed.

Chain Promise

Let’s start with the following examples:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
      resolve('lucas')
  }, 2000)
})

promise.then(data => {
  console.log(data)
  return new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(`${data} next then`)
    }, 4000)
  })
})
.then(data => {
  console.log(data)
})
Copy the code

The output will be 2 seconds later: Lucas, followed by 4 seconds later (6th second) : Lucas next then.

Those of you who have used promises know that there are a lot of them, so something like this is called a chained promise.

The onfulfilled function and onRejected function of the then method of a Promise instance can be supported to return a Promise instance again, and also to return the normal value of a non-Promise instance. The return Promise instance or the normal value of the non-Promise instance will be passed to the next THEN method onfulfilled or onRejected function, thus supporting chain calls.

After coding the then method:

Click to expand the full code
// The result variable can be either a normal value or a Promise instance, Const resolvePromise = (promise2, result, resolve, // When result and promise2 are equal, that is, ondepressing returns promise2, Reject if (result === promise2) {return Reject (new TypeError('error due to circular reference')) Onfulfilled or onFailed let Consumed = false let thenable if (result instanceof Promise) {if (result.status === 'pending') { result.then(function(data) { resolvePromise(promise2, data, resolve, reject) }, reject) } else { result.then(resolve, reject) } return } let isComplexResult = target => (typeof target === 'function' || typeof target === 'object') && (target ! If (isComplexResult(result)) {try {thenable = result.then If (typeof thEnable === 'function') {thenable.call(result, function(data) { if (consumed) { return } consumed = true return resolvePromise(promise2, data, resolve, reject) }, function(error) { if (consumed) { return } consumed = true return reject(error) }) } else { return resolve(result) } } catch(e) { if (consumed) { return } consumed = true return reject(e) } } else { return resolve(result) } } Promise.prototype.then = function(onfulfilled, onrejected) { onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data onrejected = typeof onrejected === 'function' ? onrejected : Let promise2 if (this. Status === 'depressing ') {return promise2 = new  Promise((resolve, Reject) => {setTimeout(() => {try {// This new promise2 resolved value is onfulfilled let result = onfulfilled(this.value) resolvePromise(promise2, result, resolve, reject) } catch(e) { reject(e) } }) }) } if (this.status === 'rejected') { return promise2 = new Promise((resolve, Reject) => {setTimeout(() => {try {// Let result = onRejected (this.reason) resolvePromise(promise2, result, resolve, reject) } catch(e) { reject(e) } }) }) } if (this.status === 'pending') { return promise2 = new Promise((resolve, reject) => { this.onFulfilledArray.push(value => { try { let result = onfulfilled(value) resolvePromise(promise2, result, resolve, reject) } catch(e) { return reject(e) } }) this.onRejectedArray.push(reason => { try { let result = onrejected(reason) resolvePromise(promise2, result, resolve, reject) } catch(e) { return reject(e) } }) }) } }Copy the code

Promise value through

Look at the following example:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
      resolve('lucas')
  }, 2000)
})


promise.then(null)
.then(data => {
  console.log(data)
})
Copy the code

This code will output after 2 seconds: Lucas. This is Promise penetration:

Passing a non-function value as an argument to.then() is actually resolved to.then(null), which should behave like this: The result of the previous promise object is “filtered”, and if a second.then() function still exists in the chain call, the filtered result will be obtained.

Value penetration code implementation:

Promise.prototype.then = functionThis will be a big pity = Function. Prototype, onrejected = Function. Prototype) {// This will be a big pitythenThe function that the method receives is not a function, then by default, the function that returns its value directly is assigned, thus implementing the onfulfilled = typeof onfulfilled ==='function' ? onfulfilled : data => data
  onrejected = typeof onrejected === 'function' ? onrejected : error => { throw error }

    // ...
}
Copy the code

Exception handling

Promise.prototype.catch can be used to catch exceptions.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
      reject('lucas error')
  }, 2000)
})

promise1.then(data => {
  console.log(data)
}).catch(error => {
  console.log(error)
})
Copy the code

Lucas error will be printed after 2 seconds.

In this case, it would be equivalent to:

Promise.prototype.catch = function(catchFunc) {
  return this.then(null, catchFunc)
}
Copy the code

Because we know that the second argument to the.then() method also does exception catching, we implement promise.prototype.catch relatively simply with this feature.

Promise. Prototype. Resolve to achieve

Take an example:

Promise.resolve('data').then(data => {
  console.log(data)
})
console.log(1)
Copy the code

Print 1 and then data.

Implementing promise.resolve (value) is also simple:

Promise.resolve = function(value) {
  return new Promise((resolve, reject) => {
    resolve(value)
  })
}
Copy the code

Implement a promise.reject (value) :

Promise.reject = function(value) {
  return new Promise((resolve, reject) => {
    reject(value)
  })
}
Copy the code

conclusion

Finally post the full code:

Click to expand the full code
function Promise(executor) { this.status = 'pending' this.value = null this.reason = null this.onFulfilledArray = [] this.onRejectedArray = [] const resolve = value => { if (value instanceof Promise) { return value.then(resolve, reject) } setTimeout(() => { if (this.status === 'pending') { this.value = value this.status = 'fulfilled' this.onFulfilledArray.forEach(func => { func(value) }) } }) } const reject = reason => { setTimeout(() => { if (this.status === 'pending') { this.reason = reason this.status = 'rejected' this.onRejectedArray.forEach(func => { func(reason) }) } }) } try { executor(resolve, reject) } catch(e) { reject(e) } } const resolvePromise = (promise2, // When result and promise2 are equal, that is, ondepressing returns promise2, Reject if (result === promise2) {return Reject (new TypeError('error due to circular reference')) Onfulfilled or onFailed let Consumed = false let thenable if (result instanceof Promise) {if (result.status === 'pending') { result.then(function(data) { resolvePromise(promise2, data, resolve, reject) }, reject) } else { result.then(resolve, reject) } return } let isComplexResult = target => (typeof target === 'function' || typeof target === 'object') && (target ! If (isComplexResult(result)) {try {thenable = result.then If (typeof thEnable === 'function') {thenable.call(result, function(data) { if (consumed) { return } consumed = true return resolvePromise(promise2, data, resolve, reject) }, function(error) { if (consumed) { return } consumed = true return reject(error) }) } else { return resolve(result) } } catch(e) { if (consumed) { return } consumed = true return reject(e) } } else { return resolve(result) } } Promise.prototype.then = function(onfulfilled, onrejected) { onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data onrejected = typeof onrejected === 'function' ? onrejected : Let promise2 if (this. Status === 'depressing ') {return promise2 = new  Promise((resolve, Reject) => {setTimeout(() => {try {// This new promise2 resolved value is onfulfilled let result = onfulfilled(this.value) resolvePromise(promise2, result, resolve, reject) } catch(e) { reject(e) } }) }) } if (this.status === 'rejected') { return promise2 = new Promise((resolve, Reject) => {setTimeout(() => {try {// Let result = onRejected (this.reason) resolvePromise(promise2, result, resolve, reject) } catch(e) { reject(e) } }) }) } if (this.status === 'pending') { return promise2 = new Promise((resolve, reject) => { this.onFulfilledArray.push(value => { try { let result = onfulfilled(value) resolvePromise(promise2, result, resolve, reject) } catch(e) { return reject(e) } }) this.onRejectedArray.push(reason => { try { let result = onrejected(reason) resolvePromise(promise2, result, resolve, reject) } catch(e) { return reject(e) } }) }) } } Promise.prototype.catch = function(catchFunc) { return this.then(null,  catchFunc) } Promise.resolve = function(value) { return new Promise((resolve, reject) => { resolve(value) }) } Promise.reject = function(value) { return new Promise((resolve, reject) => { reject(value) }) }Copy the code

reference

  • 30 minutes to make sure you understand the Promise principle
  • Working principle and practice of browser
  • Advanced core knowledge of front-end development