Handwritten Promise? ∑(っ ° Д °;) It's so simple

preface

Unknowingly, it has been delayed for five months. This year (lunar calendar) is really busy. I'm so busy that I'm not interested in blogging. Recently, on a whim, I realized a Promise according to my own ideas. I wrote it quickly and the code is quite simple. I'd like to record and share it as the opening blog post in 2022.

Initial realization

function getData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({code: 200, data: {desc: 'Hello world!'}})
        }, 2000)


        // setTimeout(() => {
        //     reject({code: 400, data: {desc: 'Error!'}})
        // }, 4000)
    })
}

getData().then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);
})

At first, I wanted to realize the above functions. Print {code: 200, data: {desc: 'Hello world!'} in two seconds. Then explore it with me bit by bit.

I started my blog in 2021 Simple implementation of two-way binding, life cycle and calculation properties in Vue!!! It was once written in: when we try to simulate an existing framework, we can infer how to design our code through the existing results. If we want to implement Promise, we need to analyze the basic expression of the original Promise. According to the above code:

  1. Implement a Promise function or class and receive a function as a parameter, and the function will provide two parameters. Both parameters are functions that can perform some operations
  2. The new Promise() object has then and catch methods and supports chained calls

Classes and callbacks

The description is still very simple. According to the above description, the basic prototype of Promise can be realized. First, realize the first one, as follows:

class SelfPromise {
    constructor(executor) {
        executor(val => {
            // Operation of resolve function
        },val => {
            // Operation of reject function
        })
    }
}

constructor receives an executor function and executes it. The executor function receives two parameters: the resolve function and the reject function. OK, the first item is completed.

Prototype based chain call

The second chain call is better realized, as follows:

class SelfPromise {
    constructor(executor) {
        executor(val => {
            // Operation of resolve function
        },val => {
            // Operation of reject function
        })
    }
    
    then() { return this }
    
    catch() { return this }
}

The instance object can call the method on the prototype. Here, this in the prototype method then is its caller new Promise(), which realizes the chain call.

Callback functions and asynchrony

OK, so how to print {code: 200, data: {desc: 'Hello world!'} in two seconds? Let's break it carefully in combination with the expression form of native Promise:

  1. The resolve function receives a parameter, the then function receives a callback function as the parameter, and the callback function receives the parameter passed in by resolve as the input parameter

The description here is rather convoluted, but it is true. The code implementation is as follows:

class SelfPromise {
    #resolveCallback = null

    constructor(executor) {
        executor(val => {
            // Operation of resolve function
            this.#resolveCallback && this.#resolveCallback(val)
        }, val => {
            // Operation of reject function
        })
    }

    then(callback) {
        this.#resolveCallback = callback
        return this
    }

    catch() {
        return this
    }
}

new SelfPromise((resolve) => {
    setTimeout(() => resolve('success'), 2000)
}).then(res => {
    console.log(res)
})

The above short code has realized the function of printing {code: 200, data: {desc: 'Hello world!'} in two seconds. You can directly copy the code and debug it in the browser.

The implementation principle is very simple:

Both then and catch functions are synchronous tasks. What is really asynchronous is their callback function.

Cache the input parameters of then in a private variable #resolveCallback, and the resolve function is called in a timer, even this# The resolveCallback = callback line cannot be executed until 2 seconds after it is executed.

Therefore, according to the above code, when the #resolveCallback is executed, the resolveCallback is ready to be called.

Pass the resolve input parameter to the #resolveCallback function and execute it. The function of printing {code: 200, data: {desc: 'Hello world!'}} in two seconds is realized. The #resolveCallback function here is executed asynchronously, which actually rubs the resolved car.

Concrete implementation

Execution status specification

The basic prototype has been realized. Next, I'll write a little specific. Here I refer to an online one standard:

Asynchronous operation "pending"
Asynchronous operation "resolved" (also known as fully)
Asynchronous operation "rejected"

There are only two ways to change these three states:
Asynchronous operation from incomplete to completed
Asynchronous operation from incomplete to failed

This change can only occur once. Once the current state changes to "completed" or "failed", it means that there will be no new state change. Therefore, there are only two final results of Promise objects:
The asynchronous operation succeeds. The Promise object returns a value and the status changes to resolved
The asynchronous operation fails. The Promise object throws an error and the status changes to rejected

According to the above specifications, the following codes are written:

class SelfPromise {
    #promiseState = 'pending'
    #resolveCallback = null
    #rejectCallback = null
    #finallyCallback = null

    static isFunc(func) {
        return typeof func === 'function'
    }

    constructor(executor = (resolve, reject) => {
    }) {
        executor(successValue => {
            // If the status is rejected, then does not execute
            if (this.#promiseState === 'rejected') return
            this.#promiseState = 'resolved'
            setTimeout(() => {
                SelfPromise.isFunc(this.#resolveCallback) && this.#resolveCallback(successValue)
                SelfPromise.isFunc(this.#finallyCallback) && this.#finallyCallback()
            })
        }, errorValue => {
            // If the status is resolved, catch is not executed
            if (this.#promiseState === 'resolved') return
            this.#promiseState = 'rejected'
            setTimeout(() => {
                SelfPromise.isFunc(this.#rejectCallback) && this.#rejectCallback(errorValue)
                SelfPromise.isFunc(this.#finallyCallback) && this.#finallyCallback()
            })
        })
    }

    then(callback) {
        this.#resolveCallback = callback
        return this
    }

    catch(callback) {
        this.#rejectCallback = callback
        return this
    }

    finally(callback) {
        this.#finallyCallback = callback
        return this
    }
}

Example code - 1

OK, according to the above specifications, we add an execution state to Promise, and supplement the catch and finally methods. Try with the following example code:

console.log('start')

const p = new SelfPromise((resolve, reject) => {
    console.log('promise')
    resolve('success')
}).then(res => {
    console.log(res, 'then');
}).catch(err => {
    console.log(err, 'catch');
}).finally(() => {
    console.log('finally')
})

setTimeout(() => {
    console.log('setTimeout')
})

console.log('end')

Look at the results:

Example code - 2

Nice! It is perfectly in line with the expression form of native Promise, but what if it is changed to the following?

console.log('start')

setTimeout(() => {
    console.log('setTimeout')
})

const p = new SelfPromise((resolve, reject) => {
    console.log('promise')
    resolve('success')
}).then(res => {
    console.log(res, 'then');
}).catch(err => {
    console.log(err, 'catch');
}).finally(() => {
    console.log('finally')
})

console.log('end')

According to the event loop mechanism in JavaScript, setTimeout is a macro task, new Promise() Then () is a micro task, which takes precedence over macro tasks. Here, I initially use setTimeout to simulate new Promise() The micro task of then () is placed under new promise () according to the first example code. The two timers have the same time. It is obvious that the two macro tasks will be executed from top to bottom according to the code order.

However, in the second example code, setTimeout is placed on new Promise(). If the timeout parameters are all 0, it is impossible to realize the expression of the native Promise, which does not realize the real micro task.

Realize real micro tasks

Micro task exploration

According to the above section, we know that only when the following code is executed in a micro task can we truly simulate the expression of the native Promise:

SelfPromise.isFunc(this.#resolveCallback) && this.#resolveCallback(successValue)
SelfPromise.isFunc(this.#finallyCallback) && this.#finallyCallback()

So what are the micro tasks in JavaScript?

process.nextTick NodeJS, PASS

Object.observe() is a method that has long been abandoned, PASS
MutationObserver listens to a specified dom and is called when changes occur. Huh? You need to create a dom element?? Do you want to add it to the page?? Will it affect performance??

es6-promise As the Polyfill Library of ES6 Promise, there is no doubt about the code quality and authority. Please refer to it here Its source code Confirm:

const BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;

function useMutationObserver() {
  var iterations = 0;
  var observer = new BrowserMutationObserver(flush);
  var node = document.createTextNode('');
  observer.observe(node, { characterData: true });

  return function () {
    node.data = iterations = ++iterations % 2;
  };
}

let scheduleFlush;
// Decide what async method to use to triggering processing of queued callbacks:
if (isNode) {
  scheduleFlush = useNextTick();
} else if (BrowserMutationObserver) {
  scheduleFlush = useMutationObserver();
} else if (isWorker) {
  scheduleFlush = useMessageChannel();
} else if (browserWindow === undefined && typeof require === 'function') {
  scheduleFlush = attemptVertx();
} else {
  scheduleFlush = useSetTimeout();
}

According to the above code, we can conclude that in addition to the NodeJS environment, MutationObserver is the first choice. In addition, even if setTimeout cannot be perfectly implemented, it is also regarded as the final alternative. That's because as long as the timeout value of setTimeout in the business code is greater than 1, you can also slightly simulate the expression of the native Promise (through the personal test in Google browser).

Micro task based on MutationObserver

combination MDN Learn the basic usage of MutationObserver in the document and write the following code:

function useMutationObserver(callback) {
    let iterations = 0;
    const observer = new MutationObserver(callback);
    const node = document.createTextNode('');
    // Set characterData to true to monitor changes in the character data contained in the specified target node or child node tree.
    observer.observe(node, {characterData: true});

    return function () {
        node.data = iterations = ++iterations % 2;
    };
}

console.log('start')

setTimeout(() => console.log('setTimeout'))

let microtask = useMutationObserver(() => {
    console.log('Hello MutationObserver!')
})
microtask()

console.log('end')

The printing results are as follows:

Amazing! With the help of the MutationObserver interface, the micro task call can be realized only when a blank text node is created, and the performance overhead is almost negligible ().

PS: node.data = iterations = ++iterations % 2; This line of code is a good programming skill.

Final implementation code

function useMutationObserver(callback) {
    let iterations = 0;
    const observer = new MutationObserver(callback);
    const node = document.createTextNode('');
    // Set characterData to true to monitor changes in the character data contained in the specified target node or child node tree.
    observer.observe(node, {characterData: true});

    return function () {
        node.data = iterations = ++iterations % 2;
    };
}

class SelfPromise {
    #promiseState = 'pending'
    #resolveCallback = null
    #rejectCallback = null
    #finallyCallback = null

    static isFunc(func) {
        return typeof func === 'function'
    }

    constructor(executor = (resolve, reject) => {
    }) {
        executor(successValue => {
            // If the status is rejected, then does not execute
            if (this.#promiseState === 'rejected') return
            this.#promiseState = 'resolved'
            useMutationObserver(() => {
                SelfPromise.isFunc(this.#resolveCallback) && this.#resolveCallback(successValue)
                SelfPromise.isFunc(this.#finallyCallback) && this.#finallyCallback()
            })()
        }, errorValue => {
            // If the status is resolved, catch is not executed
            if (this.#promiseState === 'resolved') return
            this.#promiseState = 'rejected'
            useMutationObserver(() => {
                SelfPromise.isFunc(this.#rejectCallback) && this.#rejectCallback(errorValue)
                SelfPromise.isFunc(this.#finallyCallback) && this.#finallyCallback()
            })()
        })
    }

    then(callback) {
        this.#resolveCallback = callback
        return this
    }

    catch(callback) {
        this.#rejectCallback = callback
        return this
    }

    finally(callback) {
        this.#finallyCallback = callback
        return this
    }
}

Try printing the following code again:

console.log('start')

setTimeout(() => {
    console.log('setTimeout')
})

const p = new SelfPromise((resolve, reject) => {
    console.log('promise')
    reject('error')
}).then(res => {
    console.log(res, 'then');
}).catch(err => {
    console.error(err, 'catch');
}).finally(() => {
    console.log('finally')
})

console.log('end')


100% restore ES6 native Promise execution results!!!

Postscript

In the first mock exam, it only wanted to print {code: 200, data: {desc:'Hello world, and so on after two seconds. It didn't expect that the result of native Promise simulation would be simulated one by one. Of course, there are many details that I haven't implemented in depth, and I haven't followed a more complete plan Promises/A+ Specification to write code.

However, deep thinking about some unknown things and solving them can really bring me great fun. Sorting out the pits I stepped on, the process of thinking and the ideas I learned into words to share, and can help others and bring me great satisfaction.

Knowledge is endless, programming ideas and creativity are endless! Both Vue and ES6 promise are created by the author's extensive and in-depth knowledge and his own thoughts. So, roll it up in 2022!!!

reference resources

ES6 promise source code

Keywords: Javascript Front-end Vue.js html

Added by rageh on Mon, 07 Mar 2022 02:16:00 +0200