The use, principle and implementation of Promise in JavaScript

Keywords: Front-end Programming axios

1. What is Promise

Promise is the mainstream solution of JS asynchronous programming, which follows promise / A +.

2. Brief analysis of promise principle

(1) promise itself is a state machine with three states

  • pending
  • fulfilled
  • rejected
    When a project object is initialized, its state is pending. When resolve is called, the project state will be reversed to full, and when reject is called, the project state will be reversed to rejected. Once these two twists happen, the project can no longer be reversed to other states.

(2) there is a then method on the prototype of the project object. The then method will return a new project object and take the result of the callback function return as the result of the project resolve. The then method will be called when a project state is reversed to full or rejected. The parameters of the then method are two functions, i.e. the state of the project object is reversed to the callback function corresponding to fulfilled and rejected

3. How to use promise

A promise object is constructed, and the asynchronous function to be executed is passed into the parameters of promise. After calling the resolve() function after the end of the asynchronous execution, the execution result of the asynchronous function can be obtained in the then method of promise.

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve()
  }, 1000)
}).then(
  res => {},
  err => {}
)

At the same time, Promise also provides us with many convenient methods:

  • Promise.resolve

Promise.resolve returns a promise in the fully completed state

const a = Promise.resolve(1)
a.then(
  res => {
    // res = 1
  },
  err => {}
)
  • Promise.all

Promise.all receives an array of promise objects as parameters. It will continue the following processing only after all promises have changed to the full state. Promise.all itself returns a promise

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise1')
  }, 100)
})
const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise2')
  }, 100)
})
const promises = [promise1, promise2]

Promise.all(promises).then(
  res => {
    // The process of changing promises to full state
  },
  err => {
    // There is a process of changing to rejected state in projects
  }
)
  • Promise.race

Project.race is similar to project.all, except that this function will start the following processing after the state of the first project in projects is reversed (either fully or rejected)

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise1')
  }, 100)
})
const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise2')
  }, 1000)
})
const promises = [promise1, promise2]

Promise.race(promises).then(
  res => {
    // At this time, only promise 1 resolves and promise 2 is still in the pending state
  },
  err => {}
)

Use with async await

In the current development scenario, we usually use async await syntax sugar to wait for the execution result of a promise, which makes the code more readable. Async itself is a syntax sugar, which packs the return value of the function into a promise.

// The async function returns a promise
const p = async function f() {
  return 'hello world'
}
p.then(res => console.log(res)) // hello world

Development skills

In front-end development, promise is mostly used to request interface, Axios library is also the most frequently used library in development, but frequent try catch catch catch errors will make code nesting very serious. Consider the optimization of the following code

const getUserInfo = async function() {
  return new Promise((resolve, reject) => {
    // resolve() || reject()
  })
}
// In order to deal with possible throwing errors, we have to nest try catch outside the code. Once the nesting becomes more, the readability of the code will drop sharply
try {
  const user = await getUserInfo()
} catch (e) {}

A good way to deal with this is to catch the error in the asynchronous function and return it normally, as shown below

const getUserInfo = async function() {
  return new Promise((resolve, reject) => {
    // resolve() || reject()
  }).then(
    res => {
      return [res, null] // Processing successful return results
    },
    err => {
      return [null, err] // Return result of processing failure
    }
  )
}

const [user, err] = await getUserInfo()
if (err) {
  // err processing
}

// Is this a lot clearer

4.Promise source code implementation

Knowledge learning needs to know what is and why, so a promise realized by a little bit can have a deeper understanding of promise.

(1) first, implement a simple promise (based on the ES6 specification) according to the most basic promise call mode. Suppose we have the following call mode

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 1000)
})
  .then(
    res => {
      console.log(res)
      return 2
    },
    err => {}
  )
  .then(
    res => {
      console.log(res)
    },
    err => {}
  )

First of all, we need to implement a Promise class. The constructor of this class will pass in a function as a parameter, and two methods, resolve and reject, will be passed to the function.
The status of the initialization Promise is pending.

class MyPromise {
  constructor(executor) {
    this.executor = executor
    this.value = null
    this.status = 'pending'

    const resolve = value => {
      if (this.status === 'pending') {
        this.value = value          // Record the value of resolve after calling resolve
        this.status = 'fulfilled'   // Call resolve to reverse promise state
      }
    }

    const reject = value => {
      if (this.status === 'pending') {
        this.value = value          // Record the value of reject after calling reject
        this.status = 'rejected'    // Call reject to reverse the project state
      }
    }

    this.executor(resolve, reject)
  }

(2) next, to implement the then method on the project object, the then method will pass in two functions as parameters, which are the processing functions of resolve and reject of the project object.
Three points should be noted here:

  • The then function needs to return a new promise object
  • When the then function is executed, the state of the project may not be reversed to full or rejected
  • A promise object can call the then function multiple times at the same time
class MyPromise {
  constructor(executor) {
    this.executor = executor
    this.value = null
    this.status = 'pending'
    this.onFulfilledFunctions = [] // Store the first function parameter passed in the then function registered by promise
    this.onRejectedFunctions = [] // Store the second function parameter passed from the then function registered by promise
    const resolve = value => {
      if (this.status === 'pending') {
        this.value = value
        this.status = 'fulfilled'
        this.onFulfilledFunctions.forEach(onFulfilled => {
          onFulfilled() // Take out the functions in onFulfilledFunctions and execute them
        })
      }
    }
    const reject = value => {
      if (this.status === 'pending') {
        this.value = value
        this.status = 'rejected'
        this.onRejectedFunctions.forEach(onRejected => {
          onRejected() // Take out and execute the functions in onRejectedFunctions
        })
      }
    }
    this.executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    const self = this
    if (this.status === 'pending') {
      /**
       *  When the project is still in the "pending" state, you need to put the registered onFulfilled and onRejected methods in the onFulfilledFunctions and onRejectedFunctions of the project for standby
       */
      return new MyPromise((resolve, reject) => {
        this.onFulfilledFunctions.push(() => {
          const thenReturn = onFulfilled(self.value)
          resolve(thenReturn)
        })
        this.onRejectedFunctions.push(() => {
          const thenReturn = onRejected(self.value)
          resolve(thenReturn)
        })
      })
    } else if (this.status === 'fulfilled') {
      return new MyPromise((resolve, reject) => {
        const thenReturn = onFulfilled(self.value)
        resolve(thenReturn)
      })
    } else {
      return new MyPromise((resolve, reject) => {
        const thenReturn = onRejected(self.value)
        resolve(thenReturn)
      })
    }
  }
}

The test code for MyPromise completed above is as follows

const p = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 1000)
})

p.then(res => {
  console.log('first then', res)
  return res + 1
}).then(res => {
  console.log('first then', res)
})

p.then(res => {
  console.log(`second then`, res)
  return res + 1
}).then(res => {
  console.log(`second then`, res)
})

/**
 *  The output results are as follows:
 *  first then 1
 *  first then 2
 *  second then 1
 *  second then 2
 */

(3) in the related content of promise, one point is often ignored. What should we do when a promise is returned in the then function?
Consider the following code:

// Use the correct Promise
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve()
  }, 1000)
})
  .then(res => {
    console.log('external promise')
    return new Promise((resolve, reject) => {
      resolve(`inside promise`)
    })
  })
  .then(res => {
    console.log(res)
  })

/**
 * The output results are as follows:
 * External promise
 * Internal promise
 */

It is not difficult to judge from the above output results. When the then function returns a promise, promise will not directly pass the promise to the next then function, but will wait for the promise resolve, pass its resolve value to the next then function, find the then function part of our implemented code, and make the following modifications:

then(onFulfilled, onRejected) {
    const self = this
    if (this.status === 'pending') {
        return new MyPromise((resolve, reject) => {
        this.onFulfilledFunctions.push(() => {
            const thenReturn = onFulfilled(self.value)
            if (thenReturn instanceof MyPromise) {
                // When the return value is promise, when the promise state inside is reversed, the promise state outside is reversed synchronously
                thenReturn.then(resolve, reject)
            } else {
                resolve(thenReturn)
            }
        })
        this.onRejectedFunctions.push(() => {
            const thenReturn = onRejected(self.value)
            if (thenReturn instanceof MyPromise) {
                // When the return value is promise, when the promise state inside is reversed, the promise state outside is reversed synchronously
                thenReturn.then(resolve, reject)
            } else {
                resolve(thenReturn)
            }
        })
        })
    } else if (this.status === 'fulfilled') {
        return new MyPromise((resolve, reject) => {
            const thenReturn = onFulfilled(self.value)
            if (thenReturn instanceof MyPromise) {
                // When the return value is promise, when the promise state inside is reversed, the promise state outside is reversed synchronously
                thenReturn.then(resolve, reject)
            } else {
                resolve(thenReturn)
            }
        })
    } else {
        return new MyPromise((resolve, reject) => {
            const thenReturn = onRejected(self.value)
            if (thenReturn instanceof MyPromise) {
                // When the return value is promise, when the promise state inside is reversed, the promise state outside is reversed synchronously
                thenReturn.then(resolve, reject)
            } else {
                resolve(thenReturn)
            }
        })
    }
}

(4) the previous promise implementation code still lacks a lot of detailed logic. A relatively complete version will be provided below. The annotation part is the added code and provides an explanation.

class MyPromise {
  constructor(executor) {
    this.executor = executor
    this.value = null
    this.status = 'pending'
    this.onFulfilledFunctions = []
    this.onRejectedFunctions = []
    const resolve = value => {
      if (this.status === 'pending') {
        this.value = value
        this.status = 'fulfilled'
        this.onFulfilledFunctions.forEach(onFulfilled => {
          onFulfilled()
        })
      }
    }
    const reject = value => {
      if (this.status === 'pending') {
        this.value = value
        this.status = 'rejected'
        this.onRejectedFunctions.forEach(onRejected => {
          onRejected()
        })
      }
    }
    this.executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    const self = this
    if (typeof onFulfilled !== 'function') {
      // Compatible with unfulfilled function
      onFulfilled = function() {}
    }
    if (typeof onRejected !== 'function') {
      // Compatible with the case of unrejected function
      onRejected = function() {}
    }
    if (this.status === 'pending') {
      return new MyPromise((resolve, reject) => {
        this.onFulfilledFunctions.push(() => {
          try {
            const thenReturn = onFulfilled(self.value)
            if (thenReturn instanceof MyPromise) {
              thenReturn.then(resolve, reject)
            } else {
              resolve(thenReturn)
            }
          } catch (err) {
            // Error in catch execution
            reject(err)
          }
        })
        this.onRejectedFunctions.push(() => {
          try {
            const thenReturn = onRejected(self.value)
            if (thenReturn instanceof MyPromise) {
              thenReturn.then(resolve, reject)
            } else {
              resolve(thenReturn)
            }
          } catch (err) {
            // Error in catch execution
            reject(err)
          }
        })
      })
    } else if (this.status === 'fulfilled') {
      return new MyPromise((resolve, reject) => {
        try {
          const thenReturn = onFulfilled(self.value)
          if (thenReturn instanceof MyPromise) {
            thenReturn.then(resolve, reject)
          } else {
            resolve(thenReturn)
          }
        } catch (err) {
          // Error in catch execution
          reject(err)
        }
      })
    } else {
      return new MyPromise((resolve, reject) => {
        try {
          const thenReturn = onRejected(self.value)
          if (thenReturn instanceof MyPromise) {
            thenReturn.then(resolve, reject)
          } else {
            resolve(thenReturn)
          }
        } catch (err) {
          // Error in catch execution
          reject(err)
        }
      })
    }
  }
}

(5) so far, a relatively complete promise has been implemented, but he still has some problems. Students who know about macro tasks and micro tasks must know that the then function of promise is actually to register a micro task, and the parameter functions in the then function will not be executed synchronously.
Check the following code:

new Promise((resolve,reject)=>{
    console.log(`promise inside`)
    resolve()
}).then((res)=>{
    console.log(`First then`)
})
console.log(`promise external`)

/**
 * The output results are as follows:
 * promise inside
 * promise external
 * First then
 */

// But if you use the MyPromise we wrote to execute the above program

new MyPromise((resolve,reject)=>{
    console.log(`promise inside`)
    resolve()
}).then((res)=>{
    console.log(`First then`)
})
console.log(`promise external`)
/**
 * The output results are as follows:
 * promise inside
 * First then
 * promise external
 */

The above reason is that our then's onFulfilled and onRejected are executed synchronously. When the state of the last project has been changed to fully when the function then is executed, onFulfilled and onRejected will be executed immediately.
To solve this problem is also very simple, put the execution of onFulfilled and onRejected in the next event loop.

if (this.status === 'fulfilled') {
  return new MyPromise((resolve, reject) => {
    setTimeout(() => {
      try {
        const thenReturn = onFulfilled(self.value)
        if (thenReturn instanceof MyPromise) {
          thenReturn.then(resolve, reject)
        } else {
          resolve(thenReturn)
        }
      } catch (err) {
        // Error in catch execution
        reject(err)
      }
    })
  }, 0)
}

As for the explanation of macro tasks and micro tasks, I have seen a great article on nuggets. It uses the example of bank counter to explain why there are two queues of macro tasks and micro tasks at the same time. You can have a look at the link pasted at the end of the article.

5. Interpretation of promise / A + scheme

All the logic we implement above is based on Promise/A+ For specification implementation, most of the content of Promise/A + specification has been explained in the implementation process of promise one by one. The following is a summary:

  1. Project has three states: pending, fulfilled, and rejected, which can only be changed from pending to fulfilled and rejected.
  2. Project needs to provide a then method that takes (onFulfilled,onRejected) two functions as parameters.
  3. onFulfilled and onRejected must be called after the completion of promise, and can only be called once.
  4. onFulfilled and onRejected are only called as functions, and this cannot point to the project that calls it.
  5. onFulfilled and onRejected can only be executed after the execution context stack contains only platform code. Platform code refers to engine, environment and Promise implementation code. (PS: this specification requires that the execution of onFulfilled and onRejected functions must follow the event loop called by then. However, the specification does not require them to be executed as a micro task or macro task, but the implementation of each platform puts the onFulfilled and onRejected of Promise into the micro task queue for execution.)
  6. onFulfilled, onRejected must be a function, otherwise ignored.
  7. The then method can be called multiple times by a promise.
  8. The then method needs to return a promise.
  9. The resolution process of promise is an abstract operation. Taking promise and a value as input, we express them as [[Resolve]](promise,x), [[Resolve]](promise,x) is to create a Resolve method and pass in two parameters of promise,x (the value returned when promise succeeds). If x is a tenable object (containing then method), and assume that x behaves like P Raise, [[Resolve]](promise,x) will create a promise with the state of X, otherwise, [[Resolve]](promise,x) will use X to reverse the state of promise. Different ways of implementing promise to get input can interact as long as they all expose Promise/A + compatible methods. It also allows promise to use reasonable then methods to assimilate some irregular promise implementations.

Point 9 only looks at the document, which is rather obscure. In fact, it is a normative explanation for this line of code in our then method.

return new MyPromise((resolve, reject) => {
  try {
    const thenReturn = onFulfilled(self.value)
    if (thenReturn instanceof MyPromise) {
      // 👈 this line of code
      thenReturn.then(resolve, reject)
    } else {
      resolve(thenReturn)
    }
  } catch (err) {
    reject(err)
  }
})

Because promise is not a JS standard at the beginning, but a method independently implemented by many third parties, it is impossible to judge whether the return value is a promise object through instanceof. Therefore, in order to make different promises interact, the 9th specification I mentioned above comes into being. When the return value thenReturn is a promise object, we need to wait for the promise state to reverse and use its return value to resolve the outer promise.

So finally, we need to implement [[Resolve]](promise,x) to meet the promise specification, as shown below.


/**
 * resolvePromise Function is a function that determines the state of promise 2 according to the value of x
 * @param {Promise} promise2  then The promise object that the function needs to return
 * @param {any} x onResolve || onReject Return value after execution
 * @param {Function} resolve  MyPromise resolve method in
 * @param {Function} reject  MyPromise reject method in
 */
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    // 2.3.1 promise 2 and x point to the same object
    reject(new TypeError())
    return
  }

  if (x instanceof MyPromise) {
    // 2.3.2 x is an instance of MyPromise, adopting its state
    if (x.status === 'pending') {
      x.then(
        value => {
          resolvePromise(promise2, value, resolve, reject)
        },
        err => {
          reject(err)
        }
      )
    } else {
      x.then(resolve, reject)
    }
    return
  }

  if (x && (typeof x === 'function' || typeof x === 'object')) {
    // 2.3.3 x is an object or function
    try {
      const then = x.then // 2.3.3.1 declare the variable then = x.then
      let promiseStatusConfirmed = false // Determination of promise status
      if (typeof then === 'function') {
        // 2.3.3.3 then is a method that binds x to this in the then function and calls
        then.call(
          x,
          value => {
            // 2.3.3.3.1 if the then function returns the value, [[Resolve]](promise, value) is used to monitor whether value is also a tenable object
            if (promiseStatusConfirmed) return // 2.3.3.3.3 i.e. the results of the three places shall prevail
            promiseStatusConfirmed = true
            resolvePromise(promise2, value, resolve, reject)
            return
          },
          err => {
            // 2.3.3.3.2 the then function throws err and uses err reject to reject the current project
            if (promiseStatusConfirmed) return // 2.3.3.3.3 i.e. the results of the three places shall prevail
            promiseStatusConfirmed = true
            reject(err)
            return
          }
        )
      } else {
        // 2.3.3.4 then is not a method, then use x to twist promise state to be fulfilled
        resolve(x)
      }
    } catch (e) {
      // 2.3.3.2 if the error E is thrown when getting the result of x.then, use e reject to reject the current project
      if (promiseStatusConfirmed) return // 2.3.3.3.3 i.e. the results of the three places shall prevail
      promiseStatusConfirmed = true
      reject(e)
      return
    }
  } else {
    resolve(x) // 2.3.4 if x is not an object function, use X to turn the promise state to full
  }
}

Then we can replace the previous part of the code with the resolcePromise method

return new MyPromise((resolve, reject) => {
  try {
    const thenReturn = onFulfilled(self.value)
    if (thenReturn instanceof MyPromise) {
      thenReturn.then(resolve, reject)
    } else {
      resolve(thenReturn)
    }
  } catch (err) {
    reject(err)
  }
})

// It's like this 

return new MyPromise((resolve, reject) => {
  try {
    const thenReturn = onFulfilled(self.value)
    resolvePromise(resolve,reject)
  } catch (err) {
    reject(err)
  }
})

This article is not to implement a complete promise, but through the trial implementation of promise, we have a deeper understanding of promise. Such implementation process can help developers better use promise in the development process.

Related reference
Promise/A + specification
Implement a full version of Promise step by step from zero
Micro task, macro task and event loop

Posted by lional on Fri, 06 Dec 2019 16:48:50 -0800