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:
- 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
- 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:
- 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!!!