Promise implementation is not as simple or as difficult as it might seem, and it takes less than 200 lines of code to implement an alternative to the original Promise.

Promises have been an integral API on the front end and are now ubiquitous. Are you sure you know Promise well enough? If not, you should know how Promise works. If you think you know it, have you ever fulfilled your Promise?

In any case, understanding how promises are implemented can help improve our front-end skills.

The following code is implemented using ES6 syntax, is not COMPATIBLE with ES5, and runs fine on the latest Google Browser.

If you want to see the results first, check out the full version at the end of this article, or check out Github, which includes unit tests.

Part of the term

  • executor

    new Promise( function(resolve, reject) {... } /* executor */ );Copy the code

    Executor is a function that takes resolve and reject.

  • onFulfilled

    p.then(onFulfilled, onRejected);Copy the code

    This parameter is called as a then callback function when a Promise becomes fulfillment.

  • OnRejected This parameter is called as a callback when a Promise turns into a rejection.

    p.then(onFulfilled, onRejected);
    p.catch(onRejected);Copy the code

Promise implementation Principle

Better to look at Promises/A+ specifications first, here is A personal summary of the fundamentals of code implementation.

Three states of Promise

  • pending
  • fulfilled
  • rejected

The Promise object’s pending may become a pity and Rejected, but it cannot be reversed.

The then and catch callback methods can only be executed in a non-pending state.

Promise lifecycle

In order to better understand, I summarized the life cycle of Promise. The life cycle is divided into two situations and the life cycle is irreversible.

pending -> fulfilld

pending -> rejected

Executor, then, Catch, and finally executions each have a new life cycle.

Principle of the chain

How do you keep chain calls to.then,.catch, and.finally?

Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ Each THEN, catch, and finally has its own Promise life cycle.

This will be a pity. Prototype. Then = function(ondepressing,onReject) {return Promise(() => {// this will be a pity.Copy the code

However, we need to consider the case that the link is broken midway. If the link continues after the break, the state of the Promise may be non-pending.

This point is not so easy to understand when first exposed to.

The default is to start with new Promise(…) .then(…) In chain calls, then, catch, and other callback functions are all in pending state, and the callback functions are added to the asynchronous queue waiting for execution. The pending state may become a pity or Rejected state, which needs to be executed immediately instead of joining the asynchronous queue waiting for execution.

Examples of continuous chains are as follows:

new Promise(...) .then(...)Copy the code

Examples of broken links are as follows:

const a = new Promise(...) // This may be a pity or the rejected state (...). // The pending state may become a pity or the rejected state.Copy the code

So you have to think about both cases.

Asynchronous lined up

This requires an understanding of macro tasks and microtasks, but not all browser JavaScript apis provide such methods.

So I’m going to use setTimeout instead.

While non-asynchronous resolve or Reject can be implemented without asynchronous queuing, the native Promise all then callbacks are executed in asynchronous queuing.

Note here that asynchronous queuing is not used in the first part of the step-by-step examples, but is added to resolve or reject asynchronously.

The first step is to define the structure

To make it different from the original Promise, we prefix it to NPromise.

Define state

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejectedCopy the code

Define the Promise instance method

class NPromise {
  constructor(executor) {}
  then(onFulfilled, onRejected) {}
  catch(onRejected) {}
  finally(onFinally) {}
}Copy the code

Define extension methods

NPromise.resolve = function(value){}
NPromise.reject = function(resaon){}
NPromise.all = function(values){}
NPromise.race = function(values){}Copy the code

A simple Promise

The first simple Promsie does not take into account cases such as asynchronous resolve, which is only used like, and the callback to then is not asynchronous.

const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' /** * @param {Function} executor * Executor is a function that takes resolve and reject * the Promise constructor calls the executor function immediately upon execution */ class NPromise {constructor(executor) {// There are some attribute variables that you don't want to define, but just to do that, This._status = pending // Ondepressing parameter this._nextValue = undefined // Error cause this._error = undefined executor(this._onFulfilled, This. _onFulfilled)} /** * @param {Any} value = (value) => {if (this._status === == this _error = undefined}} /** * The operation fails * @param {Any} reason Value */ _onRejected = (reason) => {if (this._status === PENDING) {this._status = REJECTED this._error = reason this._nextValue = undefined } } then(onFulfilled, onRejected) { return new NPromise((resolve, reject) => { if (this._status === FULFILLED) { if (onFulfilled) { const _value = onFulfilled ? onFulfilled(this._nextValue) : This._nextvalue // If ondepressing is defined, run ondepressing and return result // otherwise skip, Resolve (_value)}} if (this._status === REJECTED) {if (onRejected) {this. Resolve (onRejected(this._error))} else {// This is a pity. The Promise will return the rejected state, reject(this._error)}})} catch(onRejected) {// This is a pity This.then (null, onRejected)} finally(onRejected) {this.then(null, onRejected)} finally(onRejected) {Copy the code

Test example (setTimeout only to provide a separate execution environment)

setTimeout(() => { new NPromise((resolve) => { console.log('resolved:') resolve(1) }) .then((value) => { console.log(value) return 2 }) .then((value) => { console.log(value) }) }) setTimeout(() => { new NPromise((_, Reject) => {console.log('reject :') reject('err')}). Then ((value) => {console.log(value) return 2}) . The catch ((err) = > {the console. The log (err)})}) / / / / output resolved: / / 1 / / 2 / / rejected: / / errCopy the code

Consider catching errors

Resolve (resolve); then (resolve);

Change points relative to previous step:

  • Executor execution requires a try catch

    Then, catch, and finally are all executed by executor, so you only need to try catch executor.

  • No promise. Catch is defined and an error message is printed

    It’s not a throw, it’s a console.error, which is also native, so you can’t catch a Promise error by trying it.

  • If the callback function for then is not a function type, skip it

    This is outside the scope of error capture processing, which is mentioned here by the way.

Code implementation

const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' function isFunction(fn) { return Typeof fn === 'function'} /** * @param {function} executor * executor is a function with resolve and reject arguments * Promise constructor is called immediately upon execution Executor function */ class NPromise {constructor(executor) {constructor(executor) { This._status = pending // Ondepressing parameter this._nextValue = undefined // Error cause this._error = undefined executor(this._onFulfilled, This._onRejected)} Catch (err) {this._onRejected(err)}} /** * */ _throwErrorIfNotCatch() {setTimeout(() => {// setTimeout is required until the execution is complete, Finally, check if this._error also defines if (this._error! == undefined) {console.error('Uncaught (in promise)', This._error)}})} /** * this._error)}} @param {Any} value this = (value) => {this PENDING) { this._status = FULFILLED this._nextValue = value this._error = undefined this._throwErrorIfNotCatch() } } /** * Failed * @param {Any} Reason Failed value */ _onRejected = (reason) => {if (this._status === PENDING) {this._status = REJECTED this._error = reason this._nextValue = undefined this._throwErrorIfNotCatch() } } then(onFulfilled, onRejected) { return new NPromise((resolve, reject) => { const handle = (reason) => { function handleResolve(value) { const _value = isFunction(onFulfilled) ? onFulfilled(value) : value resolve(_value) } function handleReject(err) { if (isFunction(onRejected)) { resolve(onRejected(err)) } else { reject(err) } } if (this._status === FULFILLED) { return handleResolve(this._nextValue) } if (this._status === REJECTED) {return handleReject(reason)}} handle(this._error) // Error has been passed to the next NPromise and needs to be reset. // This._throwErrorIfNotCatch is used together with this._throwErrorIfNotCatch. This._error = undefined})} Catch (onRejected) {// This. Then (null, someday) Onrejecte)} finally(onRejecte) {}Copy the code

Test example (setTimeout only to provide a separate execution environment)

SetTimeout (() => {new NPromise((resolve) => {console.log('executor error :') const a = 2 a = 3 resolve(1)}). Catch ((value)  => { console.log(value) return 2 }) }) setTimeout(() => { new NPromise((resolve) => { resolve() }) .then(() => { const B = 3 b = 4 return 2}). Catch ((err) => {console.log(' console.log ') ') console.log(err)})}) setTimeout(() => {new NPromise((resolve) => {console.log(' print error message directly, red: ') resolve()}). Then (() => {throw Error('test') return 2})}) Assignment to constant variable. // at <anonymous>:97:7 // at new NPromise (<anonymous>:21:7) // at <anonymous>:94:3 // TypeError: Assignment to constant variable. // at <anonymous>:111:9 // at <anonymous>:59:17 // at new Promise (<anonymous>) // at Npromise. then (<anonymous>:54:12) // at <anonymous>:109:6 test // at <anonymous>:148:11 // at handleResolve (<anonymous>:76:52) // at handle (<anonymous>:89:18) // at <anonymous>:96:7 // at new NPromise (<anonymous>:25:7) // at NPromise.then (<anonymous>:73:12) // at <anonymous>:147:6Copy the code

The value considered resolved is of type Promise

Resolve (resolve); then (resolve);

Change points relative to previous step:

  • New isPromise method
  • The then method handles the case where this._nextValue is a Promise

Code implementation

const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' function isFunction(fn) { return typeof fn === 'function' } function isPromise(value) { return ( value && isFunction(value.then) && IsFunction (value.catch) &&isfunction (value.finally) // Not in the following way. / / the value then instanceof NPromise | |)} / * * * @ param {Function} executor * executor is with resolve and reject // Class NPromise {constructor(executor) {// Some attribute variables may not be defined. This._status = pending // Ondepressing parameter this._nextValue = undefined // Error cause this._error = undefined executor(this._onFulfilled, This._onRejected)} Catch (err) {this._onRejected(err)}} /** * */ _throwErrorIfNotCatch() {setTimeout(() => {// setTimeout is required until the execution is complete, Finally, check if this._error also defines if (this._error! == undefined) {console.error('Uncaught (in promise)', This._error)}})} /** * this._error)}} @param {Any} value this = (value) => {this PENDING) { this._status = FULFILLED this._nextValue = value this._error = undefined this._throwErrorIfNotCatch() } } /** * Failed * @param {Any} Reason Failed value */ _onRejected = (reason) => {if (this._status === PENDING) {this._status = REJECTED this._error = reason this._nextValue = undefined this._throwErrorIfNotCatch() } } then(onFulfilled, onRejected) { return new NPromise((resolve, reject) => { const handle = (reason) => { function handleResolve(value) { const _value = isFunction(onFulfilled) ? onFulfilled(value) : value resolve(_value) } function handleReject(err) { if (isFunction(onRejected)) { resolve(onRejected(err)) } else { reject(err) } } if (this._status === FULFILLED) { if (isPromise(this._nextValue)) { return this._nextValue.then(handleResolve, handleReject) } else { return handleResolve(this._nextValue) } } if (this._status === REJECTED) { return HandleReject (reason)}} handle(this._error) // Error was passed to the next NPromise and needs to be reset, // This._throwErrorIfNotCatch is used together with this._throwErrorIfNotCatch. This._error = undefined})} Catch (onRejected) {// This. Then (null, someday) Onrejecte)} finally(onRejecte) {}Copy the code

The test code

setTimeout(() => { new NPromise((resolve) => { resolve( new NPromise((_resolve) => { _resolve(1) }) ) }).then((value) =>  { console.log(value) }) }) setTimeout(() => { new NPromise((resolve) => { resolve( new NPromise((_, _reject) => { _reject('err') }) ) }).catch((err) => { console.log(err) }) })Copy the code

Consider the asynchronous case

Async Resolve or reject, which are more complex, need to be placed into a queue and wait for the pending state to change.

Native JavaScript comes with asynchronous queuing, and we can take advantage of that by using setTimeout instead, So this Promise has the same level of priority as setTimeout (natively, the Promise takes precedence over setTimeout).

Change points relative to previous step:

  • This. _ondepressing is fulfilled with asynchronous processing
  • This. _onRejected adds asynchronous processing
  • Add this._callbackQueue with an empty array as the initial value
  • Added the this.__runCallbackQueue method to run an asynchronous queue

    During synchronization, the Promise state immediately switches to a non-pending state and the asynchronous queue becomes null. Before asynchronously changing the Promise state, a value is pending, and then, catch, and finally callbacks are queued for execution.

    Whether asynchronous or synchronous, this.__runCallbackQueue is executed only once in the current Promise life cycle.

  • The then method needs to discriminate according to the Promise state

    If the state is not pending, execute the callback function immediately (skip if there is no callback function).

    If the state is pending, join an asynchronous queue and wait for the promises to be non-pending before they are executed.

  • The callback to the then method adds try catch error handling
  • Finally methods are implemented

Code implementation

const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' function isFunction(fn) { return typeof fn === 'function' } function isPromise(value) { return ( value && isFunction(value.then) && IsFunction (value.catch) &&isfunction (value.finally) // Not in the following way. / / the value then instanceof NPromise | |)} / * * * @ param {Function} executor * executor is with resolve and reject } / class NPromise {constructor(executor) {constructor(executor) {if (! IsFunction (executor)) {throw new TypeError('Expected the executor to be a function.')} try {// Initialize status to PENDING This. _status = PENDING // fullfilled This._error = undefined // then, catch, finally asynchronous callback queue, This._callbacQueue = [] executor(this. _ondepressing, This._onRejected)} Catch (err) {this._onRejected(err)}} /** * */ _throwErrorIfNotCatch() {setTimeout(() => {// setTimeout is required until the execution is complete, Finally, check if this._error also defines if (this._error! == undefined) {console.error('Uncaught (in promise)', This._error)}})} _runCallbackQueue = () => {if (this._callbacqueue.length > 0) {// resolve or reject ForEach (fn => {fn()}) this._callbacQueue = []} This._throwErrorIfnotCatch ()} /** * this._throwerrorIfnotcatch ()} /** * this._throwerrorIfnotcatch ()} /** * this._throwerrorIfnotcatch ()} if (this._status === PENDING) { this._status = FULFILLED this._nextValue = value this._error = undefined This._runcallbackqueue ()}} @param {Any} reason = / _onRejected = reason => {setTimeout(() => {setTimeout() => { if (this._status === PENDING) { this._status = REJECTED this._error = reason this._nextValue = undefined this._runCallbackQueue() } }) } then(onFulfilled, onRejected) { return new NPromise((resolve, reject) => { const handle = reason => { try { function handleResolve(value) { const _value = isFunction(onFulfilled) ? onFulfilled(value) : value resolve(_value) } function handleReject(err) { if (isFunction(onRejected)) { resolve(onRejected(err)) } else { reject(err) } } if (this._status === FULFILLED) { if (isPromise(this._nextValue)) { return this._nextValue.then(handleResolve, handleReject) } else { return handleResolve(this._nextValue) } } if (this._status === REJECTED) { return HandleReject (reason)}} Catch (err) {reject(err)}} if (this._status === PENDING) { // New NPromise(...); .then(...) Var a = NPromise(...) ; a.then(...) This._callbacqueue.push (() => {// Use setTimeout instead of // to ensure that the state is not pending before execution of subsequent THEN, catch, or Finally callback handle(this._error) // Error has been passed to the next NPromise and needs to be reset otherwise it will throw multiple identical errors // With this._throwErrorIfnotCatch, If there is no catch this._error = undefined})} else {// if there is no catch this._error = undefined})} else {// Var a = NPromise(...); ; // after a few seconds a.chen (...) // Handle (this._error) // Error has been passed to the next NPromise and needs to be reset, // This._throwErrorIfNotCatch is used together with this._throwErrorIfNotCatch. This._error = undefined}})} Catch (onRejected) {return this. Then (null, onRejected) } finally(onFinally) { return this.then( () => { onFinally() return this._nextValue }, () => {onFinally() // The error needs to be thrown, the next Promise will catch throw this._error})}}Copy the code

The test code

new NPromise((resolve) => {
  setTimeout(() => {
    resolve(1)
  }, 1000)
})
  .then((value) => {
    console.log(value)
    return new NPromise((resolve) => {
      setTimeout(() => {
        resolve(2)
      }, 1000)
    })
  })
  .then((value) => {
    console.log(value)
  })Copy the code

Expanding method

The relatively difficult extension method should be Promise. All, the rest are quite simple.

NPromise.resolve = function(value) {
  return new NPromise(resolve => {
    resolve(value)
  })
}

NPromise.reject = function(reason) {
  return new NPromise((_, reject) => {
    reject(reason)
  })
}

NPromise.all = function(values) {
  return new NPromise((resolve, reject) => {
    let ret = {}
    let isError = false
    values.forEach((p, index) => {
      if (isError) {
        return
      }
      NPromise.resolve(p)
        .then(value => {
          ret[index] = value
          const result = Object.values(ret)
          if (values.length === result.length) {
            resolve(result)
          }
        })
        .catch(err => {
          isError = true
          reject(err)
        })
    })
  })
}

NPromise.race = function(values) {
  return new NPromise(function(resolve, reject) {
    values.forEach(function(value) {
      NPromise.resolve(value).then(resolve, reject)
    })
  })
}Copy the code

The final version

You can also look at Github, which has unit tests.

const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' function isFunction(fn) { return typeof fn === 'function' } function isPromise(value) { return ( value && isFunction(value.then) && IsFunction (value.catch) &&isfunction (value.finally) // Not in the following way. / / the value then instanceof NPromise | |)} / * * * @ param {Function} executor * executor is with resolve and reject } / class NPromise {constructor(executor) {constructor(executor) {if (! IsFunction (executor)) {throw new TypeError('Expected the executor to be a function.')} try {// Initialize status to PENDING This. _status = PENDING // fullfilled This._error = undefined // then, catch, finally asynchronous callback queue, This._callbacQueue = [] executor(this. _ondepressing, This._onRejected)} Catch (err) {this._onRejected(err)}} /** * */ _throwErrorIfNotCatch() {setTimeout(() => {// setTimeout is required until the execution is complete, Finally, check if this._error also defines if (this._error! == undefined) {console.error('Uncaught (in promise)', This._error)}})} _runCallbackQueue = () => {if (this._callbacqueue.length > 0) {// resolve or reject ForEach (fn => {fn()}) this._callbacQueue = []} This._throwErrorIfnotCatch ()} /** * this._throwerrorIfnotcatch ()} /** * this._throwerrorIfnotcatch ()} /** * this._throwerrorIfnotcatch ()} if (this._status === PENDING) { this._status = FULFILLED this._nextValue = value this._error = undefined This._runcallbackqueue ()}} @param {Any} reason = / _onRejected = reason => {setTimeout(() => {setTimeout() => { if (this._status === PENDING) { this._status = REJECTED this._error = reason this._nextValue = undefined this._runCallbackQueue() } }) } then(onFulfilled, onRejected) { return new NPromise((resolve, reject) => { const handle = reason => { try { function handleResolve(value) { const _value = isFunction(onFulfilled) ? onFulfilled(value) : value resolve(_value) } function handleReject(err) { if (isFunction(onRejected)) { resolve(onRejected(err)) } else { reject(err) } } if (this._status === FULFILLED) { if (isPromise(this._nextValue)) { return this._nextValue.then(handleResolve, handleReject) } else { return handleResolve(this._nextValue) } } if (this._status === REJECTED) { return HandleReject (reason)}} Catch (err) {reject(err)}} if (this._status === PENDING) { // New NPromise(...); .then(...) Var a = NPromise(...) ; a.then(...) This._callbacqueue.push (() => {// Use setTimeout instead of // to ensure that the state is not pending before execution of subsequent THEN, catch, or Finally callback handle(this._error) // Error has been passed to the next NPromise and needs to be reset otherwise it will throw multiple identical errors // With this._throwErrorIfnotCatch, If there is no catch this._error = undefined})} else {// if there is no catch this._error = undefined})} else {// Var a = NPromise(...); ; // after a few seconds a.chen (...) // Handle (this._error) // Error has been passed to the next NPromise and needs to be reset, // This._throwErrorIfNotCatch is used together with this._throwErrorIfNotCatch. This._error = undefined}})} Catch (onRejected) {return this. Then (null, onRejected) } finally(onFinally) { return this.then( () => { onFinally() return this._nextValue }, () => {onFinally() // Error needs to be thrown, Throw this._error})}} npromise. resolve = function(value) {return new NPromise(resolve => { resolve(value) }) } NPromise.reject = function(reason) { return new NPromise((_, reject) => { reject(reason) }) } NPromise.all = function(values) { return new NPromise((resolve, reject) => { let ret = {} let isError = false values.forEach((p, index) => { if (isError) { return } NPromise.resolve(p) .then(value => { ret[index] = value const result = Object.values(ret) if (values.length === result.length) { resolve(result) } }) .catch(err => { isError = true reject(err) }) }) }) } NPromise.race = function(values) { return new NPromise(function(resolve, reject) { values.forEach(function(value) { NPromise.resolve(value).then(resolve, reject) }) }) }Copy the code

conclusion

After some testing, except for the following two points:

  • Use setTimeout macro task queuing instead of microtasks
  • The extension methods promise. all and promise. race only consider arrays, not iterators.

Promise alone is almost 100% the same in use and effect as native.

If you don’t believe me, check out the unit tests on Github while you try the following code:

Notice NPromise and Promise.

new NPromise((resolve) => {
  resolve(Promise.resolve(2))
}).then((value) => {
  console.log(value)
})Copy the code

or

new Promise((resolve) => {
  resolve(NPromise.resolve(2))
}).then((value) => {
  console.log(value)
})Copy the code

All of the above results are returned to normal.