When I read the book Exploring ES6, I suddenly wanted to have A deeper understanding of the design concept and design principle of Promise, so I read some articles and introduction about how to implement A Promise instance that conforms to the Promise/A+ specification, searched some relevant knowledge. I also read other people’s Promise implementation source code.

I was heartened by the results, and my understanding of promises improved considerably by implementing A Promise class that conforms to the Promise/A+ specification 😂.

It all comes down to learning ES6 Promise first, then reading the Promise/A+ specification and Promise implementation and knowledge sharing by third-party developers, and then building our own implementation logic step by step based on the Promise/A+ specification.

Then cut the crap and get started.

From specification to implementation

Before we start writing the code, let’s read the official documentation for Promises/A+.

Several terms

In short, there are five terms mentioned in the official document, as follows:

  • Promise
  • thenable
  • value
  • exception
  • reason

A Promise is an object or function that has then methods and follows the Promise/A+ specification.

Thenable means that an object or function has a THEN method

Value is a valid Javascript value.

Exception is a value thrown using a throw statement.

Reason is the reason why the Promise status changes to Rejected.

Specification briefly

Reading the specification helps us write code, organize our thinking, and finally write A Promise implementation that passes all of the Promise/A+ test cases.

Promise State
  • 2.1.1 There are only three states of a Promise:

  • Pending Indicates the initialization status

    • 2.1.1.1 Can explicitly convert states tofulfilledorrejected
  • Fulfilled successfully

    • 2.1.2.1 State Cannot be reconverted
    • 2.1.2.2 Having an immutablevalue
  • The rejected failure

    • 2.1.3.1 State Cannot be reconverted
    • 2.1.3.2 Have an immutablereason

Immutable means can be compared using === and always true, not completely immutable deep properties.

In addition, when instantiating with new, we need to provide an executor function argument to the constructor.

Thinking 🤔

Now let’s start with the simplest state requirement, assuming that we are in a confined space with only the keyboard at our fingertips.

Consider implementing the Promise State above, organizing the content of the code to be written with a few words, such as:

  • My Promise implementation is calledYo
  • YoSet the initial value and the initial state during initialization. The state can be changed tofulfilledorrejected.
  • YiThere are two static methods to explicitly convert its state:fulfillandReject, when the state ispending“, so that the method can be executed later once the state changes.

Note the corresponding specification information entry

Soon, our implementation looks like this:

// Save some commonly used variables,
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
const nop = () = > {}
const $undefined = undefined
const $function = "function"
// Use Symbol to protect Promise properties
const promiseState = Symbol("promiseState")
const promiseValue = Symbol("promiseValue")

class Yo {
  constructor(executor) {
    // Executor checks ahead of time and throws exceptions without creating additional internal variable and attribute methods
    if(executor === $undefined) {
      throw new TypeError("You have to give a executor param.")}if(typeofexecutor ! == $function) {throw new TypeError("Executor must be a function.")}this[promiseState] = PENDING / / 2.1.1
    this[promiseValue] = $undefined
    try {
      executor(this.$resolve.bind(this), this.$reject.bind(this))}catch (e) {
      this.$reject.bind(this)(e)
    }
  }

  $resolve(value) {
    if(this[promiseState] ! == PENDING)return // 2.1.2.1, 2.1.3.1
    this[promiseState] = FULFILLED / / 2.1.1.1
    this[promiseValue] = value / / 2.1.2.2
  }

  $reject(reason) {
    if(this[promiseState] ! == PENDING)return // 2.1.2.1, 2.1.3.1
    this[promiseState] = REJECTED / / 2.1.1.1
    this[promiseValue] = reason / / 2.1.3.2}}Copy the code
thenmethods

The THEN method is A core part of the Promise/A+ specification.

A Promise must provide a then method to access its value or Reason, which takes two optional arguments:

promise.then(onFulfilled, onRejected)
Copy the code

“Reading specifications always requires a little patience”

Its specifications are as follows:

  • 2.2.1 onFulfilledandonRejectedIt’s all optional
    • 2.2.1.1 ifonFulfilledIf it is not a function, this parameter is ignored
    • 2.2.1.2 ifonRejectedIf it is not a function, this parameter is ignored
  • 2.2.2 ifonFulfilledIt’s a function
    • 2.2.2.1 This function is inpromiseThe status offulfilledIs called asynchronously, and uses itvalueValue as the first argument
    • 2.2.2.2 This function is not availablepromiseThe status offullfilledWas called before
    • 2.2.2.3 in onepromiseCan only be called once on an instance
  • 2.2.3 ifonRejectedIt’s a function
    • 2.2.3.1 This function is inpromiseThe status ofrejectedIs called asynchronously, and uses itvalueValue as the first argument
    • 2.2.3.2 This function is not availablepromiseThe status ofrejectedWas called before
    • 2.2.3.3 in onepromiseCan only be called once on an instance
  • 2.2.4 onFulfilledandonRejectedWill be called asynchronously (cannot be called until the current stack is empty)
  • 2.2.5 onFulfilledandonRejectedMust be called as a function (should not be used internallythisValue, the reason lies in strict mode and non-strict modethisInconsistent values)
  • 2.2.6 thenCan be in the samepromiseInstance, so we can use one in different placespromise.thenMethods f
    • 2.2.6.1 whenpromiseThe status offulfilledWhen, all of themthenOn the incomingonFulfilledFunctions are executed in the order in which they are called
    • 2.2.6.2 whenpromiseThe status ofrejectedWhen, all of themthenOn the incomingonRejectedFunctions are executed in the order in which they are called
  • 2.2.7 thenMethod will eventually return a new onepromiseExample:promise2 = promise1.then(onFulfilled, onRejected)
    • 2.2.7.1 ifonFulfilledoronRejectedReturn a valuex, the implementation ofPromiseParsing steps of:[[Resolve]](promise2, x)
    • 2.2.7.2 ifonFulfilledoronRejectedThrow an exceptione,promise2directlyreject(e)
    • 2.2.7.3 ifonFulfilledIt’s not a function, andpromise1The status offulfilled,promise2Continue to usepromise1The state and value of.
    • 2.2.7.4 ifonFulfilledIt’s not a function, andpromise1The status ofrejected,promise2Continue to usepromise1The state andreason
Perfect ✍ ️

According to the definition of the specification, based on the above code, we will improve the THEN method.

class Yo {
  constructor(executor){...this[promiseConsumers] = []
    try {
      executor(this.$_resolve.bind(this), this.$reject.bind(this))}catch (e) {
      this.$reject.bind(this)(e)
    }
  }

  $resolve(value) {
    if(this[promiseState] ! == PENDING)return // 2.1.2.1, 2.1.3.1
    this[promiseState] = FULFILLED / / 2.1.1.1
    this[promiseValue] = value / / 2.1.2.2
    this.broadcast()
  }

  $reject(reason) {
    if(this[promiseState] ! == PENDING)return // 2.1.2.1, 2.1.3.1
    this[promiseState] = REJECTED / / 2.1.1.1
    this[promiseValue] = reason / / 2.1.3.2
    this.broadcast()
  }

  static then(onFulfilled, onRejected) {
    const promise = new Yo(nop) // The new instance returned by the then method
    / / 2.2.1.1
    promise.onFulfilled = typeof onFulfilled === $function ? onFulfilled : $undefined;
    / / 2.2.1.2
    promise.onRejected = typeof onRejected === $function ? onRejected : $undefined;
    // 2.2.6.1, 2.2.6.2
    this[promiseConsumers].push(promise)
    this.broadcast()
    / / 2.2.7
    return promise
  }

  static broadcast() {
    const promise = this;
    2.2.2.1,.2.2.2.2, 2.2.3.1, 2.2.3.2
    if(this[promiseState] === PENDING) return
    2.2.6.1, 2.2.6.2, 2.2.2.3, 2.2.3.3
    const callbackName = promise[promiseState] === FULFILLED ? "onFulfilled" : "onRejected"
    const resolver = promise[promiseState] === FULFILLED ? "$resolve" : "$reject"
    soon(
      function() {
        2.2.6.1, 2.2.6.2, 2.2.2.3, 2.2.3.3
        const consumers = promise[promiseConsumers].splice(0)
        for (let index = 0; index < consumers.length; index++) {
          const consumer = consumers[index];
          try {
            const callback = consumer[callbackName] // Gets the function passed in when the then method executes
            const value = promise[promiseValue]
            // 2.2.1.1, 2.2.1.2, 2.2.5 without context
            if(callback) {
              consumer['$resolve'](callback(value))
            } else {
              // onpity/onRejected is not a function
              // 2.2.7.3, 2.2.7.4
              consumer[resolver](value)
            }
          } catch (e) {
            // Exception is set to Rejected
            consumer['$reject'](e)
          }
        }
      }
    )
  }
}

// soon function come from Zousan.js
const soon = (() = > {
  const fq = [],  // function queue
    // avoid using shift() by maintaining a start pointer
    // and remove items in chunks of 1024 (bufferSize)
    bufferSize = 1024
  let fqStart = 0
  function callQueue() {
    while(fq.length - fqStart) {
      try {
        fq[fqStart]()
      } catch (err) {
        console.log(err)
      }
      fq[fqStart++] = undefined // increase start pointer and dereference function just called
      if(fqStart === bufferSize) {
        fq.splice(0, bufferSize)
        fqStart = 0}}}// run the callQueue function asyncrhonously as fast as possible
  // Execute this function, and the returned function is assigned to cqYield
  const cqYield = (() = > {
    // Return a function and execute
    // This is the fastest way browsers have to yield processing
    if(typeofMutationObserver ! = ='undefined')
    {
      // first, create a div not attached to DOM to "observe"
      const dd = document.createElement("div")
      const mo = new MutationObserver(callQueue)
      mo.observe(dd, { attributes: true })

      return function() { dd.setAttribute("a".0)}// trigger callback to
    }

    // if No MutationObserver - this is the next best thing for Node
    if(typeofprocess ! = ='undefined' && typeof process.nextTick === "function")
      return function() { process.nextTick(callQueue) }

    // if No MutationObserver - this is the next best thing for MSIE
    if(typeofsetImmediate ! == _undefinedString)return function() { setImmediate(callQueue) }

    // final fallback - shouldn't be used for much except very old browsers
    return function() { setTimeout(callQueue,0)}}) ()// this is the function that will be assigned to soon
  // it take the function to call and examines all arguments
  return fn= > {
    fq.push(fn) // push the function and any remaining arguments along with context
    if((fq.length - fqStart) === 1) { // upon addubg our first entry, keck off the callback
      cqYield()
    }
  }
})()
Copy the code

There are different opinions on the Internet about the logical implementation of onFulfilled or onRejected after the state transition. My favorite implementation comes from @Trincot’s answer on Stack Overflow. If you are interested, you can check the reference link at the end of this article.

The solution to a previously registered callback function that is called asynchronously after a state change is as follows:

  • useconsumersAn array to storethenMethod returnpromise
  • inthenMethod for each to be returnedpromiseAdds the parameter with the same name that it passesonFulfilledandonRejectedAs aPromiseProperty of.
  • For some that have already switched statesPromiseInstance, needs to be inthenMethodbroadcastMethods.

The broadcast method is critical and is called once in the resolve, Reject, and then methods.

We use the broadcast method to make a “broadcast” function. When the Promise state is transformed, we will create a micro-task according to its state, and asynchronously call the onFulfilled or onRejected attribute methods on all promises in the consumers array.

In addition, how to create microtasks to asynchronously execute related functions is also the key to realize the Promise class. Here, I learned the Promise implementation scheme of the predecessors of @blueJava: Zousan.js, with its Github warehouse address at the end of the article.

In Zousan.js, the author specifically creates a soon function that creates a microtask to execute as quickly as possible from the function parameters passed in.

The core of this is to create document nodes that use the API to create microtasks and eventually perform the target function if the browser environment supports MutationObserver. If not, check process.nextTick and setImmediate. Finally, setTimeout is used to create a macro task to call the target function asynchronously.

At this point, our Yo class is almost complete, and finally The third specification: The Promise Resolution Procedure.

The Promise Resolution Procedure

The Promise Resolution Procedure is expressed as [[Resolve]](Promise, x), why do we need to implement this specification?

When we use the resolve or reject methods in executor functions, the arguments passed in can be any valid Javascript value. In some cases, this value can be a primitive type of data, a Thenables object, or a Promise instance created by another Promise implementation.

We need to deal with this problem so that the different parameters have a consistent and exact solution.

So, let’s move on to how the specification is defined.

The steps to execute [[Resolve]](promise, x) are as follows:

  • 2.3.1 ifpromiseandxReference the same object, thenrejectaTypeErrorAs abnormalreason
  • 2.3.2 ifxIs aPromise, then its state is adopted
    • 2.3.2.1 ifxispendingThe,promisekeeppendinguntilxState changes
    • 2.3.2.2 、2.3.2.3 xWhen the condition is stable, use it directlyvalueorreason
  • 2.3.3 If it is notPromiseBut an ordinary personthenableobject
    • 2.3.3.1 setthenIs equal to thex.then
    • 2.3.3.2 If it is obtainedx.thenValue throws an exception, thenrejectthispromiseAnd treat the exception asreason
    • 2.3.3.3 ifthenIs a function, then willxBound to this functionthisObject that can be passed in order to change the currentpromiseMethod of stateresolveandreject
      • 2.3.3.3.1 ifresolveExecute and pass in oneyValue, the command is executed[[Resolve]](promise, y)
      • 2.3.3.3.2 ifrejectExecute and pass in onereason, this is adoptedreasonAs arejectedThe state of thereason
      • 2.3.3.3.3 ifresolveandrejectThe first call takes precedence, and only the first call is executed. Subsequent calls are ignored
      • 2.3.3.3.4 If calledthenAn exception was thrown when
        • 2.3.3.3.4.1 ifresolveorrejectIf yes, this exception is ignored
        • 2.3.3.3.4.2 otherwise,rejectThis exception serves as itsreason
    • 2.3.3.4 ifthenIs not a function, then thispromiseIn order toxforvalue, the state changes tofulfilled
  • 2.3.4 ifxIs not an object or functionpromiseIn order toxforvalue, the state changes tofulfilled

For Promise, it is the function that transforms the state that needs to consider how the above specification is implemented.

Let’s continue to work on the unfinished code.

Because I need to handle a complex resolve function, rather than just changing the state and setting value or reason after settling, I chose to name this method $_resolve to distinguish it from the simple $resolve method.

class Yo {... $_resolve(x) {let hasCalled,then;
    / / 2.3.1
    if(this === x) {
      console.log('circular');
      throw new TypeError("Circular reference error, value is promise itself.")}/ / 2.3.2
    if(x instanceof Yo) {
      console.log('instance');
      / / 2.3.2.1, 2.3.2.2, 2.3.2.3
      x.then(this.$_resolve.bind(this), this.$reject.bind(this))}else if(x === Object(x)) {
      / / 2.3.3
      try {
        / / 2.3.3.1
        then = x.then;
        if(typeof then === $function) {
          / / 2.3.3.3
          then.call(
            x,
            // first argument resolvePromise
            function(y) {
              if(hasCalled) return
              hasCalled = true
              / / 2.3.3.3.1
              this.$_resolve(y)
            }.bind(this),
            // second argument is rejectPromise
            function (reasonY) {
              if(hasCalled) return
              hasCalled = true
              / / 2.3.3.3.2
              this.$reject(reasonY)
            }.bind(this))}else {
          // 2.3.3.4 Original value
          this.$resolve(x)
        }
      } catch (e) {
        // 2.3.3.2, 2.3.3.3.4 Abnormal
        if(hasCalled) return / / 2.3.3.3.4.1
        this.$reject(e) / / 2.3.3.3.4.2}}else {
      // 2.3.4 Original value
      this.$resolve(x)
    }
  }
  ...
}
Copy the code

At this point

For a Promise implementation, we also need to add a catch method, which can be seen as syntactic sugar for the then method.

Of course, static methods resolve and reject can simply be added.

class Yo{...catch(onRejected) {
    return this.then($undefined, onRejected)
  }

  static reject(reason) {
    return new Yo((_, reject) = > {
      reject(reason)
    })
  }

  static resolve(value) {
    return new Yo(resolve= > {
      resolve(value)
    })
  }
	...
}
Copy the code

Finally, we tested our implementation using Promises – aplus-Tests.

After installing the dependencies, add the deferred static method to Yo as follows:

class Yo {...static deferred() {
    const result = {}
    result.promise = new Yo((resolve, reject) = > {
      result.resolve = resolve
      result.reject = reject
    })
    return result
  }
  ...
}
Copy the code

Then add the test command to the scripts field of package.json, and use YARN Run test to test as follows:

Since then, we’ve implemented promises that comply with the Promise/A+ specification. While Yo may not be robust enough, even some of the usual methods aren’t provided, as A simple implementation for learning about Promise, Yo does its job well. All the code is shown below (you can also access the GitHub repository source youyiqin/ Yo source code by referring to the last item below) :

Finish scattering flowers.

Write in the last

Learning about ES6 Promises, and reading some third-party Promise implementation examples from developers online, is very useful for understanding and using promises for asynchronous programming. Personally implementing A Promise implementation that can be tested through Promise/A+ test cases has enhanced the author’s ability to use Promise to some extent.

Welcome to discuss and learn ~

reference

  • Basic Javascript promise implementation attempt – Stack Overflow
  • bluejava/zousan: A Lightning Fast, Yet Very Small Promise A+ Compliant Implementation
  • Youyiqin/yukio okamoto, the source code