Promise is a commonly used syntax in development. Basically, asynchronous processing is mostly completed through Promise. There are many Promise specifications, and ES6 ultimately uses the Promise/A+ specification, so the following code is written largely based on that specification.

Let’s start by enumerating all of Promise’s instance methods and static methods

Instance methods

  • then: new Promise((resolve, reject) => {... }). Then (() = > {the console. The log (' rsolve success callback ')}, () = > {the console. The log (' failed to reject the callback ')})
  • catch: new Promise((resolve, reject) => {... }).catch(() => {console.log('reject ')})
  • finally: new Promise((resolve, reject) => {... }).finally(() => {console.log(' enter both success and failure ')})
  • All of the above method calls will return newPromise

A static method

  • resolve: Promise.resolve(value)returnPromiseThe instance
  • reject: Promise.reject(value)returnPromiseThe instance
  • all: Promise.all(promises): passed in array formatPromiseAnd return a newPromiseFor instance, success returns the values in order, and one failure becomes a failure
  • race: Promise.race(promises): passed in array formatPromiseAnd return a newPromiseFor example, success or failure depends on the way the first one is done

Once the Promise state is confirmed to be changed, it cannot be changed again. It has the following three states: Pending, depressing and Rejected Promise are implemented in the micro-task queue in the browser and need to process the micro-task (Event Loop mechanism in JavaScript).

1. Declare the instance method of the Promise

class Promise {
  _value
  _state = 'pending'
  _queue = []
  constructor(fn) {
    if (typeoffn ! = ='function') {
      throw 'Promise resolver undefined is not a function'
    }
    /* new Promise((resolve, reject) => {resolve: reject: fail}) */
    fn(this._resolve.bind(this), this._reject.bind(this))}// Take 1-2 arguments, the first for a successful callback and the second for a failed callback
  then(onFulfilled, onRejected) {
    // It might already be resolved, because promises can be resolved early and then registered after the then method
    if (this._state === 'fulfilled') { onFulfilled? . (this._value)
      return
    }
    / / reject again
    if (this._state === 'rejected') { onRejected? . (this._value)
      return
    }
    // The Promise isn't finished yet, so push it to a queue. When it's finished, execute the corresponding function in the queue
    this._queue.push({
      onFulfilled,
      onRejected,
    })
  }

  // Failed to receive callback
  catch(onRejected) {
    // This is equivalent to calling then directly and passing in a failed callback
    this.then(null, onRejected)
  }

  // Callback executed on both success and failure
  finally(onDone) {
    const fn = () = > onDone()
    this.then(fn, fn)
  }
  / / the resolve to success
  _resolve(value) {
    // When the state is fixed, it does not change anymore
    if (this._state ! = ='pending') return
    this._state = 'fulfilled'

    // Just store the value and fetch it when called again, because once a Promise is made, it won't change
    this._value = value

    // Execute the push function argument in the previous. Then method, so that the corresponding method is executed.
    this._queue.forEach((callback) = >{ callback.onFulfilled? . (this._value)
    })
  }

  / / reject failure
  _reject(error) {
    // When the state is fixed, it does not change anymore
    if (this._state ! = ='pending') return
    this._state = 'rejected'
    this._value = error
    this._queue.forEach((callback) = >{ callback.onRejected? . (this._value)
    })
  }
}
Copy the code

Call logic:

  1. OnFulfilled => then((onFulfilled, onFulfilled) => {… })

  2. Place the ondepressing function in the _queue collection in the then method. => this._queue.push({ onFulfilled, onRejected })

  3. Once the async callback is complete, resolve is executed, at which point the functions collected by _queue registered with the then method are called. Execute these functions in unison so that the asynchronous callback completes, executing the corresponding functions in the THEN method

// Print the result
const p = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('success')},1000)
})
p.then((res) = > {
  console.log(res) // => success
})

// reject
const p1 = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    reject('fail')},1000)
})
p1.catch((res) = > {
  console.log(res) // => fail
})

// finally
const p2 = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve()
  }, 1000)
})
p2.finally(() = > {
  console.log('done') // => done
})
Copy the code

Online code demo

2. Microtask and return promises

A. Perform microtasks

In the browser, promises are pushed to the microtask after completion, so we need to do that as well. Using MutationObserver in the browser, Node can use process.nexttick

class Promise {...// Push into microtasks
  _nextTick(fn) {
    if (typeofMutationObserver ! = ='undefined') { // The browser implements microtasks through MutationObserver
      // This block can be used separately to avoid unnecessary overhead, otherwise nodes need to be generated every time.
      const observer = new MutationObserver(fn)
      let count = 1
      const textNode = document.createTextNode(String(count))
      observer.observe(textNode, {
        characterData: true
      })
      textNode.data = String(++count)
    } else if (typeofprocess.nextTick ! = ='undefined') { // The node side uses process.nextTick
      process.nextTick(fn)
    } else {
      setTimeout(fn, 0)}}/ / the resolve to success
  _resolve(value) {
    // When the state is fixed, it does not change anymore
    if (this._state ! = ='pending') return
    // Push into microtasks
    this._nextTick(() = > {
      this._state = 'fulfilled'
      this._value = value
      this._queue.forEach((callback) = >{ callback.onFulfilled? . (this._value)
      })
    })
  }

  / / reject failure
  _reject(error) {
    // When the state is fixed, it does not change anymore
    if (this._state ! = ='pending') return
    // Push into microtasks
    this._nextTick(() = > {
      this._state = 'rejected'
      this._value = error
      this._queue.forEach((callback) = >{ callback.onRejected? . (this._value)
      })
    })
  }
  ...
}
Copy the code

Results demonstrate

B. Return the Promise to make the chain call

Typically promises handle multiple asynchronous requests, sometimes with interdependencies.

Such as:

const getUser = () = > {
  return new Promise((resolve, reject) = > {
    setTimeout(() = > {
      resolve({
        userId: '123'})},500)})}const getDataByUser = (userId) = > {
  return new Promise((resolve, reject) = > {
    setTimeout(() = > {
      / /...
      resolve({a: 1})},500)})}/ / use
getUser().then((user) = > {
  return getDataByUser(user.userId)
}).then((res) = > {
  console.log(res)// {a: 1}
})
Copy the code

GetDataByUser relies on getUser to request the user information back. Here we need to use the Promise chain call. Let’s change our code

class Promise {
  constructor(fn) {
    fn(this._resolve.bind(this), this._reject.bind(this))}...// 1. The then method needs to return a new Promise, because the chained call is required and the next THEN method accepts the value of the previous THEN method
  // 2. The returned Promise must be a new Promise, otherwise the state and result will be shared.
  // 3. Treat the return value from the previous THEN method as the value of the next Promise resolve
  then(onFulfilled, onRejected) {
    // Return a new Promise
    return new Promise((resolve, reject) = > {
      // It is possible that the Promise will already resolve, because it can be pre-resolved and registered after the then method, in which case the value can be returned directly to the function
      if (this._state === 'fulfilled' && onFulfilled) {
        this._nextTick(onFulfilled.bind(this.this._value))
        return
      }
      if (this._state === 'rejected' && onRejected) {
        this._nextTick(onRejected.bind(this.this._value))
        return
      }
      /* Associate the parameters of the current Promise's then method with the new Promise's resolve, reject. This is a big pity. This will be a big pity. This is a big pity that can associate the onFulfilled Promise with the resolve in the new Promise. Reject the same * /
      this._queue.push({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
    })
  }
  / / reject again
  _resolve(value) {
    // When the state is fixed, it does not change anymore
    if (this._state ! = ='pending') return

    // This example returns a Promise, not a Promise, so we need to do something special here.
    If it is a Promise object, we need to parse the Promise result and then pass the value to resolve
    if (typeof value === 'object' && typeof value.then === 'function') {
      // We can pass the current _resolve method, because as soon as the next Promise resolve is passed, the then parameter is executed and the value is passed.
      // Get the Promise value
      // this._resove => obj.onFulfilled? .(this._value)
      // this._reject => obj.onRejected? .(this._value)
      value.then(this._resolve.bind(this), this._reject.bind(this))
      return
    }

    // Push into microtasks
    this._nextTick(() = > {
      this._state = 'fulfilled'
      this._value = value
      this._queue.forEach((obj) = > {
        // Accept ondepressing return value
        constval = obj.onFulfilled? . (this._value)
        Then ((res) => {consolle.log(res)})
        Resolve is the resolve function of the new Promise, which passes the return value from the then method to the next Promise
        obj.resolve(val)
      })
    })
  }
  ...
}

Copy the code

Results demonstrate

Call logic:

  1. Microtasks are implemented using MutationObserver and process.nextTick

  2. The Promise is called chained, by associating the (onFulfilled, onRejected) parameter in the THEN method with the (resolve, reject) parameter in the newly returned Promise.

  3. Once the previous Promise is fulfilled, the ondepressing function is called, and the value returned in the ondepressing function can be put into the new Promise’s resolve.

  4. If you encounter a resolve value that is a Promise object, parse recursively and return the value

The complete code

class Promise {
  _value
  _state = 'pending'
  _queue = []
  constructor(fn) {
    if (typeoffn ! = ='function') {
      throw new Error('Promise resolver undefined is not a function')}/* new Promise((resolve, reject) => {resolve: reject: fail}) */
    fn(this._resolve.bind(this), this._reject.bind(this))}// Take 1-2 arguments, the first for a successful callback and the second for a failed callback
  then(onFulfilled, onRejected) {
    // Return a new Promise
    return new Promise((resolve, reject) = > {
      // It is possible that the Promise will already resolve, because it can be pre-resolved and registered after the then method, in which case the value can be returned directly to the function
      if (this._state === 'fulfilled' && onFulfilled) {
        this._nextTick(onFulfilled.bind(this.this._value))
        return
      }
      if (this._state === 'rejected' && onRejected) {
        this._nextTick(onRejected.bind(this.this._value))
        return
      }
      // Associate the parameters of the current Promise's then method with the new Promise's resolve, reject
      this._queue.push({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
    })
  }

  // Failed to receive callback
  catch(onRejected) {
    return this.then(null, onRejected)
  }

  // Callback executed on both success and failure
  finally(onDone) {
    return this.then((value) = > {
      onDone()
      return value
    }, (value) = > {
      // console.log(value)
      onDone()
      throw value
    })
  }

  // Push into microtasks
  _nextTick(fn) {
    if (typeofMutationObserver ! = ='undefined') { / / the browser
      // This block can be used separately to avoid unnecessary overhead, otherwise nodes need to be generated every time.
      const observer = new MutationObserver(fn)
      let count = 1
      const textNode = document.createTextNode(String(count))
      observer.observe(textNode, {
        characterData: true
      })
      textNode.data = String(++count)
    } else if (typeofprocess.nextTick ! = ='undefined') { // node
      process.nextTick(fn)
    } else {
      setTimeout(fn, 0)}}/ / the resolve to success
  _resolve(value) {
    // When the state is fixed, it does not change anymore
    if (this._state ! = ='pending') return

    // This example returns a Promise, not a Promise, so we need to do something special here.
    If resolve() is a Promise object, we need to resolve the Promise result and pass it to resolve
    if (typeof value === 'object' && typeof value.then === 'function') {
      // We can pass the current _resolve method, because as soon as the next Promise resolve is passed, the then parameter is executed and the value is passed.
      // Get the Promise value
      // this._resove => obj.onFulfilled? .(this._value)
      // this._reject => obj.onRejected? .(this._value)
      value.then(this._resolve.bind(this), this._reject.bind(this))
      return
    }

    // Push into microtasks
    this._nextTick(() = > {
      this._state = 'fulfilled'
      this._value = value
      this._queue.forEach((obj) = > {
        // Use try catch to catch onFulfilled internal errors
        try {
          // Accept onFulfilled return value. If there is none, pass this._value downward
          const val = obj.onFulfilled ? obj.onFulfilled(this._value) : this._value
          Then ((res) => {consolle.log(res)})
          Resolve is the resolve function of the new Promise, which passes the return value from the then method to the next Promise
          obj.resolve(val)
        } catch (e) {
          obj.reject(e)
        }
      })
    })
  }

  / / reject failure
  _reject(error) {
    if (this._state ! = ='pending') return
    this._nextTick(() = > {
      this._state = 'rejected'
      this._value = error
      this._queue.forEach((obj) = > {
        try {
          const val = obj.onRejected ? obj.onRejected(this._value) : this._value
          // Reject returns a new Promise after the current reject executes, which should be resolved properly, so resolve should be used instead of continuing with reject to make the next Promise fail
          obj.resolve(val)
        } catch (e) {
          obj.reject(e)
        }
      })
    })
  }
}
Copy the code

Static methods that declare promises

Resolve, promise. reject, promise. all, and promise. race are all static methods that return a new Promise.

class Promise {.../** * resolve */
  static resolve(value) {
    // The Promise returns directly
    if (value instanceof Promise) {
      return value
    } else if (typeof value === 'object' && typeof value.then === 'function') {
      // The object passed in contains the then method
      const then = value.then
      return new Promise((resolve) = > {
        then.call(value, resolve)
      })
    } else {
      // Return the new Promise in resolve
      return new Promise((resolve) = > resolve(value))
    }
  }

  /**
   * 直接reject, 测试下Promise.reject并没做特殊处理,所以直接返回即可。
   */
  static reject(value) {
    return new Promise((resolve, reject) = > reject(value))
  }

  /** * passes in an array of 'Promise' and returns a new 'Promise' instance. Success returns the values in order, and one failure becomes a failure */
  static all(promises) {
    return new Promise((resolve, reject) = > {
      let count = 0
      let arr = []
      // Push into the array with the corresponding subscript
      promises.forEach((promise, index) = > {
        // Convert to a Promise object
        Promise.resolve(promise).then((res) = > {
          count++
          arr[index] = res
          if (count === promises.length) {
            resolve(arr)
          }
        }, err= > reject(err))
      })
    })
  }
  
  /** * passes in an array of 'Promise' and returns a new instance of 'Promise', depending on how the first completes */
  static race(promises) {
    return new Promise((resolve, reject) = > {
      promises.forEach((promise, index) = > {
        // Convert to a Promise object
        Promise.resolve(promise).then((res) = > {
          // Who executes direct resolve, or reject first
          resolve(res)
        }, err= > reject(err))
      })
    })
  }
  ...
}
Copy the code

Promise implements the complete code

class Promise {
  _value
  _state = 'pending'
  _queue = []
  constructor(fn) {
    if (typeoffn ! = ='function') {
      throw new Error('Promise resolver undefined is not a function')}/* new Promise((resolve, reject) => {resolve: reject: fail}) */
    fn(this._resolve.bind(this), this._reject.bind(this))}/** * takes 1-2 arguments, the first for a successful callback and the second for a failed callback@param {*} onFulfilled
   * @param {*} onRejected
   * @return {*} 
   * @memberof Promise* /
  then(onFulfilled, onRejected) {
    // Return a new Promise
    return new Promise((resolve, reject) = > {
      // It is possible that the Promise will already resolve, because it can be pre-resolved and registered after the then method, in which case the value can be returned directly to the function
      if (this._state === 'fulfilled' && onFulfilled) {
        this._nextTick(onFulfilled.bind(this.this._value))
        return
      }
      if (this._state === 'rejected' && onRejected) {
        this._nextTick(onRejected.bind(this.this._value))
        return
      }
      // Associate the parameters of the current Promise's then method with the new Promise's resolve, reject
      this._queue.push({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
    })
  }

  /** * Failed to receive callback **@param {*} onRejected
   * @return {*} 
   * @memberof Promise* /
  catch(onRejected) {
    return this.then(null, onRejected)
  }

  /** * Callback executed on both success and failure@param {*} onDone
   * @return {*} 
   * @memberof Promise* /
  finally(onDone) {
    return this.then((value) = > {
      onDone()
      return value
    }, (value) = > {
      onDone()
      // The error can be caught in a try catch
      throw value
    })
  }

  /** * resolve **@static
   * @param {*} value
   * @return {*} 
   * @memberof Promise* /
  static resolve(value) {
    if (value instanceof Promise) {
      return value
    } else if (typeof value === 'object' && typeof value.then === 'function') {
      // The object passed in contains the then method
      const then = value.then
      return new Promise((resolve) = > {
        then.call(value, resolve)
      })
    } else {
      return new Promise((resolve) = > resolve(value))
    }
  }

  /** * Reject, reject, reject, Promise. Reject@static
   * @param {*} value
   * @return {*} 
   * @memberof Promise* /
  static reject(value) {
    return new Promise((resolve, reject) = > reject(value))
  }

  /** * passes in an array of 'Promise' and returns a new 'Promise' instance. Success returns the values in order, and one failure becomes a failure@static
   * @param {*} promises
   * @memberof Promise* /
  static all(promises) {
    return new Promise((resolve, reject) = > {
      let count = 0
      let arr = []
      if (Array.isArray(promises)) {
        if (promises.length === 0) {
          return resolve(promises)
        }
        promises.forEach((promise, index) = > {
          // Convert to a Promise object
          Promise.resolve(promise).then((res) = > {
            count++
            arr[index] = res
            if (count === promises.length) {
              resolve(arr)
            }
          }, err= > reject(err))
        })
        return
      } else {
        reject(`${promises} is not Array`)}}}/** * Passes an array of 'Promise' and returns a new instance of 'Promise', which succeeds or fails depending on how the first completes@static
   * @param {*} promises
   * @return {*} 
   * @memberof Promise* /
  static race(promises) {
    return new Promise((resolve, reject) = > {
      if (Array.isArray(promises)) {
        promises.forEach((promise, index) = > {
          // Convert to a Promise object
          Promise.resolve(promise).then((res) = > {
            resolve(res)
          }, err= > reject(err))
        })
      } else {
        reject(`${promises} is not Array`)}}}// Push into microtasks
  _nextTick(fn) {
    if (typeofMutationObserver ! = ='undefined') { / / the browser
      // This block can be used separately to avoid unnecessary overhead, otherwise nodes need to be generated every time.
      const observer = new MutationObserver(fn)
      let count = 1
      const textNode = document.createTextNode(String(count))
      observer.observe(textNode, {
        characterData: true
      })
      textNode.data = String(++count)
    } else if (typeofprocess.nextTick ! = ='undefined') { // node
      process.nextTick(fn)
    } else {
      setTimeout(fn, 0)}}/ / the resolve to success
  _resolve(value) {
    // When the state is fixed, it does not change anymore
    if (this._state ! = ='pending') return

    // This example returns a Promise, not a Promise, so we need to do something special here.
    If resolve() is a Promise object, we need to resolve the Promise result and pass it to resolve
    if (typeof value === 'object' && typeof value.then === 'function') {
      // We can pass the current _resolve method, because as soon as the next Promise resolve is passed, the then parameter is executed and the value is passed.
      // Get the Promise value
      // this._resove => obj.onFulfilled? .(this._value)
      // this._reject => obj.onRejected? .(this._value)
      value.then(this._resolve.bind(this), this._reject.bind(this))
      return
    }

    // In the print test, if resolve is executed directly in the thread, the state and value of resolve appear to be changed directly, without completing the main process.
    // So state changes and value changes are moved out of the microtask and are only handled through the microtask when a callback is made
    this._state = 'fulfilled'
    this._value = value

    // Push into microtasks
    this._nextTick(() = > {
      this._queue.forEach((obj) = > {
        // Use try catch to catch onFulfilled internal errors
        try {
          // Accept onFulfilled return value. If there is none, pass this._value downward
          const val = obj.onFulfilled ? obj.onFulfilled(this._value) : this._value
          Then ((res) => {consolle.log(res)})
          Resolve is the resolve function of the new Promise, which passes the return value from the then method to the next Promise
          obj.resolve(val)
        } catch (e) {
          obj.reject(e)
        }
      })
    })
  }

  / / reject failure
  _reject(error) {
    if (this._state ! = ='pending') return
    this._state = 'rejected'
    this._value = error

    this._nextTick(() = > {
      this._queue.forEach((obj) = > {
        try {
          // An internal error was caught in the function passed by the user
          if (obj.onRejected) {
            const val = obj.onRejected(this._value)
            // Reject returns a new Promise after the current reject executes, which should be resolved properly, so resolve should be used instead of continuing with reject to make the next Promise fail
            obj.resolve(val)
          } else {
            // Pass recursive reject error
            obj.reject(this._value)
          }
        } catch (e) {
          obj.reject(e)
        }
      })
    })
  }
}
Copy the code

Full demo effect

Blog Post

The complete code of this project: GitHub

This is how Promises work. There are certainly differences between Promises/A+ and Promises. This is just for learning purposes.

QQ group: front end dozen miscellaneous group

Public account: Dongmelon Bookstore