Today, we’re going to write a promise by hand, but it’s not about the code, it’s about understanding promise in more depth.

Why is that? Because if we focus on “handwritten promise” this matter, may only benefit when taking an exam, and such rote writing method is easy to forget, but if we deeply grasp the promise, we will find that “handwritten promise” is a natural thing, will come naturally.

Promise is A grammar introduced by ES 6. Promises are Promises/A+.

Prior to writing this article, I didn’t refer to other written promise articles. I mostly referred to the Promise specification, the Promise section of JavaScript Advanced Programming, and the Promise section of JavaScript Language Essentials and Programming Practices. So, the answers given below may, in a sense, be less than standard. With that in mind, if you’re still interested, read on.

Let’s start by looking at Promise objects.

Typically, we would construct a Promise object like this:

const p = new Promise((resolve, reject) = > {
    resolve('hello world')})Copy the code

We use the new operator to create a new object. The constructor is of the following format:

declare function executor(resolve, reject) : void 
Copy the code

It is worth noting that this executor function will also be executed immediately whenever we construct a new Promise object. Because it’s used to set promise values.

A Promise object has three states: Pending, depressing, and Rejected.

As we all know, there is no way to access a promise object using JS to know what state it is currently in; the state of a promise is maintained internally. However, it’s up to us to change a promise state. That’s what the executor function does.

When initializing the Promise object, we can call resolve to change the state of the Promise to depressing, or call Reject to change the state of the Promise to Rejected.

const p = new Promise((resolve, reject) = > {
    resolve('hello world') // This is a big pity
})
Copy the code

In other words, executor is the built-in promise callback open to us, which communicates with the Promise object, even though we cannot read the state of the promise directly. And its two arguments, we can call them “valuers”.

This is the first point where we need to change our thinking. In the future, think of executor, the Promise constructor’s input parameter, as a function that interacts with the built-in Promise object. We change the promise state with resolve and reject.

Next, we examine the generated promise object P.

This object is also called the Thenable object. What is a Thenable object? The promise object is a built-in Thenable object. We can also write a Thenable object right away:

const thenableObj = {
    then: () = >{}}Copy the code

For p, it has a function then and takes two arguments. The usual expressions are:

p.then(
    function onfulfilled() {
        console.log('onfulfilled');
    },
    function onRejected() {
        console.log('onRejected'); });Copy the code

This is the pity/Rejected function. This is a big pity/Rejected, but I will not perform it immediately. There is a sentence in the specification:

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

OnFulfilled or onRejected ‘must not be called until the execution context stack contains only the platform code.

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, Or with a “micro-task” mechanism such as MutationObserver or process.nexttick. Since the promise implementation is considered platform code, It may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

The “platform code” here is the guidance engine, environment, and promise implementation code. In practice, this requirement ensures that the onFulfilled and onRejected are executed asynchronously, after the event loop of calling then, and the new stack is used. This can be done through “macro task” mechanisms, such as setTimeout or setImmediate, or using “microtask” mechanisms, such as MutationObserver or process.nexttick. Because the Promise implementation is considered platform code, it may itself contain a task scheduling queue, or “trampoline,” in which handlers are invoked.

This is a big pity/Rejected. This task will be added to the queue of asynchronous tasks when P meets the pity/Rejected function. As for when to pull out and execute, there is currently no platform code to execute. This is what we call the synchronization code in the execution context is done.

The specification doesn’t mandate whether promises are included as macro or microtasks, but browser-side implementations add microtasks.

The return value for THEN is also a Promise object, as well as then methods. And so on and so forth, and so on.

p.then().then().then()
Copy the code

As you can see, both functions accepted by THEN can be omitted. If THEN omits the function that handles the current PROMsie object state, the original promise state continues to be passed on. Take an example to illustrate the problem.

We initially generate a promsie that rejects the state, but then the promsie does not pass the onRejected function, and its state is passed later until it encounters the promsie that does.

Promise.reject(1)
    .then(() = > {
        console.log('Success only'); // This will not be printed
    })
    .then()
    .then(undefined.(err) = > {
        console.log(err); / / 1
    });
Copy the code

With that in mind, you’re ready to start implementing a first version of promise. As our first version of the implementation, we don’t care about the return value of the then function for the moment, because there are a lot of points to consider in the return value, so we implement the then method that generates a Promise object and then calls it.

Do it now, before it’s too late.

So let’s write our call code

const p = new MyPromise((resolve, reject) = > {
    setTimeout(() = > {
        resolve('hi')},1000)})console.log('a')
p.then((res) = > {
    console.log('c')
    console.log(res);
})
console.log('b')
Copy the code

The expected results are:

A b // one second later C hiCopy the code

Let’s implement the code against the usage:

type Executor<T> = (
    resolve: (value: T) => void, reject: (reason? :any) = >void
) = > void

type ThenCallbackQueueItem<T> = [OnFulfilled<T> | undefined, OnRejected | undefined]
type OnFulfilled<T> = (value: T | undefined) = > void;
type OnRejected = (reason: any) = > void

class MyPromise<T> {
    #status: 'pending' | 'fulfilled' | 'rejected' = 'pending';
    #value: T | undefined = undefined;
    #reason: any = undefined;
    #thenCallbackQueue: ThenCallbackQueueItem<T>[] = [];

    constructor(executor: Executor<T>) {
        executor(this.resolveFn.bind(this), this.rejectFn.bind(this));
    }
    
    // Ignore the return value of then for the moment, void for convenience
    // This will be changed in the next versionthen(onFulfilled? : OnFulfilled<T>, onRejected? : OnRejected):void {
        // The order in which promise then is executed is mount first
        // So we maintain a queue
        this.#thenCallbackQueue.push(
            [onFulfilled, onRejected]
        );

        // If the then method is called, the promise is already resolved
        // Execute all currently mounted callbacks
        if (this.isSettled()) {
            this.releaseThenCallbackInOrder(); }}resolveFn(value: T) {
        this.#status = 'fulfilled';
        this.#value = value;

        The current promise state is pending when calling THEN
        // The callback function received by then is queued
        // We will release the queue after it is resolved
        if(this.#thenCallbackQueue.length > 0) {
            this.releaseThenCallbackInOrder()
        }
    }

    rejectFn(reason: any) {
        this.#status = 'rejected';
        this.#reason = reason;

        if(this.#thenCallbackQueue.length > 0) {
            this.releaseThenCallbackInOrder()
        }
    }

    isSettled() {
        return this.#status ! = ='pending';
    }

    releaseThenCallbackInOrder() {
        // Add the then operation to the event loop
        setTimeout(() = > {
            this.#thenCallbackQueue.forEach(([onFulfilled, onRejected]) = > {
                if (this.#status === 'fulfilled') { onFulfilled? . (this.#value);
                } else{ onRejected? . (this.#reason); }});this.#thenCallbackQueue = []; }); }}Copy the code

Using the above implementation, our output is exactly what we expected:

So we have achieved the first version, since it is late, I will write here today, the rest will be updated tomorrow.

Finally, readers, it’s Christmas. Merry Christmas to you. 🎄 🎄 🎄

If you have someone to accompany, I sincerely hope you have a happy; If there is no one to accompany, then accept this Christmas tree I gave you, and I hope you will eventually find the one to accompany you.