Write Promises by hand and pass Promises/A+ all tests

The first is in a wechat public account to see an article, there is a part of the handwritten Promise, feel very novel, is also a challenge, then I also want to try, the difference is that they are a common hand-written JS set of an article list, Promise is just a small piece of it, I want to carry it out alone, ha ha

Public account article

This article addresses

The basic concept

1.Promise

Promise is a solution to asynchronous programming that is more rational and powerful than the traditional solution of callbacks and events. It was first proposed and implemented by the community, and ES6 wrote it into the language standard, unifying usage and providing Promise objects natively.

A Promise is simply a container that holds the result of an event (usually an asynchronous operation) that will end in the future. Syntactically, a Promise is an object from which to get messages for asynchronous operations. Promise provides a uniform API so that various asynchronous operations can be handled in the same way.

The above is from Ruan Yifeng’s introduction to ECMAScript 6

2.Promises/A+

What are Promises/A+? Here is A sentence from Promises/A+ website

An open standard for sound, interoperable JavaScript promises — by implementers, for implementers.

Promises/A+ is the open standard for JavaScript Promises. Promises follow the basic standard. Promises are known as ES6 Promises that fully comply with Promises/A+ specifications. They are not identical, however. ES6 Promises complement catch, finally, static methods all, resolve, reject, and so on

3.Promises/A+?

At first, I wanted to list all the rules for Promises/A+, but after A while, it seemed like there was no need for it. Everyone interested can go to have a look at their own, I think the consult the original English, use Google translation can roughly basically accurate, but there are still a little bit of translation does not reach the designated position, is recommended here to look at the others translation of ready-made, I here looking for a someone else’s translation, personal feel translation also listen to good, There are notes to Promises/A+ translation

Write a Promise

Promise /A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises

Handwritten Promise code

const CustomPromise = class  {
  // Define a static map of all states
  static STATUS_MAP = {
    Pending: 'Pending'.Fulfilled: 'Fulfilled'.Rejected: 'Rejected',}// The state of the Promise
  status = CustomPromise.STATUS_MAP.Pending
  This is a list of onfulfilled functions passed to the then method
  onfulfilled = []
  // The list of onRejected functions passed in to the then method
  onrejected = []
  
  result = undefined
  reason = undefined

  // The list of the executor callback function resolve for the Promise parameter returned by the then method
  resolve = []
  // A list of the reject callback function for the Promise parameter returned by the then method
  reject = []
  // The list of promises returned by the then method
  promises = []

  /** * constructor *@param {function} Executor Promise Specifies the executor@returns* /
  constructor (executor) {
    if (typeof executor === 'undefined' || typeofexecutor ! = ='function') {
      throw new TypeError('CustomPromise resolver is not a function')}This function will be fulfilled gradually. This function will be fulfilled gradually@param {*} result 
     */
    const setResult = (result) = > {
      this.result = result
      this.status = CustomPromise.STATUS_MAP.Fulfilled
      if (this.onfulfilled.length > 0) {
        this.onfulfilled.forEach((onfulfilled_item, index) = > {
          this.excuteOnfulfilled(onfulfilled_item, index, this.result)
        })
      }
    }

    /** * set the reason you rejected and the onrejected function *@param {*} result 
     */
    const setReason= (reason) = > {
      this.reason = reason
      this.status = CustomPromise.STATUS_MAP.Rejected
      if (this.onrejected.length > 0) {
        this.onrejected.forEach((onrejected_item, index) = > {
          this.excuteOnrejected(onrejected_item, index, this.reason)
        })
      }
    }
    try {
      const resolve = (result) = > {
        if (this.status === CustomPromise.STATUS_MAP.Pending) { // The internal state of the Promise has the effect of solidification, once determined no change
          if(result ! = =null && (typeof result === 'function' || typeof result === 'object')) {
            let called = false
            try {
              const { then } = result // The resolve method accepts a thenable object
              if (typeof then === 'function') {
                const then_ = then.bind(result)
                then_(res= > {
                  if (called) return // Ensure that the resolvePromise callback for the then method on the thenable object is executed only once
                  called = true
                  setResult(res)
                }, err= > {
                  if (called) return
                  called = true
                  setReason(err)
                })
              } else {
                setResult(result)
              }
            } catch (error) {
              if (called) return
              setReason(error)
            }
          } else {
            setResult(result)
          }
        }
      }
      const reject = (reason) = > {
        if (this.status === CustomPromise.STATUS_MAP.Pending) {
          setReason(reason)
        }
      }

      const executor_ = executor.bind(null, resolve, reject) // Bind parameters to the actuator
      executor_() // Executor execution (synchronization)
    } catch (e) {
      if (this.status === CustomPromise.STATUS_MAP.Fulfilled || this.status === CustomPromise.STATUS_MAP.Rejected) return
      setReason(e)
    }

  }

  /** * then method *@param {function} onfulfilled 
   * @param {function} onrejected 
   * @returns * /
  then (onfulfilled, onrejected) {
    this.onfulfilled.push(onfulfilled)
    if (this.status === CustomPromise.STATUS_MAP.Fulfilled) { // Promise objects can still call the THEN method after the state is frozen
      this.onfulfilled.forEach((item, index) = > {
        if (item === onfulfilled) {
          this.excuteOnfulfilled(item, index, this.result)
        }
      })
    }
    this.onrejected.push(onrejected)
    if (this.status === CustomPromise.STATUS_MAP.Rejected) {
      this.onrejected.forEach((item, index) = > {
        if (item === onrejected) {
          this.excuteOnrejected(item, index, this.reason)
        }
      })
    }
    const customPromise = new CustomPromise((resolve, reject) = > {
      this.resolve.push(resolve)
      this.reject.push(reject)
    })
    this.promises.push(customPromise)
    return customPromise // The then method returns a new Promise object
  }

  This function will be fulfilled gradually. /** *@param {function} onfulfilled 
   * @param {number} index 
   * @param {*} result 
   */
  excuteOnfulfilled (onfulfilled, index, result) {
    if (typeof onfulfilled === 'function') {
      setTimeout(() = > {
        let x = null
        try {
          x = onfulfilled(result)
        } catch (error) {
          this.reject[index](error)
        }

        if (x === this.promises[index]) {
          this.reject[index](new TypeError('[onFulfilled] return the same value with [then] function'))}this.resolutionProcedure(x, this.promises[index], this.resolve[index], this.reject[index])
      }, 0)}else {
      if (this.status === CustomPromise.STATUS_MAP.Fulfilled) {
        setTimeout(() = > {
          this.resolve[index](result)
        }, 0)}}}/** * execute the onrejected function@param {function} onrejected 
   * @param {number} index 
   * @param {*} reason 
   */
  excuteOnrejected (onrejected, index, reason) {
    if (typeof onrejected === 'function') {
      setTimeout(() = > {
        let x = null
        try {
          x = onrejected(reason)
        } catch (error) {
          this.reject[index](error)
        }

        if (x === this.promises[index]) {
          this.reject[index](new TypeError('[onrejected] return the same value with [then] function'))}this.resolutionProcedure(x, this.promises[index], this.resolve[index], this.reject[index])
      }, 0)}else {
      if (this.status === CustomPromise.STATUS_MAP.Rejected) {
        setTimeout(() = > {
          this.reject[index](reason)
        }, 0)}}}/** * Promise process (focus) *@param {*} X the value returned by the resolvePromise function after execution@param {CustomPromise} The PROMISE * returned by the Promise THEN method@param {function} The callback function resolve * to the executor argument to the Promise returned by the resolve THEN method@param {function} Reject * callback to executor as the parameter to the Promise returned by the reject THEN method@returns * /
  resolutionProcedure (x, promise, resolve, reject) {
    if (x instanceof CustomPromise) {
      x.then(res= > {
        resolve(res)
      }, err= > {
        reject(err)
      })
    } else if(x ! = =null && (typeof x === 'function' || typeof x === 'object')) {
      let called = false
      try {
        const { then } = x // The value returned by the resolvePromise function is a Thenable object, and the then method is executed
        if (typeof then === 'function') {
          const then_ = then.bind(x)
          const resolvePromise = y= > {
            if (called) return // Make sure the resolvePromise is executed only once
            called = true
            // The value returned by the resolvePromise function is a Thenable object, if the resolvePromise parameter is called back after the then method is executed
            // continue to execute the Promise resolutionProcedure for the y parameter involved in the callback, that is, call the resolutionProcedure method
            this.resolutionProcedure(y, promise, resolve, reject)
          }
          const rejectPromise = r= > {
            if (called) return
            called = true
            reject(r)
          }
          then_(resolvePromise, rejectPromise)
        } else {
          resolve(x)
        }
      } catch (error) {
        if (called) return
        reject(error)
      }
    } else {
      resolve(x)
    }
  }

  /** * Static resolved method that returns a Promise that has been successful@param {*} result 
   * @returns * /
  static resolved (result) {
    return new CustomPromise((resolve, reject) = > {
      if(result ! = =null && (typeof result === 'function' || typeof result === 'object')) {
        let called = false
        try {
          const { then } = result
          if (typeof then === 'function') {
            const then_ = then.bind(result)
            then_(res= > {
              if (called) return
              called = true
              resolve(res)
            }, err= > {
              called = true
              reject(err)
            })
            
          } else {
            resolve(result)
          }
        } catch (error) {
          if (called) return
          reject(error)
        }
        
      } else {
        resolve(result)
      }
    })
  }

  /** * returns a Promise that has failed@param {*} result 
   * @returns * /
  static rejected (reason) {
    return new CustomPromise((resolve, reject) = > {
      reject(reason)
    })
  }

  / * * * *@returns Test with * /
  static deferred () {
    const result = {};
    result.promise = new CustomPromise(function(resolve, reject) {
      result.resolve = resolve;
      result.reject = reject;
    });
    returnresult; }}module.exports = CustomPromise;
Copy the code

Write Promise notes

List a typical standard code that uses promises, and some of the following terms will be subject to this

const p = new Promise((resolve, reject) = > {
  if (xxx) {
    resolve()
  } else {
    reject(new TypeError('error'))}})/ / thenable object
const thenable = (val) = > {
  return {
    then: (resolvePromise, rejectPromise) = > {
      // balabala
      rejectPromise(val)
    }
  }
}
const onFulfilled = (res) = > {
  return x
}
const onRejected = (err) = > {}
const p1 = p.then(onFulfilled, onRejected)
Copy the code

Here are a few points that I find easy to overlook when writing promises

  • PromiseThe internal state has a solidification effect and will not change once it is determined
  • PromiseThe constructor of is executed synchronously
  • PromiseThe inside of theresolveExecution is synchronous, butPromiseWhen used,resolveIt could be called synchronously or asynchronously, so be aware of that.resolveIf it’s called synchronously,thenWhen the method executes, execute it immediatelyonFulfilledAs well asonRejectedNow,resolveIf it’s called asynchronously,thenMethod is going to be executed firstonFulfilledAs well asonRejectedPut it away untilresolveWhen it is called.resolveYou can pass in athenableObject, if yesthenableObject that needs to perform the actions shown in the code below (calling it)thenMethods)
  • onFulfilledAs well asonRejectedinPromiseInternal needs to be called asynchronously (here let’s first directly understand as asynchronous can, in-depth talk also involvesMacro task micro task, interested in can go to understand, here I am direct usesetTimeoutImplement asynchronous, is can pass the test smoothly
  • thenCan be called multiple times (p.then(); p.then();), can also be called chained (p.then().then();), each time the THEN method returns a new onePromise, soPromiseInternal design preservationonFulfilledAs well asonRejectedThe data structure is an array
  • Very simple beep beepPromiseSolution process:thenMethod returns aPromisecallp1In returnthenthePromiseWhen we take the constructor argumentexecutorCallback functionresolveAs well asrejectPut it in storage.onFulfilledIf I return athenableObject (the one abovexIf notthenableObject directlyresolve(x)), to thisthenableObject performs the actions shown in the code below (calling it)thenMethods). ifresolvePromiseIt’s called, and the parameter we represent isyIf theyisthenableObject to continue the following operation (if not directly)resolve(y)), and so on, until you meetynotthenableObject (this is just my own simple understanding, if the expression see confused also please withPromises/A+Although I think that one might be more confusing…)
  • You need to deploy three static methods, staticresolvedMethod (can also be passed inthenableObject) to return a successfulPromise; staticrejectedMethod that returns a failed onePromiseThe staticdeferredMethod that returns an object containing (aPromiseObject, create thisPromiseObject is a constructor parameterexecutorCallback functionresolveAs well asreject).resolvedMethods andrejectedMethod deployment is not mandatory
  • ** Important: ** Constructor parametersexecutorCallback functionresolveAnd staticresolvedEither way is acceptablethenableObject as an argumentthenableObject performs the actions shown in the code below (calling it)thenMethod), this isPromises/A+What the specification does not specify, it is ignored in the first place, which leads to the failure of the test to proceed smoothly
const resolvePromise = (y) = > {
  // balabala
}
const rejectPromise = (err) = > {
  // balabala
}
thenable.then(resolvePromise, rejectPromise)
Copy the code

Write Promise in-line tests using promises-tests

The test steps are simple

  • Install dependencies
npm install promises-aplus-tests --save-dev
Copy the code
  • package.jsonJoin the script
"test": "Promises -aplus-tests < Your handwritten Promise JS path >"
Copy the code
  • Console input
npm run test
Copy the code
  • Waiting for the results…

Rest assured, it’s not going to be pretty at first, just like me… 😂

In the end 😉

Debug guide

If you are looking at the code over and over again, you will find that there are still a few errors in the code you are testing. If you are looking at the code, you will find that there is no danger of losing the test. If you are looking at the code, you will find that there is no danger of losing the test. You don’t know how to pass a test until you know what’s on it, do you? (As if the metaphor is strange 😅)

Promises – Tests repository address

Clone the project first and then open it to see what the entry is

"bin": "lib/cli.js".Copy the code

We openlib/cli.js

It turns out that its main function is right herelib/programmaticRunner.js

promises-testsUsing aMochaConducted tests in the test filelib/testsNext, he will read the test files and test them in turn

You can locate your own failed test cases based on the error message of the test

Most of my tests fell on 2.3.3.3. We opened 2.3.3 and found that it contains one main test case with three more test cases in it

Open them one by one until 2.3.3.3

Extract the main test code

  • PromiseTest.js
import thenables from './thenables';
import CustomPromise from '.. /promise/CustomPromise';

const resolved = CustomPromise.resolved;
const rejected = CustomPromise.rejected;

const sentinel = { sentinel: "sentinel" };
const dummy = { dummy: "dummy" };

export default function testMain () {
  function yFactory() {
    return thenables.fulfilled['an already-fulfilled promise'](thenables.fulfilled['an asynchronously-fulfilled custom thenable'](sentinel));
  }
  
  function xFactory() {
    return {
      then: function (resolvePromise) {
        constyFactory_ = yFactory() resolvePromise(yFactory_); }}; }const promise = resolved(dummy).then(function onBasePromiseFulfilled() {
    const xFactory_ = xFactory()
    return xFactory_;
  });
  
  const test = function (promise) {
    promise.then(function onPromiseFulfilled(value) {
      console.log('At last:', value);
    })
  }
  test(promise)
}
Copy the code
  • thenables.js
import CustomPromise from './CustomPromise';

var resolved = CustomPromise.resolved;
var rejected = CustomPromise.rejected;
var deferred = CustomPromise.deferred;

var other = { other: "other" }; // a value we don't want to be strict equal to

const fulfilled = {
    "a synchronously-fulfilled custom thenable": function (value) {
        return {
            then: function (onFulfilled) { onFulfilled(value); }}; },/ / a little
};

const rejected_ = {
    "a synchronously-rejected custom thenable": function (reason) {
        return {
            then: function (onFulfilled, onRejected) { onRejected(reason); }}; },/ / a little
};

export default {
    fulfilled: fulfilled,
    rejected: rejected_
}
Copy the code

This will be fulfilled fulfilled someday. In this case, if you can output {sentinel: “sentinel”}, it will mean that the test has passed. Then you can debug your program based on this small test code. 🤗

The meaning of a handwritten Promise

  • The first is being able to write by handPromiserightPromiseTo have a deeper understanding, like I had no idea beforeonFulfilledreturnthenableOf course, this implementation is still very rudimentary, such asES6 PromiseThe instancecatch,finally, static methodall,anyAs well asraceWe haven’t done that yet. We can add it later
  • Secondly, the interview is now easily build aircraft carriers, do not learn something 😷

Next up

If go online smoothly should push down my small program, welcome everyone to try 🤗