preface

Handwritten Promise, platitude topic, but the actual use, will inevitably step on the pit, today to realize it step by step, feel where it is sacred. If you haven’t written promises yet, or if you’d like to brush up on their implementation, you can check out this article.

The next step is to implement a Promise that complies with the PromiseA+ specification.

Here, PromiseA+

V0: basic implementation

This is a big pity, which must be fulfilled before I forget my Promise. This is a pity, which must be fulfilled before I forget my Promise.

The prototype of the Promise

class MyPromise {
  constructor(executor) {
    this.status = 'pending' / / state
    this.value = undefined // Successful results
    this.reason = undefined // Cause of failure

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

    executor(resolve, reject)
  }

  then(onFulfilled, onRejected){}}Copy the code

Executor fault tolerance

    // The incoming execution function may throw an error
    try {
      // Assign resolve and reject to the user
      executor(resolve, reject)
    } catch (e) {
      // error injection reject
      reject(e)
    }
Copy the code

Resolve and Reject methods

    / / define the resolve
    let resolve = data= > {
      // Can only change from Pending to depressing or Rejected
      if (this.status === 'pending') {
        this.status = 'fulfilled'
        this.value = data
      }
    }
    / / define reject
    let reject = data= > {
      // Can only change from Pending to depressing or Rejected
      if (this.status === 'pending') {
        this.status = 'rejected'
        this.reason = data
      }
    }
Copy the code

Then method

  // Define the then method, passing the result of the fulfilled or rejected into onFulfilled or rejected
  then(onFulfilled, onRejected) {
    if (this.status === 'fulfilled') {
      onFulfilled(this.value)
    }
    if (this.status === 'rejected') {
      onRejected(this.reason)
    }
  }
Copy the code

So, a simple implementation is complete.

Test:

let p = new MyPromise((resolve, reject) = > resolve(1))
p.then(res= > console.log(res)) / / 1
Copy the code

The above example works fine, but what if promises are asynchronous internally?

let q = new MyPromise((resolve, reject) = > {
  setTimeout(() = > {
    resolve(2)},0)
})
q.then(res= > console.log(res)) // There is no output
Copy the code

Due to asynchronous delay, when calling the THEN method, the state is still pending, and ondepressing or onRejected cannot be called. Therefore, we need to deal with the asynchronous case accordingly.

V1: Add asynchronous processing

How do I handle asynchrony?

This is a big pity/Pity. The callback function should be saved until the state changes (fulfilled/Rejected). This is a big pity/Pity. Given the possibility of multiple callback functions, we use arrays to store callback functions, forming callback queues.

1. Define two arrays as callback queues

this.onResolvedCallbacks = [] // Store the successful callback
this.onRejectedCallbacks = [] // Store the failed callback
Copy the code

2. The then method handles callback functions in pending state

  then(onFulfilled, onRejected) {
    if (this.status === 'fulfilled') {
      onFulfilled(this.value)
    }
    if (this.status === 'rejected') {
      onRejected(this.reason)
    }
    // New: Store callback functions when Promise is still in wait state
    if (this.status === 'pending') {
      this.onResolvedCallbacks.push(onFulfilled)
      this.onRejectedCallbacks.push(onRejected)
    }
  }
Copy the code

3. Invoke the callback queue function

When is the callback queue invoked?

Since resolve or Reject is in an asynchronous queue, we already store the corresponding callback function in THEN, so when the state changes, that is, when resolve or Reject occurs, the callback function can be taken out and called in sequence.

Modify the constructor method:

    let resolve = data= > {
      if (this.status === 'pending') {
        this.status = 'fulfilled'
        this.value = data
        // When the state changes, the functions that fetch the callback queue are called in sequence
        this.onResolvedCallbacks.forEach(cb= > cb(this.value))
      }
    }
    let reject = data= > {
      if (this.status === 'pending') {
        this.status = 'rejected'
        this.reason = data
        // When the state changes, the functions that fetch the callback queue are called in sequence
        this.onRejectedCallbacks.forEach(cb= > cb(this.reason))
      }
    }
Copy the code

When we store the callback function, value or Reason does not have a value until the state changes, so we pass in the corresponding value or Reason when we call the callback queue function in turn.

Test to see if adding asynchrony works

let q = new MyPromise((resolve, reject) = > {
  setTimeout(() = > {
    resolve(2)},0)
})
q.then(res= > console.log(res)) / / 2
Copy the code

Asynchronous processing, complete.

Is this it? But don’t forget the chain call of Promise!

V2: Implement chain call

There is no way to implement chain calls based on the previous implementation.

q.then(res= > {
  console.log(res)
  return 3
}).then(res= > console.log(res)) // Uncaught TypeError: Cannot read property 'then' of undefined
Copy the code

Don’t forget Promises/A+ specification: The then method returns A new Promise.

Next, refine the then method

Then parameters: onFulfilled and onRejected

    // Onpity and onRejected are functions
    // Ondepressing is not a function that wraps as a function and returns the passed value
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : value= > value
    // If onRejected is not a function, an error must be thrown; otherwise, resolve will be caught in the chain call
    onRejected =
      typeof onRejected === 'function' ? onRejected : error= > {throw error}    
      
Copy the code

The then method returns a new Promise and adds a try… catch

  then(onFulfilled, onRejected) {
    // Onpity and onRejected are functions
    // Ondepressing is not a function that wraps as a function and returns the passed value
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : value= > value
    // If onRejected is not a function, you need to throw an error, otherwise resolve will be caught in the chain call!!
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : error= > {
            throw error
          }

    const promise = new MyPromise((resolve, reject) = > {
      if (this.status === 'fulfilled') {
        try {
          let x = onFulfilled(this.value)
          resolve(x)
        } catch (e) {
          reject(e)
        }
      }
      if (this.status === 'rejected') {
        try {
          let x = onRejected(this.reason)
          resolve(x)
        } catch (e) {
          reject(e)
        }
      }
      // When a Promise is still in the wait state, store the callback function
      if (this.status === 'pending') {
        this.onResolvedCallbacks.push(() = > {
          try {
            let x = onFulfilled(this.value)
            resolve(x)
          } catch (e) {
            reject(e)
          }
        })
        this.onRejectedCallbacks.push(() = > {
          try {
            let x = onRejected(this.reason)
            resolve(x)
          } catch (e) {
            reject(e)
          }
        })
      }
    })
    return promise
 }
Copy the code

This is not an error when you do the chain call

q.then(res= > {
  console.log(res)
  return 3
}).then(res= > console.log(res)) // Output 2 3 at a time

q.then()then().then(res= > console.log(res)) // 2, passed in sequence, so output 2
Copy the code

At this point, you’ve basically implemented the Promise that supports chain calls.

叒 but in onFulfilled and onRejected we can return any value, including the original data type, reference type and even Promise! Based on the above implementation, it is not enough to process all the returned values. Do not believe you:

q.then(res= > {
  console.log(res)
  return new MyPromise((resolve) = > resolve(3))
}).then(res= > console.log(res)) // Output 2 MyPromise
Copy the code

This is a big pity, but in fact, the Promise will go step by step until it gets a value in the Promise which is fulfilled and onRejected. In other words, he’s actually going to print 2, 3.

This brings us to the next version.

We extracted the logic of resolve from then and replaced it with a complete version of resolvePromise.

V3: Introduce the resolvePromise method

What do we want this function to do?

  • Process the return value X of onFulfilled and onRejected. Resolve is required when onFulfilled and reject is required when onFulfilled
  • Circular reference: Raises TypeError if the return value promise for THEN is the same reference as x (2.3.1)

The parameters required by resolvePromise

Based on the above analysis, the function looks like this

/** * Process the return value of onFulfilled or onRejected * from then@param {Object} The Promise object * returned by the Promise then method@param {*} X onFulfilled or onRejected returns *@param {Function} The resolve Promise constructor's resolve method *@param {Function} Reject The reject method */ of the reject Promise constructor
function resolvePromise(promise, x, resolve, reject) {
  // 
}
Copy the code

A TypeError error is raised during a circular reference

if (promise === x) {
    return reject(new TypeError('Promise loop referenced '))}Copy the code

Processing return value

Something like this:

  // return x as Promise (2.3.2)
  // This section can be omitted because it is already covered in the treatment of THEN below
  // if (x instanceof Promise) {}

  // Return x as an object or function (2.3.3) including x as a Promise (2.3.2)
  if(x ! = =null && (typeof x === 'object' || typeof x === 'function')) {
    // try... Catch prevents an exception from then
    try {
      let then = x.then / / (2.3.3.1)/}catch (e) {
      reject(e) / / (2.3.3.2) (2.3.3.3.4.2)}}else {
    // The return value x is just a normal value (2.3.4)
    resolve(x)
  }
Copy the code

Next, deal with the complex scenario of the return value

  // return x as Promise (2.3.2)
  // This section can be omitted because it is already covered in the treatment of THEN below
  // if (x instanceof Promise) {}

  let called = false // resolve or reject
  // Return x as an object or function (2.3.3) including x as a Promise (2.3.2)
  if(x ! = =null && (typeof x === 'object' || typeof x === 'function')) {
    // try... Catch prevents an exception from then
    try {
      let then = x.then / / (2.3.3.1)
      if (typeof then === 'function') {
        // Return value x has then and then is a function, call it and point this to x (2.3.3.3)
        then.call(
          x,
          y= > {
            if (called) return Resolve or reject (2.3.3.3.3)
            called = true
            // y may still be a Promise, recursive
            resolvePromise(promise, y, resolve, reject)
          },
          r= > {
            if (called) return Resolve or reject (2.3.3.3.3)
            called = true
            reject(r)
          }
        )
      } else {
        // Resolve is a normal object or function
        resolve(x)
      }
    } catch (e) {
      if (called) return Resolve or reject (2.3.3.3.4.1)
      called = true
      reject(e) / / (2.3.3.2) (2.3.3.3.4.2)}}else {
    // The return value x is just a normal value (2.3.4)
    resolve(x)
  }
Copy the code

V4: Add asynchronous delay to all returns in THEN (standard version)

SetTimeout is used here to simulate the delay

  then(onFulfilled, onRejected) {
    // ...
    
    let promise
    promise = new MyPromise((resolve, reject) = > {
      if (this.status === 'fulfilled') {
        setTimeout(() = > {
          try {
            let x = onFulfilled(this.value)
            resolvePromise(promise, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)}if (this.status === 'rejected') {
        setTimeout(() = > {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)}// When a Promise is still in the wait state, store the callback function
      if (this.status === 'pending') {
        this.onResolvedCallbacks.push(() = > {
          setTimeout(() = > {
            try {
              let x = onFulfilled(this.value)
              resolvePromise(promise, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)})this.onRejectedCallbacks.push(() = > {
          setTimeout(() = > {
            try {
              let x = onRejected(this.reason)
              resolvePromise(promise, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)})}})return promise
  }
Copy the code

Standard version implementation: MyPromise

Tests compliance with the PromiseA+ specification

Use promises- aplus-Tests to test whether promises comply with PromiseA+

yarn add promises-aplus-tests
Copy the code

Before testing, end your Promise with:

MyPromise.defer = MyPromise.deferred = function () {
  let defer = {}
  defer.promise = new MyPromise((resolve, reject) = > {
    defer.resolve = resolve
    defer.reject = reject
  })
  return defer
}
try {
  module.exports = MyPromise
} catch (e) {}
Copy the code

Now you can test it

npx promises-aplus-tests Promise.js
Copy the code

Based on the above implementation, you see a series of green checks, culminating in 872 passing (17s), which passes the test.

V5: Plus peripheral methods (modern full version)

In fact, based on V4 is enough, and passed the test. However, when we use Promise, we can directly use Promise. Resolve, Promise, reject and other methods. V5 is based on V4, Added implementations of resolve, Reject, Catch, finally, All, race, allSettled, and so on. This is called the “modern complete Edition”.

Modern full version implementation: Promise_pro

Q & A

1. Why are we talking about this old topic again?

Constant review and review is the last stubbornness of the ungifted. I’d be honored if I could do the same for you.

2. Why use setTimeout to delay all returns in THEN?

First, the Promise itself is synchronous, and its then and catch methods are asynchronous. The use of setTimeout to simulate asynchracy is consistent with the Promise A+ specification and has been tested, but you can also use other methods, such as MutationObserver.

Although setTimeout is a macro task in Eventloop, in reality the then and catch of promises are microtask queues, this implementation is slightly different from reality. But that doesn’t prevent us from understanding how it works by writing A Promise that conforms to the Promise A+ specification.

reference

Thank you for paving the way and helping me move forward

  • PromiseA+
  • The most readable Promise/A+ ever fully implemented
  • Promise is a Promise you can understand