Implementation of the basic structure

Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

class CustomPromise {
  #STATUS_PENDING = 'pending'
  #STATUS_FULFILLED = 'fulfilled'
  #STATUS_REJECTED = 'rejected'

  #status = this.#STATUS_PENDING

  #resolve = () = > {}
  #reject = () = > {}

  constructor(executor) {
    const resolve = value= > {
      if (this.#status === this.#STATUS_PENDING) {
        this.#status = this.#STATUS_FULFILLED

        // At this point, only the executor method is being executed
        // The then method is not actually implemented
        // So use queueMicrotask to put the execution of resolve or reject into the microtask queue of the current event queue
        When the main thread is complete, the browser will automatically execute the microtask queue in the current event queue
        queueMicrotask(() = > {
          this.#resolve(value)
        })
      }
    }

    const reject = reason= > {
      if (this.#status === this.#STATUS_PENDING) {
        this.#status = this.#STATUS_REJECTED

        queueMicrotask(() = > {
          this.#reject(reason)
        })
      }
    }

    executor(resolve, reject)
  }

  then(onresolved, onrejected) {
    this.#resolve = onresolved
    this.#reject = onrejected
  }
}

// Test the code
const promise = new CustomPromise((resolve, reject) = > {
  resolve(123)
  reject('error')
})

promise.then(res= > console.log(res), err= > console.log(err))
Copy the code

Instance methods

Then method

Execute the THEN method multiple times

In the code that implemented the promise basic structure, if we called the THEN method multiple times, the later THEN method would override the previous THEN method

#resolve = onresolved
// So multiple promise.then calls will overwrite the previous one
promise.then(res= > console.log(res), err= > console.log(err))
promise.then(res= > console.log('res:' + res), err= > console.log('err' + err))
Copy the code

So, in effect, we want the array to store all the onresolve and OnReject methods passed in

class CustomPromise {
  #STATUS_PENDING = 'pending'
  #STATUS_FULFILLED = 'fulfilled'
  #STATUS_REJECTED = 'rejected'

  #status = this.#STATUS_PENDING

  #resolveFns = []
  #rejecetFns = []

  constructor(executor) {
    const resolve = value= > {
      if (this.#status === this.#STATUS_PENDING) {
        this.#status = this.#STATUS_FULFILLED

        queueMicrotask(() = > {
          this.#resolveFns.forEach(fn= > fn(value))
        })
      }
    }

    const reject = reason= > {
      if (this.#status === this.#STATUS_PENDING) {
        this.#status = this.#STATUS_REJECTED

        queueMicrotask(() = > {
          this.#rejecetFns.forEach(fn= > fn(reason))
        })
      }
    }

    executor(resolve, reject)
  }

  then(onresolved, onrejected) {
    if (onresolved) {
      this.#resolveFns.push(onresolved)
    }

    if (onrejected) {
      this.#rejecetFns.push(onrejected)
    }
  }
}

// Test the code
const promise = new CustomPromise((resolve, reject) = > {
  resolve(123)
  reject('error')
})

promise.then(res= > console.log(res), err= > console.log(err))
promise.then(res= > console.log('res: ' + res), err= > console.log('err: ' + err))
Copy the code

But there is still a problem

promise.then(res= > console.log(res), err= > console.log(err))
promise.then(res= > console.log('res: ' + res), err= > console.log('err: ' + err))

setTimeout(() = > {
  // The then method is not called back normally
  // The executor resolve method is still executed
  // It simply adds the body of the function to the array that needs to be executed, but it does not execute
  promise.then(res= > console.log('setTimeout res: ' + res), err= > console.log('setTimeout err: ' + err))
}, 1000)
Copy the code

So the code needs to be improved accordingly

class CustomPromise {
  #STATUS_PENDING = 'pending'
  #STATUS_FULFILLED = 'fulfilled'
  #STATUS_REJECTED = 'rejected'

  #status = this.#STATUS_PENDING

  #resolveFns = []
  #rejecetFns = []

  #value = undefined
  #reason = undefined

  constructor(executor) {

    const resolve = value= > {
      // add microtask
      queueMicrotask(() = > {
        if (this.#status === this.#STATUS_PENDING) {
          this.#status = this.#STATUS_FULFILLED
          this.#value = value
          this.#resolveFns.forEach(fn= > fn(value))
        }
      })
    }

    const reject = reason= > {
      // add microtask
      queueMicrotask(() = > {
        if (this.#status === this.#STATUS_PENDING) {
          this.#status = this.#STATUS_REJECTED
          this.#reason = reason
          this.#rejecetFns.forEach(fn= > fn(reason))
        }
      })
    }

    executor(resolve, reject)
  }

  then(onresolved, onrejected) {
    // If the current status is Resolved or Rejected, the status is determined. Execute the corresponding function
    if (onresolved && this.#status === this.#STATUS_FULFILLED) {
      onresolved(this.#value)
    }

    if (onrejected && this.#status === this.#STATUS_REJECTED) {
      onrejected(this.#reason)
    }

    // If the current state is pending, all resolved or Rejected functions need to be stored in an array and called later
    if (this.#status === this.#STATUS_PENDING) {
      if (onresolved) {
        this.#resolveFns.push(onresolved)
      }

      if (onrejected) {
        this.#rejecetFns.push(onrejected)
      }
   }
  }
}

// Test the code
const promise = new CustomPromise((resolve, reject) = > {
  resolve(123)
  reject('error')
})

promise.then(res= > console.log(res), err= > console.log(err))
promise.then(res= > console.log('res: ' + res), err= > console.log('err: ' + err))

setTimeout(() = > {
  promise.then(res= > console.log('setTimeout res: ' + res), err= > console.log('setTimeout err: ' + err))
}, 1000)
Copy the code

Chain calls

The promise.then method should return a new promise, but the previous code logic then method did not return a value, so the chain call was not possible

This needs to be modified accordingly

// tool function --> Make a try for a Promise instance... catch ... Method encapsulation
function catchPromiseError(fn, param, resolve, reject) {
  try {
    resolve(fn(param))
  } catch (err) {
    reject(err)
  }
}

class CustomPromise {
  #STATUS_PENDING = 'pending'
  #STATUS_FULFILLED = 'fulfilled'
  #STATUS_REJECTED = 'rejected'

  #resolveFns = []
  #rejecetFns = []

  #value = undefined
  #reason = undefined

  constructor(executor) {
    this.status = this.#STATUS_PENDING

    const resolve = value= > {
      // add microtask
      queueMicrotask(() = > {
        if (this.status === this.#STATUS_PENDING) {
          this.status = this.#STATUS_FULFILLED
          this.#value = value
          this.#resolveFns.forEach(fn= > fn(value))
        }
      })
    }

    const reject = reason= > {
      // add microtask
      queueMicrotask(() = > {
        if (this.status === this.#STATUS_PENDING) {
          this.status = this.#STATUS_REJECTED
          this.#reason = reason
          this.#rejecetFns.forEach(fn= > fn(reason))
        }
      })
    }

    executor(resolve, reject)
  }

  then(onresolved, onrejected) {
    // The return value requires a PROMISE, and the Promise executor function executes immediately
    // So use a new promise to wrap the logic implementation in the then method
    return new CustomPromise((resolve, reject) = > {
      if (onresolved && this.status === this.#STATUS_FULFILLED) {
        catchPromiseError(onresolved, this.#value, resolve, reject)
      }

      if (onrejected && this.status === this.#STATUS_REJECTED) {
        catchPromiseError(onrejected, this.#reason, resolve, reject)
      }

      if (this.status === this.#STATUS_PENDING) {
        if (onresolved) {
          // Use closures
          // The outer function is passed in, but the internal catchPromiseError method is actually executed
          The purpose is to get the correct resolve and reject values
          this.#resolveFns.push(value= > catchPromiseError(onresolved, value, resolve, reject))
        }

        if (onrejected) {
          this.#rejecetFns.push(reason= > catchPromiseError(onrejected, reason, resolve, reject))
        }
      }
    })
  }
}

// Test the code
const promise = new CustomPromise((resolve, reject) = > {
  // resolve(123)
  reject('error')
})

promise
  .then(res= > {
    console.log('res1: ' + res)
    // return 'res1 resolved return value'
    throw new Error('error in res1 resolved')},err= > {
    console.log('err1: ' + err)
    return 'errr1 rejected return value'
  })
  .then(res= > console.log('res2: ' + res), err= > console.log('err2: ' + err))

setTimeout(() = > {
  promise.then(res= > console.log('setTimeout res: ' + res), err= > console.log('setTimeout err: ' + err))
}, 1000)
Copy the code

But the return value could be a new Promise instance or an object that implements ThEnable

function catchPromiseError(fn, param, resolve, reject, status) {
  try {
    const res = fn(param)

    // When the outer promise state is Resolved
    if (status === 'resolved') {
      // The return value is a new promise
      if (res instanceof CustomPromise) {
        // Execute the returned promise to determine the status of the actual returned value
        res.then(res= > resolve(res), err= > reject(err))
      } else if (typeofres? .then ==='function') {
        // If the return value is an object implementing the THEN method, the final state is determined by the thenable object
        res.then(resolve, reject)
      } else {
        resolve(res)
      }
    } else {
      resolve(res)
    }
  } catch (err) {
    reject(err)
  }
}

class CustomPromise {
  #STATUS_PENDING = 'pending'
  #STATUS_FULFILLED = 'fulfilled'
  #STATUS_REJECTED = 'rejected'

  #status = this.#STATUS_PENDING

  #resolveFns = []
  #rejecetFns = []

  #value = undefined
  #reason = undefined

  constructor(executor) {
    const resolve = value= > {
      // add microtask
      queueMicrotask(() = > {
        if (this.#status === this.#STATUS_PENDING) {
          this.#status = this.#STATUS_FULFILLED
          this.#value = value
          this.#resolveFns.forEach(fn= > fn(value))
        }
      })
    }

    const reject = reason= > {
      // add microtask
      queueMicrotask(() = > {
        if (this.#status === this.#STATUS_PENDING) {
          this.#status = this.#STATUS_REJECTED
          this.#reason = reason
          this.#rejecetFns.forEach(fn= > fn(reason))
        }
      })
    }

    executor(resolve, reject)
  }

  then(onresolved, onrejected) {
    return new CustomPromise((resolve, reject) = > {
      if (onresolved && this.#status === this.#STATUS_FULFILLED) {
        catchPromiseError(onresolved, this.#value, resolve, reject, 'resolved')}if (onrejected && this.#status === this.#STATUS_REJECTED) {
        catchPromiseError(onrejected, this.#reason, resolve, reject)
      }

      if (this.#status === this.#STATUS_PENDING) {
        if (onresolved) {
          this.#resolveFns.push(value= > catchPromiseError(onresolved, value, resolve, reject, 'resolved'))}if (onrejected) {
          this.#rejecetFns.push(reason= > catchPromiseError(onrejected, reason, resolve, reject))
        }
      }
    })
  }
}

// Test the code
const promise = new CustomPromise((resolve, reject) = > {
  resolve(123)
  // reject('error')
})

promise
  .then(res= > {
    console.log('res1: ' + res)
    // return new CustomPromise((resolve, reject) => reject('something error in return promise'))
    return {
      then(resolve, reject) {
        reject('rejected in thenable object')}}},err= > {
    console.log('err1: ' + err)
  })
  .then(res= > console.log('res2: ' + res), err= > console.log('err2: ' + err))
Copy the code

Catch method

function catchPromiseError(fn, param, resolve, reject, status) {
  try {
    const res = fn(param)

    if (status === 'resolved') {
      if (res instanceof CustomPromise) {
        res.then(res= > resolve(res), err= > reject(err))
      } else if (typeofres? .then ==='function') {
        res.then(resolve, reject)
      } else {
        resolve(res)
      }
    } else {
       resolve(res)
    }
  } catch (err) {
    reject(err)
  }
}

class CustomPromise {
  #STATUS_PENDING = 'pending'
  #STATUS_FULFILLED = 'fulfilled'
  #STATUS_REJECTED = 'rejected'

  #status = this.#STATUS_PENDING

  #resolveFns = []
  #rejecetFns = []

  #value = undefined
  #reason = undefined

  constructor(executor) {
    const resolve = value= > {
      queueMicrotask(() = > {
        if (this.#status === this.#STATUS_PENDING) {
          this.#status = this.#STATUS_FULFILLED
          this.#value = value
          this.#resolveFns.forEach(fn= > fn(value))
        }
      })
    }

    const reject = reason= > {
      queueMicrotask(() = > {
        if (this.#status === this.#STATUS_PENDING) {
          this.#status = this.#STATUS_REJECTED
          this.#reason = reason
          this.#rejecetFns.forEach(fn= > fn(reason))
        }
      })
    }

    executor(resolve, reject)
  }

  // If the onRejected method is not passed, the default value is used
  // If the then does not handle the exception itself, it passes to the next THEN or catch method
  then(onresolved, onrejected = err => { throw err }) {
    return new CustomPromise((resolve, reject) = > {
      if (onresolved && this.#status === this.#STATUS_FULFILLED) {
        catchPromiseError(onresolved, this.#value, resolve, reject, 'resolved')}if (onrejected && this.#status === this.#STATUS_REJECTED) {
        catchPromiseError(onrejected, this.#reason, resolve, reject)
      }

      if (this.#status === this.#STATUS_PENDING) {
        if (onresolved) {
          this.#resolveFns.push(value= > catchPromiseError(onresolved, value, resolve, reject, 'resolved'))}if (onrejected) {
          this.#rejecetFns.push(reason= > catchPromiseError(onrejected, reason, resolve, reject))
        }
      }
    })
  }

  catch(onrejected) {
    // Catch is essentially a then method in a special resolved function with undefined
    The catch method still needs to return a new promise instance
    return this.then(undefined, onrejected)
  }
}

// Test the code
const promise = new CustomPromise((resolve, reject) = > {
  reject('error')
})

promise
  .then(res= > {
    console.log('res1: ' + res)
  })
  // Catch should essentially accept the Promise. then method which returns the Promise rejected method
  // instead of the Rejected method, which promise returns
  .catch(err= > console.log('err2: ' + err))

// When a promise itself can handle the rejected state, there is no need for the catch method to handle the corresponding logic
promise
  .then(res= > {
    console.log('res1: ' + res)
  }, err= > console.log('err1: ' + err))
  .catch(err= > console.log('err2: ' + err))
Copy the code

The finally method

function catchPromiseError(fn, param, resolve, reject, status) {
  try {
    const res = fn(param)

    if (status === 'resolved') {
      if (res instanceof CustomPromise) {
        res.then(res= > resolve(res), err= > reject(err))
      } else if (typeofres? .then ==='function') {
        res.then(resolve, reject)
      } else {
        resolve(res)
      }
    } else {
       resolve(res)
    }
  } catch (err) {
    reject(err)
  }
}

class CustomPromise {
  #STATUS_PENDING = 'pending'
  #STATUS_FULFILLED = 'fulfilled'
  #STATUS_REJECTED = 'rejected'

  #status = this.#STATUS_PENDING

  #resolveFns = []
  #rejecetFns = []

  #value = undefined
  #reason = undefined

  constructor(executor) {
    const resolve = value= > {
      queueMicrotask(() = > {
        if (this.#status === this.#STATUS_PENDING) {
          this.#status = this.#STATUS_FULFILLED
          this.#value = value
          this.#resolveFns.forEach(fn= > fn(value))
        }
      })
    }

    const reject = reason= > {
      queueMicrotask(() = > {
        if (this.#status === this.#STATUS_PENDING) {
          this.#status = this.#STATUS_REJECTED
          this.#reason = reason
          this.#rejecetFns.forEach(fn= > fn(reason))
        }
      })
    }

    executor(resolve, reject)
  }

  // The then method can still be used after a catch, and the catch does not handle the resolve state
  // The value of the resolve state returned by the previous promise needs to be forwarded
  // Pass to the next then method for processing
  then(onresolved = value => value, onrejected = err => { throw err }) {
    return new CustomPromise((resolve, reject) = > {
      if (this.#status === this.#STATUS_FULFILLED) {
        catchPromiseError(onresolved, this.#value, resolve, reject, 'resolved')}if (this.#status === this.#STATUS_REJECTED) {
        catchPromiseError(onrejected, this.#reason, resolve, reject)
      }

      if (this.#status === this.#STATUS_PENDING) {
        this.#resolveFns.push(value= > catchPromiseError(onresolved, value, resolve, reject, 'resolved'))

        this.#rejecetFns.push(reason= > catchPromiseError(onrejected, reason, resolve, reject))
      }
    })
  }

  catch(onrejected) {
    return this.then(undefined, onrejected)
  }

  finally(onfinally) {
    // finally does not need to have any return value
    // Finally is also a special then method - the resolve and rejeced methods implement the same logic
    this.then(onfinally, onfinally)
  }
}

// Test the code
const promise = new CustomPromise((resolve, reject) = > {
  resolve(123)
  // reject('error')
})

promise
  .catch(err= > {
    console.log('err2: ' + err)
    return 123
  })
  .then(res= > console.log('res: ' + res))
  .finally(() = > console.log('finally'))
Copy the code

A static method

Resolve and reject

{
  / /... Code omission here
  
  static resolve(value) {
    return new CustomPromise(resolve= > resolve(value))
  }

  static reject(reason) {
    return new CustomPromise((resolve, reject) = > reject(reason))
  }
  
  / /... Code omission here
}
Copy the code

All and allSettled

all

static all(promises) {
  let count = 0
  const results = []

  return new CustomPromise((resolve, reject) = > {
    // IIFE is used to ensure that the order of the array in resolve returned is the same as that passed in
    // Instead of the order of execution
    promises.forEach((promise, index) = >{(function(index) {
        // If a Promise instance is not passed in, you need to convert it to a Promise instance object
        if(! (promiseinstanceof CustomPromise)) {
          promise = CustomPromise.resolve(promise)
        }

        promise.then(res= > {
          results[index] = res
          count++

          if (count === promises.length) {
            resolve(results)
          }
        }, reject) Reject => reject(err)
      })(index)
    })
  })
}
Copy the code

allSettled

The allSettled method will wait until all of the promises are settled before returning a result
// The allSettled method will execute resolved and never the Rejected method
static allSettled(promises) {
  const results = []
  let count = 0

  return new CustomPromise(resolve= > {
    promises.forEach((promise, index) = > {
      if(! (promiseinstanceof CustomPromise)) {
         promise = CustomPromise.resolve(promise)
      }
      
      (function(index) {
        promise.then(res= > {
          results[index] = {
            status: 'fulfilled'.value: res
          }
          count++

          if(count === promises.length){
            resolve(results)
          }
        }, err= > {
          results[index] = {
            stauts: 'rejected'.reason: err
          }
          count++

          if(count === promises.length){
            resolve(results)
          }
        })
      })(index)
    })
  })
}
Copy the code

Race and any

race

static race(promises) {
  return new CustomPromise((resolve, reject) = > {
    promises.forEach(promise= > {
      if(! (promiseinstanceof CustomPromise)) {
        promise = CustomPromise.resolve(promise)
      }

      promise.then(resolve, reject)
    })
  })
}
Copy the code

any

static any(promises) {
  const results = []
  let count = 0

  return new CustomPromise((resolve, reject) = > {
    promises.forEach((promise, index) = > {
      if(! (promiseinstanceof CustomPromise)) {
        promise = CustomPromise.resolve(promise)
      }

      (function(index) {
        promise.then(resolve, err= > {
          results[index] = err
          count++

          if (count === promises.length) {
            // The current node LTS version is v14.18.1
            // AggregateError is new in ES2021
            // So the any method needs to be tested on current versions of Node or the latest versions of browsers
            reject(new AggregateError(results, 'All promises were rejected'))
          }
        })
      })(index)
    })
  })
}
Copy the code

Complete implementation

function catchPromiseError(fn, param, resolve, reject, status) {
  try {
    const res = fn(param)

    if (status === 'resolved') {
      if (res instanceof CustomPromise) {
        res.then(res= > resolve(res), err= > reject(err))
      } else if (typeofres? .then ==='function') {
        res.then(resolve, reject)
      }
    } else {
       resolve(res)
    } else {
      resolve(res)
    }
  } catch (err) {
    reject(err)
  }
}

class CustomPromise {
  #STATUS_PENDING = 'pending'
  #STATUS_FULFILLED = 'fulfilled'
  #STATUS_REJECTED = 'rejected'

  #status = this.#STATUS_PENDING

  #resolveFns = []
  #rejecetFns = []

  #value = undefined
  #reason = undefined

  constructor(executor) {
    const resolve = value= > {
      queueMicrotask(() = > {
        if (this.#status === this.#STATUS_PENDING) {
          this.#status = this.#STATUS_FULFILLED
          this.#value = value
          this.#resolveFns.forEach(fn= > fn(value))
        }
      })
    }

    const reject = reason= > {
      queueMicrotask(() = > {
        if (this.#status === this.#STATUS_PENDING) {
          this.#status = this.#STATUS_REJECTED
          this.#reason = reason
          this.#rejecetFns.forEach(fn= > fn(reason))
        }
      })
    }

    executor(resolve, reject)
  }

  then(onresolved = value => value, onrejected = err => { throw err }) {
    return new CustomPromise((resolve, reject) = > {
      if (this.#status === this.#STATUS_FULFILLED) {
        catchPromiseError(onresolved, this.#value, resolve, reject, 'resolved')}if (this.#status === this.#STATUS_REJECTED) {
        catchPromiseError(onrejected, this.#reason, resolve, reject)
      }

      if (this.#status === this.#STATUS_PENDING) {
        this.#resolveFns.push(value= > catchPromiseError(onresolved, value, resolve, reject, 'resolved'))

        this.#rejecetFns.push(reason= > catchPromiseError(onrejected, reason, resolve, reject))
      }
    })
  }

  catch(onrejected) {
    return this.then(undefined, onrejected)
  }

  finally(onfinally) {
    this.then(onfinally, onfinally)
  }

  static resolve(value) {
    return new CustomPromise(resolve= > resolve(value))
  }

  static reject(reason) {
    return new CustomPromise((resolve, reject) = > reject(reason))
  }

  static all(promises) {
    let count = 0
    const results = []

    return new CustomPromise((resolve, reject) = > {
      promises.forEach((promise, index) = >{(function(index) {
          if(! (promiseinstanceof CustomPromise)) {
            promise = CustomPromise.resolve(promise)
          }

          promise.then(res= > {
            results[index] = res
            count++

            if (count === promises.length) {
              resolve(results)
            }
          }, reject)
        })(index)
      })
    })
  }

  static allSettled(promises) {
    const results = []
    let count = 0

      return new CustomPromise(resolve= > {
        promises.forEach((promise, index) = > {
          if(! (promiseinstanceof CustomPromise)) {
            promise = CustomPromise.resolve(promise)
          }

         (function(index) {
            promise.then(res= > {
              results[index] = {
                status: 'fulfilled'.value: res
              }
              count++

              if(count === promises.length){
                resolve(results)
              }
            }, err= > {
              results[index] = {
                stauts: 'rejected'.reason: err
              }
              count++

              if(count === promises.length){
                resolve(results)
              }
            })
         })(index)
        })
    })
  }

  static race(promises) {
    return new CustomPromise((resolve, reject) = > {
      promises.forEach(promise= > {
        if(! (promiseinstanceof CustomPromise)) {
          promise = CustomPromise.resolve(promise)
        }

        promise.then(resolve, reject)
      })
    })
  }

  static any(promises) {
    const results = []
    let count = 0

    return new CustomPromise((resolve, reject) = > {
      promises.forEach((promise, index) = > {
        if(! (promiseinstanceof CustomPromise)) {
          promise = CustomPromise.resolve(promise)
        }

        (function(index) {
          promise.then(resolve, err= > {
            results[index] = err
            count++

            if (count === promises.length) {
              reject(new AggregateError(results, 'All promises were rejected'))
            }
         })
        })(index)
      })
    })
  }
}
Copy the code