Promise Advancement - How to Implement a Promise Library

Summary

It has been a long time since the Promise / A + specification was last updated. Previously, due to business needs, a Promise Library of TypeScript language was completed. This time, let's step by step introduce how we can implement a Promise library that conforms to Promise/A+.

If you don't know much about Promise/A + specifications, I suggest you read the last blog first.—— Front-end Basic Knowledge Reserve-Promise/A+Specification.

Implementation process

First, let's look at the following parts of the code in this Promise I implemented:

  • Global Asynchronous Function Actuator
  • Constants and attributes
  • Class method
  • Class static method

Through the above four parts, we can get a complete Promise. These four parts are related to each other. Next, let's look at each module.

Global Asynchronous Function Actuator

Prior to Promiz Source Code Analysis I mentioned in my blog how we can implement an asynchronous function executor. From the execution principle of JavaScript, we can know that if we want to implement asynchronous execution of related functions, we can choose to use macro task and micro task, which is also mentioned in the Promise/A+specification. Therefore, below we provide a macro task to achieve asynchronous function executor code for your reference.

let index = 0;

if (global.postMessage) {
    global.addEventListener('message', (e) => {
        if (e.source === global) {
            let id = e.data;
            if (isRunningTask) {
                nextTick(functionStorage[id]);
            } else {
                isRunningTask = true;

                try {
                    functionStorage[id]();
                } catch (e) {

                }
                isRunningTask = false;
            }

            delete functionStorage[id];
            functionStorage[id] = void 0;
        }
    });
}

function nextTick(func) {
    if (global.setImmediate) {
        global.setImmediate(func);
    } else if (global.postMessage) {
        functionStorage[++index] = func;
        global.postMessage(index, '*')
    } else {
        setTimeout(func);
    }
}

From the above code, we can see that we use setImmediate, postMessage, setTimeout three methods to add macro tasks to perform one-step function execution.

Constants and attributes

Having said how to execute asynchronous functions, let's look at the constants and attributes. Before implementing Promise, we need to define some constants and class attributes to store data later. Let's look at it one by one.

constant

Firstly, Promise has five states, which we need to define with constants, as follows:

enum State {
    pending = 0,
    resolving = 1,
    rejecting = 2,
    resolved = 3,
    rejected = 4
};

These five constants correspond to the five states in Promise respectively. I believe you can understand them from their names, so we won't talk more about them.

attribute

In Promise, we need some attributes to store data status and subsequent Proise references, as follows:

class Promise {
    private _value;
    private _reason;
    private _next = [];
    public state: State = 0;
    public fn;
    public er;
}

We describe the attributes one by one:

  • _ value, which is used to store the current value in the resolved state.
  • _ reason is used to store the current reason when rejected.
  • _ next, which means that the current Promise is followed by a reference to the then function.
  • fn, which represents the first callback function of the then method in the current Promise.
  • er, which represents the second callback function of the then method in the current Promise (that is, the first parameter of catch, as you can understand from the catch implementation method below).

Class method

Having looked at the properties of constants and classes, let's look at the static methods of classes.

Constructor

First, if we want to implement a Promise, we need a constructor to initialize the original Promise. The code is as follows:

class Promise {
    constructor(resolver?) {
        if (typeof resolver !== 'function' && resolver !== undefined) {
            throw TypeError()
        }


        if (typeof this !== 'object') {
            throw TypeError()
        }

        try {
            if (typeof resolver === 'function') {
                resolver(this.resolve.bind(this), this.reject.bind(this));
            }
        } catch (e) {
            this.reject(e);
        }
    }
}

From the Promise/A + specification, we can see that if resolver exists and is not a function, then we should throw an error; otherwise, we should pass resolve and reject methods to resolver as parameters.

resolve && reject

So what are resolve and reject methods for? These two methods are mainly used to make the current Promise transition state, that is, from pending state to resolving state or rejecting state. Let's look at the code in detail:

class Promise {
    resolve(value) {
        if (this.state === State.pending) {
            this._value = value;
            this.state = State.resolving;

            nextTick(this._handleNextTick.bind(this));
        }

        return this;
    }

    reject(reason) {
        if (this.state === State.pending) {
            this._reason = reason;
            this.state = State.rejecting;
            this._value = void 0;

            nextTick(this._handleNextTick.bind(this));
        }

        return this;
    }
}

As you can see from the above code, when a resolve or reject method is triggered, we all change the current status of the Proimse and invoke the _handleNextTick method asynchronously. The change of state indicates that the current Promise has changed from pending state to resolving or rejecting state, and the corresponding _value and _reson also indicate the data passed from the previous Promise to the next Promise.

So what does this _handleNextTick method do? In fact, the function of this method is very simple, which is used to deal with the callback functions fn and er passed in by the then function following the current Promise.

then && catch

Before we get to know _handleNextTick, let's look at the implementation of the then function and the catch function.

class Promise {
    public then(fn, er?) {
        let promise = new Promise();
        promise.fn = fn;
        promise.er = er;

        if (this.state === State.resolved) {
            promise.resolve(this._value);
        } else if (this.state === State.rejected) {
            promise.reject(this._reason);
        } else {
            this._next.push(promise);
        }

        return promise;
    }

    public catch(er) {
        return this.then(null, er);
    }
}

Because the call to the catch function is an alias for the then function, let's just discuss the then function.

When the then function executes, we create a new Promise, and then save the two incoming callback functions with the properties of the new Promise. Then, we first determine the current status of Promise. If it is resolved or rejected, we immediately call the resolve or reject method in the new Promise to pass the _value or _reason of the current Promise to the next Promise and trigger the state change of the next Promise. If the current Promise state is still pending, save the newly generated Promise and trigger the new Promise change after the current Promise state changes. Finally, we return the example of Promise.

handleNextTick

After looking at the then function, we can look at the handleNextTick function we mentioned.

class Promise {
    private _handleNextTick() {
        try {
            if (this.state === State.resolving && typeof this.fn === 'function') {
                this._value = this.fn.call(getThis(), this._value);
            } else if (this.state === State.rejecting && typeof this.er === 'function') {
                this._value = this.er.call(getThis(), this._reason);
                this.state = 1;
            }
        } catch (e) {
            this.state = State.rejecting;
            this._reason = e;
            this._value = void 0;
            this._finishThisTypeScriptPromise();
        }
        
        // if promise === x, use TypeError to reject promise
        // If promise and x point to the same object, type Error is used as a reason to reject promise
        if (this._value === this) {
            this.state = State.rejecting;
            this._reason = new TypeError();
            this._value = void 0;
        }
        
        this._finishThisTypeScriptPromise();
    }
}

Let's start with a simple version of the _handleNextTick function, which can help us quickly understand the main process of Promise.

When the _handleNextTick function is triggered asynchronously, we can judge the current user's state. If the current Promise is resolving, we will call the fn function, which we set for the new Promise when the function is called. If the current Promise is rejecting, we will call the er function.

The getThis method mentioned above is used to get a specific value of this, and the specifications require that we introduce it later.

By executing these two synchronized fn or er functions, we can get the value of the current Promise after executing the incoming callback. What we need to explain here is that before we execute fn or er functions, the values we store in _value and _reason are the values passed down from the last Promise. Only when the fn or ER functions are executed, the values stored in _value and _reason are the values we need to pass to the next Promise.

It may be surprising that our this point has not changed, but why does our this point to the new Promise instead of the original one?

We can look at the problem from another perspective: is our current Promise generated by the last Promise? If this is the case, we can understand that when the last Promise generated the current Promise, two functions fn and er were set.

You may ask, then, how did the fn and er parameters of our first Promise come from?

Then we need to take a closer look at the above logic. Let's just discuss the first case where Promise is pending, and the rest is basically the same. The first Promise does not have the previous Promise to set the fn and er parameters, so the value of the two parameters is undefined. So in the above logic, we have ruled out this situation and went directly into the _finishThisTypeScriptPromise function.

What we need to specify here is that some people will think that when we call the two callback functions fn and ER passed in by the then function, the current Promise will end. In fact, this is not the case. We get the return values of the two functions fn or er, and then pass the values to the next Promise, the last Promise will end. For this logic, we can look at the _finishThisTypeScriptPromise function.

finishThisTypeScriptPromise

_ The code for the finishThisTypeScriptPromise function is as follows:

class Promise {
    private _finishThisTypeScriptPromise() {
        if (this.state === State.resolving) {
            this.state = State.resolved;

            this._next.map((nextTypeScriptPromise) => {
                nextTypeScriptPromise.resolve(this._value);
            });
        } else {
            this.state = State.rejected;

            this._next.map((nextTypeScriptPromise) => {
                nextTypeScriptPromise.reject(this._reason);
            });
        }
    }
}

From the _finishThisTypeScriptPromise function, we can see that after we get the _value or _reason that needs to be passed to the next Promise, we use the map method to call the newly generated Promise instance and its resolve method one by one, so we trigger the state of the Promise from pending to resolving or rejecting.

By this point, we have fully understood the entire life cycle of a Promise from its inception to its end. Now let's look at the handling of some branch logic mentioned in the Promise/A + specification.

The value passed by the last Promise is the Thenable instance

First, let's look at what the Thenable example is. The Thenable instance refers to an object with the then function in the attribute. Promise is a special Thenable object.

Next, in order to facilitate the explanation, we will use Promise instead of Thenable to explain, other Thenable classes you can refer to similar ideas for analysis.

If we are a Promise instance in the _value passed to us, then we must wait for the incoming Promise state to be converted to resolved before the current Promise can continue to execute, that is, when we get a non-Thenable return value from the incoming Promise, we can use this value to call the fn or er method in the attribute.

So, how do we get the return value of the incoming Promise? A very clever method is used in Promise: because there is a then function (Thenable definition) in the incoming Promise, we call the then function and pass in the fetch_value in the first callback function fn to trigger the current Promise to continue execution. If the second callback function er is triggered, the _reason obtained in er is used to reject the current Promise. The concrete judgment logic is as follows:

class Promise {
    private _handleNextTick() {
        let ref;
        let count = 0;

        try {
            // Determine whether the incoming this._value is a thanable
            // check if this._value a thenable
            ref = this._value && this._value.then;
        } catch (e) {
            this.state = State.rejecting;
            this._reason = e;
            this._value = void 0;

            return this._handleNextTick();
        }

        if (this.state !== State.rejecting && (typeof this._value === 'object' || typeof this._value === 'function') && typeof ref === 'function') {
            // add a then function to get the status of the promise
            // Add a then function after the original TypeScriptPromise to determine the status of the original promise

            try {
                ref.call(this._value, (value) => {
                    if (count++) {
                        return;
                    }

                    this._value = value;
                    this.state = State.resolving;
                    this._handleNextTick();
                }, (reason) => {
                    if (count++) {
                        return;
                    }

                    this._reason = reason;
                    this.state = State.rejecting;
                    this._value = void 0;
                    this._handleNextTick();
                });
            } catch (e) {
                this.state = State.rejecting;
                this._reason = e;
                this._value = void 0;
                this._handleNextTick();
            }
        } else {
            try {
                if (this.state === State.resolving && typeof this.fn === 'function') {
                    this._value = this.fn.call(getThis(), this._value);
                } else if (this.state === State.rejecting && typeof this.er === 'function') {
                    this._value = this.er.call(getThis(), this._reason);
                    this.state = 1;
                }
            } catch (e) {
                this.state = State.rejecting;
                this._reason = e;
                this._value = void 0;
                this._finishThisTypeScriptPromise();
            }

            this._finishThisTypeScriptPromise();
        }
    }
}

promise === value

In the Promise/A+specification, if the return value of _value equals the user itself, the current Promise is rejected by a TypeError error. So we need to add the following judgment code in _handleNextTick:

class Promise {
        private _handleNextTick() {
        let ref;
        let count = 0;

        try {
            // Determine whether the incoming this._value is a thanable
            // check if this._value a thenable
            ref = this._value && this._value.then;
        } catch (e) {
            this.state = State.rejecting;
            this._reason = e;
            this._value = void 0;

            return this._handleNextTick();
        }

        if (this.state !== State.rejecting && (typeof this._value === 'object' || typeof this._value === 'function') && typeof ref === 'function') {
            // add a then function to get the status of the promise
            // Add a then function after the original TypeScriptPromise to determine the status of the original promise
            
            ...

        } else {
            try {
                if (this.state === State.resolving && typeof this.fn === 'function') {
                    this._value = this.fn.call(getThis(), this._value);
                } else if (this.state === State.rejecting && typeof this.er === 'function') {
                    this._value = this.er.call(getThis(), this._reason);
                    this.state = 1;
                }
            } catch (e) {
                this.state = State.rejecting;
                this._reason = e;
                this._value = void 0;
                this._finishThisTypeScriptPromise();
            }

            // if promise === x, use TypeError to reject promise
            // If promise and x point to the same object, type Error is used as a reason to reject promise
            if (this._value === this) {
                this.state = State.rejecting;
                this._reason = new TypeError();
                this._value = void 0;
            }

            this._finishThisTypeScriptPromise();
        }
    }
}

getThis

In the Promise/A+specification, it is stipulated that the direction of this is limited when we call fn and er callback functions. In strict mode, the value of this should be undefined; in loose mode, the value of this should be global.

Therefore, we also need to provide a getThis function to handle the above situation. The code is as follows:

class Promise {
    ...
}

function getThis() {
    return this;
}

Class static method

Through the class method mentioned above and the logical processing of some specific branches, we have implemented a Promise class that conforms to the basic functions. Now let's look at some of the standard APIs provided in ES6 and how we can implement them. Specific APIs are as follows:

  • resolve
  • reject
  • all
  • race

Now let's look at one way or another.

resolve && reject

First, let's look at the simplest solution and reject methods.

class Promise {
    public static resolve(value?) {
        if (TypeScriptPromise._d !== 1) {
            throw TypeError();
        }

        if (value instanceof TypeScriptPromise) {
            return value;
        }

        return new TypeScriptPromise((resolve) => {
            resolve(value);
        });
    }

    public static reject(value?) {
        if (TypeScriptPromise._d !== 1) {
            throw TypeError();
        }

        return new TypeScriptPromise((resolve, reject) => {
            reject(value);
        });
    }
}

From the above code, we can see that the resolve and reject methods basically use the internal constructor method to build Promise directly.

all

class Promise {
    public static all(arr) {
        if (TypeScriptPromise._d !== 1) {
            throw TypeError();
        }

        if (!(arr instanceof Array)) {
            return TypeScriptPromise.reject(new TypeError());
        }

        let promise = new TypeScriptPromise();

        function done() {
            // Statistics on how many incomplete TypeScript Promises remain
            // count the unresolved promise
            let unresolvedNumber = arr.filter((element) => {
                return element && element.then;
            }).length;

            if (!unresolvedNumber) {
                promise.resolve(arr);
            }

            arr.map((element, index) => {
                if (element && element.then) {
                    element.then((value) => {
                        arr[index] = value;
                        done();
                        return value;
                    });
                }
            });
        }

        done();

        return promise;
    }
}

Let's briefly discuss the basic idea of all function according to the above code.

First, we need to create a new Promise for return, to ensure that later users can set up the fn and er callback functions of the new Promise when they call the then function for subsequent logical processing.

Then, how do we get the value of each Promise in the Promise array above? The method is simple, as we mentioned earlier: we call the then function of each Promise to get the current value of the Promise. Also, when each Promise is completed, we check whether all Promises have been completed, and if so, trigger the transition from pending to resolving or rejecting.

race

class Promise {
    public static race(arr) {
        if (TypeScriptPromise._d !== 1) {
            throw TypeError();
        }

        if (!(arr instanceof Array)) {
            return TypeScriptPromise.reject(new TypeError());
        }

        let promise = new TypeScriptPromise();

        function done(value?) {
            if (value) {
                promise.resolve(value);
            }

            let unresolvedNumber = arr.filter((element) => {
                return element && element.then;
            }).length;

            if (!unresolvedNumber) {
                promise.resolve(arr);
            }

            arr.map((element, index) => {
                if (element && element.then) {
                    element.then((value) => {
                        arr[index] = value;
                        done(value);
                        return value;
                    });
                }
            });
        }

        done();

        return promise;
    }
}

race's thinking is basically the same as all's. It's just that we're different in processing functions. Whenever we detect that one of the Promise s in the array has been converted to a resolve or rejected state (judged by the absence of the then function), we immediately start to convert the state of the newly created Proise example from pending to resolving or rejecting.

summary

We introduce Promise's asynchronous function executors, constants and attributes, class methods, class static methods one by one, so that you can have a deep understanding and understanding of the whole Promise's construction and declaration cycle. Some key points and details that need to be noticed in the whole development process are also explained above. All you need to do is follow this line of thought and compare it. Promise/A+Specification A specification-compliant Promise library can be completed.

Finally, I would like to provide you with one. Promise/A+Test Tool After you have implemented your Promise, you can use this tool to test whether it fully conforms to the entire Promise/A + specification. Of course, if you want to use my off-the-shelf code, you are also welcome to use my code. Github/typescript-proimse.

Tags: Javascript TypeScript Attribute REST

Posted on Sat, 18 May 2019 04:31:50 -0700 by oscar2