The topic of handwritten Promsie is no longer new, and today we will not continue to get tired. Let’s try something new: introduce and implement some core features of Promise syntactic level by combining ECMA Spec documents, and strive to make the language flat, simple and easy to understand

About this article

This is the first column of “Job-hopping plan in July”, I believe readers may understand the title, in order to meet the upcoming job-hopping in July, I will make some preparations, study or review the content, and then record these in this column, I will update regularly, welcome to pay attention to.

Recently, I happened to be looking at ECMA’s official specification document. After reading the part of Promise, I felt that I had gained a lot. So I realized the Promise and recorded my experience

How do I create a Promsie object

First, let’s talk about what a Promise object is, in authoritative terms: Promises are placeholders for the end result of delayed (possibly asynchronous) computations, so Promsie is used to handle delayed computations, and if you’re not going to handle delayed computations, then you’d better not use it.

ECMA Specification defines placeholder properties for Promise objects, including:

  • PromiseState, promsie status
  • PromiseResult, the end result of the promise
  • PromiseFulfillReactions, an array of callback functions (called when a Promise changes from Pending to Fufilled)
  • PromiseRejectReactions, an array of callback functions (invoked when promise changes from Pending to Rejected)

It is easy to define a Promsie constructor:

function Promise(executor){
    this.PromiseState = "pending";
    this.PromiseResult = undefined;
    this.PromiseFulfillReactions = [];
    this.PromiseRejectReactions = [];
    // Determine the executor type
    if(typeofexecutor ! = ='function') {throw "executor must be a function!"
    }
    / / to be perfect
    executor()
}
new Promise(function(res,rej){});Copy the code

Obviously, we also need to define parsing functions to handle PROMsie

Define the Promise resolution function

First, promsie is resolved in resolve and reject. Promsie can only be resolved in one or the other. Promsie cannot be resolved repeatedly. Also for the interpretation of the analytic function specification: CreateResolvingFunctions, this function accepts a promise object into the ginseng, promise with the analytic function binding, simple implementation:

Function createResolvingFunctions(promise){const AlreadyResolved = {value:false resolveFunction = (resolution)=>{ //avoid unnecessary invoke if(resolveFunction.AlreadyResolved.value){ return; } rejectFunction.AlreadyResolved.value = true; Const rejectFunction = (resolution)=>{// Avoid unnecessary invoke if(rejectFunction.AlreadyResolved.value){ return; } rejectFunction.AlreadyResolved.value = true; ResolveFunction. Promise = promise; rejectFunction.promise = promise; resolveFunction.AlreadyResolved = rejectFunction.AlreadyResolved = AlreadyResolved; return { resolve:resolveFunction, reject:rejectFunction } }Copy the code

Add the Promise constructor:

function Promise(executor){
    / / to omit...
    
    // Start refining
    const promsieResolveFns = createResolvingFunctions(this);
    executor(promsieResolveFns.resolve,promsieResolveFns.reject)
}
new Promise(function(res,rej){
    res("fulfilled")});Copy the code

When RES (” pity “) is called, promsie’s internal state should be updated. We will split this logic and create two functions responsible for FULFILL or rejectPromsie

Fulfill and reject functions

As the name implies, these are used to directly modify the internal state of a promise:

function fulfill(promsie,value){ if(promise.PromiseState === 'pending'){ //extrat reactions from promise const reactions  = promise.PromiseFulfillReactions; //update promise properties promise.PromiseState = "fulfilled"; promise.PromiseResult = value; promise.PromiseFulfillReactions = undefined; promise.PromiseRejectReactions = undefined; // Execute then functions, base on this change triggerReactions(reactions,value)}} //rejectCopy the code

Above there are two lines of interesting logic: promise. PromiseFulfillReactions and triggerReactions (reactions, value), they’re called promsie. Then method about:

  • When calling a inpendingStatus promsie.then(onFulfilled,onRejected)Method,onFulfilled.onRejectedDo not perform
  • When the promsie status changes tofulfilledorrejected, extract all registered callback functions and trigger execution in turn

To complete the analytic function:

Const resolveFunction = (resolution)=>{// omit... // const promise = resolvefunction. promise; Fulfill (promsie,resolution)} const rejectFunction = (resolution)=>{ Const promise = rejectfunction. promise; reject(promsie,resolution) }Copy the code

Now, let’s talk about the then method.

Every time dot then creates a new promise

According to the ECMA specification, each call to. Then creates a new PromiseCapability: it contains a new promsie and the resolution function associated with that promise. The factory function that creates it is:

function NewPromiseCapability(){
    const promsie = new Promise(() = >{});
    const resolvingFns = createResolvingFunctions(promise);
    return {
        promise,
        resolve:resolvingFns.resolve,
        reject:resolvingFns.reject
    }
}
Copy the code

Note that. Then calls can be made in one of two ways:

  • The promise is already calledfulfilledorrejected
  • The promise is in the callpending

You must consider both cases, and consider collecting callback functions for future execution:

Promise.prototype = {
    then(onFulfilled,onRejected){
        const promise = this;
        New PromiseCapability / /
        const capability = NewPromiseCapability();
        
        // Collect two types of callback functions and ancillary information
        const fulfillReaction = {
            Handler:onFulfilled,
            Type:'fulfill'.Capability:capability
        }

        const rejectReaction = {
            Handler:onRejected,
            Type:'reject'.Capability:capability
        }
        
        setTimeout(() = >{
            switch(promise.PromiseState){
                case "fulfilled":
                    executeReaction(fulfillReaction,promise.PromiseResult)
                    break;
                case "rejected":
                    executeReaction(rejectReaction,promise.PromiseResult)
                    break;
                default:
                    // Collect pending by default
                    promise.PromiseFulfillReactions.push(fulfillReaction);
                    promise.PromiseRejectReactions.push(rejectReaction);
                    break; }},0)
        // Return the newly created promsie
        returncapability.promise; }}Copy the code

The above code is interesting as you can see that we subcontract the execution with a setTimeout, because failure to do so would result in the following:

var promsie = fulfilledPromise.then(function(result){
    // The promsie is undefined
    return promsie;
})
Copy the code

The reason is very simple, ondepressing is not completed yet, so the return value is empty. So we synchronize the return first, then do the asynchronous judgment execution, and the problem is solved.

The value of the next step is how to access the promsie, congratulations eagle-eyed friend: executeReaction (fulfillReaction, promise. PromiseResult) :


function executeReaction(reaction,value){
    const capability = reaction.Capability;
    // Retrieve the callback function
    const handler = reaction.Handler;
    const type = reaction.Type;
    
    // Call this handler, at which point the first argument to your THEN has been called and the result recorded
    let handlerResult = handler(value);
    // Take the result of the above call (return value) as an input parameter to the resolve call of the new promsie in the then method
    capability.resolve(handlerResult);
}
Copy the code

In the last line we call capability.resolve(handlerResult), at which point you can continue the chain operation and access the value of handlerResult:

p.then((res)=>{ return "a new value" }).then(value=>{ console.log(value); //"a new value" })Copy the code

legacy

Although the skeleton has been built, there are still many problems:

  • PromiseSome other prototype methods (catch) and static methods ofresolve.all.race)
  • The code is basically synchronous (this is a bit awkward, can synchronous code handle asynchronous operations? Just kidding!)
  • The then method may return a new Promise object.
  • Didn’t doerror handling(This is the most basic, do not bother to handle errors or write code)
  • Didn’t do the tests

The first problem can be added slowly according to the situation, the other several problems are to be solved, otherwise how to change jobs?

  • This is a pity or onFulfilled, so that the synchronization code can be implemented with setTimeout. This will not affect the implementation of other synchronized code.
  • Error capture, try-catch handling of some key function calls
  • Type check, just make a few criteria for this

It took a lot of effort to finish the improvement, which brought the best of the key issues: testing

Annoying tests

In the beginning, I wrote a few simple test cases based on my experience with the original Promsie, based on the NodeJS environment, and the run got stuck with one problem: loop dependency.

The so-called circular dependency is the dependence between modules to form A closed loop, such as A-B-C-A, which will result in the null output of some modules.

When Google discovered that it was possible to add the Promsie constructor to Global to expose the Promsie constructor to memory, it temporarily solved the problem by adding the key module that caused the loop dependency to global runtime memory, and the test could proceed.

After basic testing, I came up with A test solution with Promsie A+.

Write an adaptor according to the instructions:

// Expose the Promise variable to global global
require(".. /index");
const {createResolvingFunctions} = require(".. /src/resolve")
module.exports = {
    resolved(value){
        return new Promise((res) = >{
            res(value)
        })
    },
    rejected(value){
        return new Promise((res,rej) = >{
            rej(value)
        })
    },
    deferred(){
        const promise = new Promise(() = >{});
        const resolveingFunctions = createResolvingFunctions(promise);
        return {
            promise,
            resolve:resolveingFunctions.resolve,
            reject:resolveingFunctions.reject
        }
    }
}
Copy the code

The introduction of adaptor

var promisesAplusTests = require("promises-aplus-tests");
const adapter = require("./adaptor")
promisesAplusTests(adapter, function (err) {
    // All done; output is in the console. Or check `err` for number of failures.
    console.log(err)
});
Copy the code

Run the test, the result is a big red, improve a bit, less red, improve and less red. After two days of on-off improvements (thumbing through its test cases), the results paid off:

Isn’t it wonderful?

Write in the last

After reading the ECMAScript® 2022 Language Specification for a while, it feels more like an implementation Specification, and while they claim to be technology-neutral, there is no doubt that reading this document will give you a deeper understanding of ECMA.

I realized it manually after reading the Promise part, which also gave me a deeper understanding and understanding of Promise, which is the root cause of this article. Although it will be very uncomfortable at the beginning, on the one hand, it is all In English, on the other hand, it has some obscure terms. But it’ll be fine when you get used to it.

Finally, due to space constraints, many complex concepts and implementations are not covered in this article, but are present in the code. All the code and test cases of the project have been hosted on Github. If you have any doubts or ideas about this article or Promise, please browse and discuss it.