Research on the principle of JS native method (10): how to realize promise / A + by handwriting and its methods?

Time:2021-9-18

This is the tenth article in the JS native method principle Exploration Series. This article describes how to write a matchPromise a + specificationPromise, and the related methods of realizing promise.

Implement promise / A+

term

In order to better read this article, first agree on some terms and expressions:

Research on the principle of JS native method (10): how to realize promise / A + by handwriting and its methods?

  • Promise is in the pending state when it is in the initial state; It can be settled in the resolved state (fully completed state), and its resolved value is represented by value; It can also be set to the rejected state, and its rejected value is represented by reason.
  • The successful callback function accepted by the then method is called onfulfilled, and the failed callback function is called onrejected

Implement promise constructor

We first try to implement a basic promise constructor.

First, the state of promise instance is represented by three constants:

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

The promise constructor is used to create a promise instance. For a promise instance, it has several basic attributes: status records the state of promise (initially pending), value records the value of promise resolve (initially null), and reason records the value of promise reject (initially null).

We define them in promise constructor:

function Promise(){
    this.status = PENDING
    this.value = null
    this.reason = null
}

When new calls the promise constructor, an executor function executor will be passed into the constructor. The executor function will be executed immediately, and it itself accepts the remove function and reject function as parameters. The resolve function and reject function are responsible for actually changing the state of promise. Their call timing depends on the logic of the executor function defined by the developer. We only need to write the code to call the executor function.

Therefore, the code is further expanded as follows:

function Promise(executor){
    //Save the reference of the promise instance for easy access in the resolve function and reject function
    let self = this
    self.status = PENDING
    self.value = null
    self.reason = null
    //Define the resolve function
    function resolve(){ ... }
    //Define reject function
    function reject(){ ... }
    //Call executor function
    executor(resolve,reject)
}

The resolve function and reject function accept value and reason as parameters respectively, and change the promise state based on these two values. However, in order to ensure the irreversibility of the promise state, the promise state can be modified only when it is determined that the promise state is pending. Therefore, the resolve function and reject function are defined as follows:

function resolve(value){
    if(self.status === PENDING){
        self.status = FULFILLED
        self.value = value
    }
}
function reject(reason){
    if(self.status === PENDING){
        self.status = REJECTED
        self.reason = reason
    }
}

The developer will pass in a custom executor to the promise constructor, and the executor may call resolve or reject to finally create a promise instance with a settled state. But according to the code,The executor itself may throw an exception when executing, if so, you need to catch the exception and return a promise instance that rejects the exception. Therefore, the modified code is as follows:

function Promise(executor){
    let self = this
    
    self.status = PENDING
    self.value = null
    self.reason = reason
    
    function resolve(value){
        if(self.status === PENDING){
            self.status = FULFILLED
            self.value = value
        }
    }
    function reject(reason){
        if(self.status === PENDING){
            self.status = REJECTED
            self.reason = reason
        }
    }
    //Catch possible exceptions when calling the executor
    try{
        executor(resolve,reject)
    } catch(e) {
        reject(e)
    }
}

Then method to implement promise instance

1) Preliminary implementation of then method

All promise instances can call the then method. The then method is responsible for further processing the promise whose state is settled. It accepts the success callback function onfulfilled and the failure callback function onrejected as parameters, while onfulfilled and onrejected accept the value and reason of promise as parameters respectively.

The then method is always executed synchronously, there will be different processing logic according to the promise state when executing the then method:

(1) If promise is in the resolved state, the onfulfilled function is executed

(2) If promise is in the rejected state, the onrejected function is executed

(3) If the promise is in the pending state, the onfulfilled function and onrejected function will not be executed temporarily. Instead, these two functions will be put into a cache array respectively. When the promise state is settled in the future, the corresponding callback function will be taken from the array for execution

be careful:In fact, onfulfilled and onrejected are executed asynchronously, but for now we think they are executed synchronously)

In either case, a new promise instance will eventually be returned after calling the then method, which is a key to the chain call of the then method.

According to the above statement, the preliminarily implemented then method is as follows:

Promise.prototype.then = function (onFulfilled,onRejected) {
    //Because the promise instance calls the then method, this points to the instance, which is saved here for future use
    let self = this
    //Promise finally returned
    let promise2
    //1) if it is in full status
    if(self.status === FULFILLED){
        return promise2 = new Promise((resolve,reject) => {
            onFulfilled(self.value)
        })
    }
    //2) if the status is rejected
    else if(self.status === REJECTED){
        return promise2 = new Promise((resolve,reject) => {
            onRejected(self.reason)
        })
    }
    //3) if it is in pending status
    else if(self.status === PENDING){
        return promise2 = new Promise((resolve,reject) => {
            self.onFulfilledCallbacks.push(() => {
                onFulfilled(self.value)
            })
            self.onRejectedCallbacks.push(() => {
                onRejected(self.reason)
            })
        })
    }
}

2) Modify promise constructor: add two cache arrays

As you can see, there are two more cache arrays:onFulfilledCallbacksandonRejectedCallbacks, they are actually mounted on the promise instance, so modify the promise constructor:

function Promise(executor){
    //... omit other code
    //Add two cache arrays
    self.onFulfilledCallbacks = []
    self.onRejectedCallbacks = []
}

3) Modify the resolve and reject functions: execute the callback function in the cache array

When the then method is executed, the state of promise has not been determined. We do not know which callback function should be executed, so we choose to store the successful callback and failed callback into the cache array first. So when should the callback function be executed? It must be when the promise state is settled, and because the promise state depends on the resolve function and reject function, the execution time of these two functions is the execution time of the callback function in the cache array.

Modify the resolve function and reject function:

function resolve(value){
    if(self.status === PENDING){
        self.status = FULFILLED
        self.value = value
        //Traverse the cache array and fetch all successful callback functions for execution
        self.onFulfilledCallbacks.forEach(fn => fn())
    }
}
function reject(reason){
    if(self.status === PENDING){
        self.status = REJECTED
        self.reason = reason
        //Traverse the cache array and fetch all successful callback functions for execution
        self.onRejectedCallbacks.forEach(fn => fn())
    }  
}

4) Improved then method: exception capture

According to the specification, when executing a successful callback or a failed callback, the callback itself may throw an exception. If so, you need to catch the exception and finally return a promise instance that rejects the exception.

Therefore, on all places where callbacks are performedtry...catch, the method of improving then is as follows:

Promise.prototype.then = function (onFulfilled,onRejected) {
    //Because the promise instance calls the then method, this points to the instance, which is saved here for future use
    let self = this
    //Promise finally returned
    let promise2
    if(self.status === FULFILLED){
        return promise2 = new Promise((resolve,reject) => {
            try {
                onFulfilled(self.value)
            } catch (e) {
                reject(e)
            }
        })
    }
    else if(self.status === REJECTED){
        return promise2 = new Promise((resolve,reject) => {
            try {
                onRejected(self.reason)
            } catch (e) {
                reject(e)
            }
        })
    }
    else if(self.status === PENDING){
        return promise2 = new Promise((resolve,reject) => {
            self.onFulfilledCallbacks.push(() => {
                try {
                    onFulfilled(self.value)
                } catch (e) {
                    reject(e)
                }
            })
            self.onRejectedCallbacks.push(() => {
                 try {
                    onRejected(self.reason)
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
}

5) Improved then method: value penetration

Sometimes, parameters of function type may not be passed to the then method, or no parameters may be passed at all, such as:

//Pass in parameters of non function type
Promise.reject(1).then(null,{}).then(null,err => {
    Console.log (ERR) // still print normally 1
})

//No parameters passed
Promise.resolve(1).then().then(res => {
    Console.log (RES) // still print normally 1
})

But even so, the value or reason of the initial promise can still penetrate the then method and pass down. This is the characteristic of promise value penetration. To implement this feature, you can actually judge whether the parameter passed to the then method is a function. If not (including the case of no parameter passed), you can customize a callback function:

  • Onfulfilled if it is not a function: define a function that returns value, pass value down, and capture it by the subsequent successful callback
  • Onrejected if it is not a function: define a function that throws reason, pass the reason down, and catch it by the subsequent failed callback

Therefore, the improvement method of then is as follows:

Promise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? 
        onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? 
        onRejected : reason => { throw reason }
    //... omit other code
}

6) Improve the then method: determine the return value of the then method

So far, we have not implemented the most critical logic, that is, to determine the return value of the then method — although the previous code has asked the then method to return a promise, we have not determined the state of the promise.

The state of promise returned after calling then depends on the return value of the callback function, the logic of this part is complex. We will use a resolvepromise function to handle it separately, and the then method is only responsible for calling this method.

The improvement methods are as follows:

Promise.prototype.then = function (onFulfilled,onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? 
        onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? 
        onRejected : reason => { throw reason }
    
    let self = this
    
    //Promise finally returned
    let promise2
    if(self.status === FULFILLED){
        return promise2 = new Promise((resolve,reject) => {
            try {
                let x = onFulfilled(self.value)
                //Handle the return value of then with resolvepromise
                resolvePromise(promise2,x,resolve,reject)
            } catch (e) {
                reject(e)
            }
        })
    }
    else if(self.status === REJECTED){
        return promise2 = new Promise((resolve,reject) => {
            try {
                let x = onRejected(self.reason)
                resolvePromise(promise2,x,resolve,reject)
            } catch (e) {
                reject(e)
            }
        })
    }
    else if(self.status === PENDING){
        return promise2 = new Promise((resolve,reject) => {
            self.onFulfilledCallbacks.push(() => {
                try {
                    let x = onFulfilled(self.value)
                    resolvePromise(promise2,x,resolve,reject)
                } catch (e) {
                    reject(e)
                }
            })
            self.onRejectedCallbacks.push(() => {
                 try {
                    let x = onRejected(self.reason)
                    resolvePromise(promise2,x,resolve,reject)
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
}

Implement the resolvepromise method

Always remember that the goal of resolvepromise isDetermine the state of promise returned after calling then based on the return value of the callback functionTherefore, the resolve call or reject call in resolvepromise will determine the final returned promise state

1) General idea

The general idea of implementing the resolvepromise method is as follows:

  1. First, judge whether the return value x of the callback function is equal to the return value PROMISE2 after calling then. If it is equal, a reject is directly returned, and the reason is a typeerror. This is because the state of promise 2 depends on X. if they are the same object, it needs to determine its own state, which is impossible.

    //This will result in an error, because the return value of then is equal to the return value of the callback function
    let p = Promise.resolve(1).then(res => p)
  2. Then judge whether x is a non null object or function:

    1. If not: then x can never be a thenable. At this time, resolve x directly
    2. If so, judge againx.thenIs it a function:

      1. If yes, then x is a thenable. Continue processing later
      2. If not: then x is a non thenable object or function, just resolve X

The code implemented according to this idea is as follows:

function resolvePromise(promise2,x,resolve,reject){
    //If PROMISE2 and X are the same object, it will cause an endless loop
    if(promise2 === x){
        return reject(new TypeError('Chaining cycle'))
    }
    //If x is an object or function
    if(x !== null && typeof x === 'object' || typeof x === 'function'){
        //If x is a thenable
        if(typeof x.then === 'function'){
            //... continue processing
        } 
        //Otherwise
        else {
            resolve(x)
        }
    }
    //Otherwise
    else {
        resolve(x)
    }
}

2) How to deal with the case where x is thenable

If x is a thenable (including the case where x is promise), what should be done? Let’s take a look at an example:

let p1 = Promise.resolve(1).then(res => {
    return new Promise((resolve,reject) => {
        resolve(2)
    })
})
let p2 = Promise.resolve(1).then(res => {
    return new Promise((resolve,reject) => {
        reject(2)
    })
})
//The results of printing P1 and P2 are:
Promise <fulfilled> 2
Promise <rejected> 2

It can be seen that when the return value x of the callback function is thenable, the promise returned after calling then will follow the value or reason of X.

So what we have to do is actually very simple, that is, after judging that x is a thenable,Immediately call its then method, and pass in resolve and reject as success callbacks and failure callbacks。 No matter whether the state of X is settled or not, it will always call resolve or reject based on its own state at some time, and will also pass in the value or reason of X, which is equivalent to our callingresolve(value)perhapsreject(reason)Therefore, the state of promise returned after calling then can be determined.

But here’s a problem, consider the following code:

let p1 = Promise.resolve(1).then(res => {
    return new Promise((resolve,reject) => {
        resolve(new Promise((resolve,reject) => {
            resolve(2)
        }))
    })
})
let p2 = Promise.resolve(1).then(res => {
    return new Promise((resolve,reject) => {
        reject(new Promise((resolve,reject) => {
            resolve(2)
        }))
    })
})
//The results of printing P1 and P2 are:
Promise { <fulfilled> : 2}
Promise { <rejected> : Promise }

The difference here is that although the callback function also returns a promise, the internal resolve of the promise is still a promise. If you call directly according to the previous statementresolve(value), the final returned promise is a promise of resolve promise, but in fact, it should be a promise of resolve innermost layer value (2 in this example). Therefore, it cannot be used directly hereresolve(value)Instead, it should be called recursivelyresolvePromise(promise2,value,resolve,reject)Until the base value of the innermost layer is found as the value of the final resolve.

However, if the internal reject of the promise returned by the callback function is still a promise, the final returned promise is also a reject promise. In this case, there is no need to recursively call to find the basic value of the innermost layer.

Therefore, the code of this part is as follows:

function resolvePromise(promise2,x,resolve,reject){
    if(promise2 === x){
        return reject(new TypeError('Chaining cycle'))
    }
    if(x !== null && typeof x === 'object' || typeof x === 'function'){
        if(typeof x.then === 'function'){
            x.then(
                 (y) => {
                    resolvePromise(promise2,y,resolve,reject)
                },
                (r) => {
                    reject(r)
                })
        }  else {
            resolve(x)
        }
    }  else {
        resolve(x)
    }
}

3) Other points needing attention

Referring to the specification, we can find that our resolvepromise function still needs to be improved:

1) Promise has many different versions of implementations, and their specific behavior may be different. In order to ensure that different versions of promise implementations can interoperate and improve compatibility, some special cases will be handled in the resolvepromise method. include:

  • The then attribute of X may beObject.definePropertyA getter is defined, and an exception will be thrown every time you get. So you need to try to get it firstx.thenAnd catch possible exceptions — once caught, reject the exception (which means that the final return is a promise to reject the exception)
  • When calling then, it will not passx.thenCall, but throughthen.call(x)Call. Why? First of all, we have passed in frontlet then = x.thenGet the reference of the then method, so the consideration here is not to get it again, but to use it directlythenVariable, but direct use will cause it to lose this point, so you need to usecallBind this to X.

2) The successful callback and failed callback passed to then may be executed multiple times. If so, the callback executed first shall prevail, and other executions will be ignored. Therefore, variables are usedcalledIndicates whether a callback has been executed

3) An exception may also be thrown when calling then. If so, reject the exception. However, if a successful callback or a failed callback has been called when an exception is caught, you do not need to reject.

According to the points mentioned above, the final resolvepromise function is as follows:

function resolvePromise (promise2,x,resolve,reject) {
    if(promise2 === x){
        return reject(new TypeError('Chaining cycle'))
    }
    if(x !== null && typeof x === 'object' || typeof x === 'function'){
        let then
        let called = false
        try {
            then = x.then
        } catch (e) {
            return reject(e)
        }
        if (typeof then === 'function') {
           try {
               then.call(x,(y) => {
                   if(called) return 
                   called = true
                   resolvePromise(promise2,y,resolve,reject)
               },(r) => {
                   if(called) return 
                   called = true
                   reject(r)
               })
           } catch (e) {
               if(called) return
               reject(e)
           }
        } else {
            resolve(x)
        }
    } else {
        resolve(x)
    }
}

Implement asynchronous execution of callback function

Finally, you should also pay attention to the execution timing of then’s callback function.

If you only look at the implementation of the previous code, you will think that when the promise state is settled, executing then will synchronously execute the callback inside, but in fact, this is not the case——Callbacks in then are executed asynchronously。 This specification also mentions:

In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously.

Specifically, when executing then:

  • If the promise status above is determined, then the callback of then will be pushed into the task queue first, and then the callback will be taken out from the queue for execution after the synchronization code is executed.
  • If the previous promise status is not determined: the then callback will be stored in the corresponding cache array first. After the promise status is determined, the callback will be taken out of the corresponding array and pushed into the task queue. After the synchronization code is executed, the callback will be taken out of the queue for execution.

So the question is, does the execution of callback function belong to micro task or macro task?

Let’s look at the specification:

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.

To be clear, the a + specification only specifies that the callback function must be executed asynchronously, and does not require that it must be a micro task or macro task. In other words, there is no problem in relying on macro tasks to implement promise. In theory, it can also pass the a + test. What really requires promise to rely on micro tasks to implement is the HTML standard, which can also be found in relevant documents.

Therefore, there are two ways to simulate the asynchronous execution of callback functions. The first is based on macro taskssetTimeoutExecution of package callback function; The second is implemented based on micro tasks, which can be consideredqueueMicrotaskperhapsprocess.nextTick

1) Implementation based on macro task

The execution logic of the callback function is written in the then method, so you only need to modify the then method and wrap a callback function outside the logic that originally executed the callback functionsetTimeoutYou can:

Promise.prototype.then = function (onFulfilled,onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? 
        onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? 
        onRejected : reason => { throw reason }
    
    let self = this
    
    //Promise finally returned
    let promise2
    if(self.status === FULFILLED){
        return promise2 = new Promise((resolve,reject) => {
            setTimeout(() => {
                try {
                    let x = onFulfilled(self.value)
                    //Handle the return value of then with resolvepromise
                    resolvePromise(promise2,x,resolve,reject)
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
    else if(self.status === REJECTED){
        return promise2 = new Promise((resolve,reject) => {
            setTimeout(() => {
                try {
                    let x = onRejected(self.reason)
                    resolvePromise(promise2,x,resolve,reject)
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
    else if(self.status === PENDING){
        return promise2 = new Promise((resolve,reject) => {
            self.onFulfilledCallbacks.push(() => {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(self.value)
                        resolvePromise(promise2,x,resolve,reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            })
            self.onRejectedCallbacks.push(() => {
               setTimeout(() => {
                    try {
                        let x = onRejected(self.reason)
                        resolvePromise(promise2,x,resolve,reject)
                    } catch (e) {
                        reject(e)
                    }
               })
            })
        })
    }
}

So, callsetTimeoutOnly the callback function passed to it will be put into the macro task queue, which can be regarded as putting the successful callback or failed callback into the macro task queue.

2) Implementation based on micro task

Similarly, if you want to implement promise based on micro tasks, you can usequeueMicrotaskTo wrap the execution of the callback function, so that its execution can be put into a micro task queue.

Promise.prototype.then = function (onFulfilled,onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? 
        onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? 
        onRejected : reason => { throw reason }
    
    let self = this
    
    //Promise finally returned
    let promise2
    if(self.status === FULFILLED){
        return promise2 = new Promise((resolve,reject) => {
            queueMicrotask(() => {
                try {
                    let x = onFulfilled(self.value)
                    //Handle the return value of then with resolvepromise
                    resolvePromise(promise2,x,resolve,reject)
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
    else if(self.status === REJECTED){
        return promise2 = new Promise((resolve,reject) => {
            queueMicrotask(() => {
                try {
                    let x = onRejected(self.reason)
                    resolvePromise(promise2,x,resolve,reject)
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
    else if(self.status === PENDING){
        return promise2 = new Promise((resolve,reject) => {
            self.onFulfilledCallbacks.push(() => {
                queueMicrotask(() => {
                    try {
                        let x = onFulfilled(self.value)
                        resolvePromise(promise2,x,resolve,reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            })
            self.onRejectedCallbacks.push(() => {
               queueMicrotask(() => {
                    try {
                        let x = onRejected(self.reason)
                        resolvePromise(promise2,x,resolve,reject)
                    } catch (e) {
                        reject(e)
                    }
               })
            })
        })
    }
}

In the node environment, you can also useprocess.nextTick()replacequeueMicrotask

PS: in addition, it should be noted that node was introduced after V11queueMicrotaskMethod, so pay attention to upgrade the version of the node, otherwise it will fail the a + test.

Perfect resolve function

In fact, our current code can pass the a + test, but our resolve function still needs to be improved.

First look at the reject function. When new promise creates an instance, if the parameter accepted by the reject function is also a promise, what will be the final returned instance? Try using native promise:

let p1 = new Promise((resolve,reject) => {
    reject(new Promise((resolve,reject) => {
        resolve(123)
    }))
})
let p2 = new Promise((resolve,reject) => {
    reject(new Promise((resolve,reject) => {
        reject(123)
    }))
})
//Print promise {fully: 123}
p1.then(null,e => {
    console.log(e)              
})
//Print promise {rejected: 123}
p2.then(null,e => {
    console.log(e)              
})

It can be seen that even if the parameter accepted by the reject function is a promise, it will take the whole promise as the reason and return a promise in the rejected state. The logic of the reject function we implemented earlier is just like this, which shows that there is no problem in the implementation of this function.


But the resolve function is different. Try using native promise:

let p1 = new Promise((resolve,reject) => {
    resolve(new Promise((resolve,reject) => {
        resolve(123)
    }))
})
let p2 = new Promise((resolve,reject) => {
    resolve(new Promise((resolve,reject) => {
        reject(123)
    }))
})
//Print value 123
p1.then(
    (value) => {console.log('value',value)},
    (reason) => {console.log('reason',reason)}
)
//Print reason 123
p2.then(
    (value) => {console.log('value',value)},
    (reason) => {console.log('reason',reason)}
)

It can be seen that if the promise of the resolved state is passed in to the resolve function (the promise of the resolved state is the same for how many layers are nested here), a promise of the value of the innermost layer of resolve will eventually be returned; If you pass in a promise in the rejected state, you will eventually return a “same” promise (the same state and reason).

However, according to the logic of the resolve function we implemented earlier, we uniformly take the parameters passed to resolve as value, and always return a promise in the resolved state. Obviously, this is not consistent with the native behavior (note that it is not said to be wrong, because the a + specification does not require this). So how should it be modified?

In fact, it is also very simple. That is to detect whether the parameter passed to resolve is promise. If so, continue to call the then method through this parameter. In this way, if the parameter is a promise in the rejected state, calling then means calling the failed callback function reject and passing in the reason of the parameter, so as to ensure that the final return is a promise with the same status and reason as the parameter; If the parameter is a promise in the resolved state, calling then means that the callback function resolve is successfully called and the value of the parameter is passed in, so as to ensure that the final return is a promise with the same parameter state and the same value – even if there is a nesting of promises in multiple resolved States, Anyway, we can always get the resolution value of the innermost layer in the end.

Therefore, the modified resolve function is as follows:

function resolve(value){
    if (value instanceof Promise) {
       return value.then(resolve,reject) 
    }
    if(self.status === PENDING){
        self.status = FULFILLED
        self.value = value
        self.onFulfilledCallbacks.forEach(fn => fn())
    }
}

Final code

The final code is as follows:

// promise.js

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function Promise (executor) {
    let self = this
    self.status = PENDING
    self.value = null
    self.reason = null
    self.onFulfilledCallbacks = []
    self.onRejectedCallbacks = []
    function resolve(value){
        if (value instanceof Promise) {
               return value.then(resolve,reject) 
        }
        if(self.status === PENDING){
            self.status = FULFILLED
            self.value = value
            self.onFulfilledCallbacks.forEach(fn => fn())
        }
    }
    function reject(reason){
        if(self.status === PENDING){
            self.status = REJECTED
            self.reason = reason
            self.onRejectedCallbacks.forEach(fn => fn())
        }
    }
    try {
        executor(resolve,reject)
    } catch (e) {
        reject(e)
    }
}
Promise.prototype.then = function (onFulfilled,onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
    onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e }
    let self = this
    let promise2
    if (self.status === FULFILLED) {
        return promise2 = new Promise((resolve,reject) => {
            queueMicrotask(() => {
                try {
                    let x = onFulfilled(self.value)
                    resolvePromise(promise2,x,resolve,reject)
                } catch (e) {
                    reject(e)
                }
            })
        })
    } 
    else if (self.status === REJECTED) {
        return promise2 = new Promise((resolve,reject) => {
            queueMicrotask(() => {
                try {
                    let x = onRejected(self.reason)
                    resolvePromise(promise2,x,resolve,reject)
                } catch (e) {
                    reject(e)
                }
            })
        })
    } 
    else if (self.status === PENDING) {
        return promise2 = new Promise((resolve,reject) => {
            self.onFulfilledCallbacks.push(() => {
                queueMicrotask(() => {
                    try {
                        let x = onFulfilled(self.value)
                        resolvePromise(promise2,x,resolve,reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            })
            self.onRejectedCallbacks.push(() => {
                queueMicrotask(() => {
                    try {
                        let x = onRejected(self.reason)
                        resolvePromise(promise2,x,resolve,reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            })
        })
    }
}
function resolvePromise (promise2,x,resolve,reject) {
    if(promise2 === x){
        return reject(new TypeError('Chaining cycle!'))
    }
    if (x !== null && typeof x === 'object' || typeof x === 'function') {
        let then 
        try {
            then = x.then
        } catch (e) {
            reject(x)
        }
        if (typeof then === 'function') {
            let call = false
            try {
                then.call(x,(y) => {
                    if(called) return
                    called = true
                    resolvePromise(promise2,y,resolve,reject)
                },(r) => {
                    if(called) return
                    called = true
                    reject(r)
                })
            } catch (e) {
                if(called) return
                reject(e)
            }
        } else {
            resolve(x)
        }
    } else {
        resolve(x)
    }
}

Promise a + test

Can helppromises-aplus-testThis library tests the promise we implemented.

First install through NPM:

npm install promises-aplus-test -D

Then inpromise.jsAdd to file:

// promise.js

Promise.defer = Promise.deferred = function () {
    let dfd = {};
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve;
        dfd.reject = reject;
    });
    return dfd;
}

module.exports = Promise;

Finally run the test:

promises-aplus-test ./promise.js

The test results are as follows:

Research on the principle of JS native method (10): how to realize promise / A + by handwriting and its methods?

872 test cases have been successfully passed, which shows that our promise meets the a + specification. If some test cases fail, they can be changed according to the specification.

Static method and prototype method of promise

Promise a + specification does not require the implementation of promise’s static methods and prototype methods (except then methods), but with the previous foundation, it is not difficult to implement these methods. Let’s implement one by one.

Promise.resolve()

Promise.resolveIf the accepted parameter is a promise, the promise will be returned as it is; If it is a thenable, return a promise in the thenable state; In other cases, promise of the given parameter resolve is returned.

The implementation is as follows:

Promise.resolve = (param) => {
    if(param instanceof Promise){
        return param
    } 
    return new Promise((resolve,reject) => {
        //If it's thenable
        if(param && param.then && typeof param.then === 'function'){
            param.then(resolve,reject)
        } else {
            resolve(param)
        }
    })
}

PS: whyparam.then(resolve,reject)Can the returned promise follow the param state? Because param will always execute a callback in then at a certain time, and pass in the corresponding parameters – that is, executeresolve(value)perhapsreject(reason), and this execution is performed inside the returned promise, so the returned promise will follow the param state.

Promise.reject()

In any case,Promise.reject()Will return a promise that rejects the given parameter:

Promise.reject = (param) => {
    return new Promise((resolve,reject) => {
        reject(param)
    })
}

Promise.all()

Promise.all()Accepted parameters:

  • If it is not iteratable, return a promise in the rejected state;
  • If it is an empty iteratable object, a promise of resolve empty array will be returned;
  • When iteratable, if it is a non empty iteratable object:

    • If the promise does not contain the rejected state and pending state, a promise of the resolve result array is returned, and the result array contains the resolved values of each promise
    • Contains a promise in the rejected state and returns the same promise
    • If it does not contain a promise in the rejected state, but contains a promise in the pending state, a promise in the pending state is returned

PS: every member of an iteratable object isPromise.resolve()Package into a promise

Therefore, the implementation code is as follows:

Promise.all = (promises) => {
    //Determine whether it can be iterated
    let isIterable = (params) => typeof params[Symbol.iterator] === 'function'
    return new Promise((resolve,reject) => {
        //If not iterative
        if(!isIterable(promises)) {
            reject(new TypeError(`${promises} is not iterable!`))
        }  else {
            let result = []
            let count = 0
            if(promises.length === 0){
                resolve(result)
            } else {
                for(let i = 0;i < promises.length;i++){
                    Promise.resolve(promises[i]).then((value) => {
                        count++
                        result[i] = value
                        if(count === promises.length);resolve(result)
                    },reject)
                }
            }
        }
    })
}

As you can see, we will traversepromisesFor each member in the, count the number of promises in the resolved state and store their values in the result array. As long as it is found that all members are promises in the resolved state, a promise in the resolve result array will be returned; As long as a promise in the rejected state is found, its reason will be used as the reason to return a promise in the rejected state; If there is a promise in pending status, it is impossible to execute resolve or reject. Therefore, a promise in pending status will be returned.

Promise.race()

andPromise.all()Similar, butPromise.race()Only one promise status is required, and finally the same promise will be returned. If an empty iteratable object is passed in, it means that it will never get a promise with the expected state settled. However, it will continue to wait, so it will eventually return a promise in pending state.

The implementation code is as follows:

Promise.race = (promises) => {
    let isIterable = (param) => typeof param[Symbol.iterator] === 'function'
    return new Promise((resolve,reject) => {
        if (!isIterable(promises)) {
            reject(new TypeError(`${promises} is not iterable!`))
        } else {
            for(let i = 0;i < promises.length;i++){
                Promise.resolve(promises[i]).then(resolve,reject)    
            }
        }        
    })
}

In fact, generally speaking, there are only two situations:

  • One is that there is at least one promise whose state is settled in the promises. When this promise is encountered, it will call then, and then call resolve or reject to settle the final returned promise state. It doesn’t matter if you encounter another promise whose state is determined and execute the corresponding resolve or reject, because the state of promise is irreversible
  • The other is that the status of all promises has not been determined, which means that it is never possible to execute the callback in then, that is, it is impossible to execute resolve or reject, so the final return is a promise in pending status

Promise.allSettled()

andPromise.all()Similarly, a promise of the resolve result array will be returned, but the result array will contain the resolved value or rejected value of each promise, similar to this:

[
    {status: "fulfilled", value: 11}
    {status: "rejected", reason: 22}
    {status: "fulfilled", value: 33}
]

If there is a promise in pending status in promises, the true “allsettled” cannot be reached, and a promise in pending status will be returned eventually.

The implementation code is as follows:

Promise.allSettled = (promises) => {
    let isIterable = param => typeof param[Symbol.iterator] === 'function'
    return new Promise((resolve,reject) => {
        if (!isIterable(promises)) {
           reject(new TypeError(`${promises} is not iterable!`)) 
        } else {
            let result = []
            let count = 0
            if(promises.length === 0) {
                resolve(result)
            } else {
                for(let i = 0;i < promises.length;i++){
                    Promise.resolve(promises[i]).then(
                        value => {
                            count++
                            result[i] = {
                                status: 'fulfilled',
                                value
                            }
                            if(count === promises.length) resolve(result)
                        },
                        reason => {
                            count++
                            result[i] = {
                                status: 'rejected',
                                reason
                            }
                            if(count === promises.length) resolve(result)
                        }
                    )
                }
            }
        }
    })
}

Promise.prototype.catch()

If the previous promise is in the rejected state, the catch method will be executed. Therefore, the catch method can be regarded as a then method without a successful callback as a parameter:

Promise.prototype.catch = (onRejected) => {
    return this.then(null,onRejected)
}

Promise.prototype.finally()

Finally, the method has two characteristics:

  • Regardless of whether the previous promise is in the resolved or rejected state, the callback function passed to finally can be executed
  • Finally, a promise will also be returned, which will generally follow the state of the promise calling finally. Unless the finally callback function returns a promise in the rejected state

The final implementation is as follows:

Promise.prototype.finally = (fn) => {
    let P = this.constructor
    return this.then(
        value  => P.resolve(fn()).then(() => value),
        reason => P.resolve(fn()).then(() => { throw reason })
    )
}

Several points to note:

1) Not directly used herePromise.resolve(), because if it is written in this way, the finally method can only be compatible with our promise version; The constructor of the promise instance can always obtain the promise version corresponding to the instance

2) Because the state of promise returned after calling finally depends on the promise instance calling finally, athis.then(...), which is convenient to obtain the value or reason of promise instance

3) In the success callback and failure callback of then, not only FN is executed, but also its execution results are used asP.resolve()This is mainly to handle the case where the FN execution result may be promise – in this case, it may affect the state of the last returned promise.

Understand the purpose of this writing through two examples. for instance:

Promise.resolve(1).finally(() => {return Promise.resolve(2)})

According to our code, it will return after calling finallyPromise.resolve(1).then(...), follow the logic of successful callback, and the obtained value is 1. andP.resolve(fn())Will returnfn(), that isPromise.resolve(2), follow the logic of successful callback, and the callback returns value. Finally, the call returns exactly a promise of resolve 1.

But if so:

Promise.resolve(1).finally(() => {return Promise.reject(2)})

Note that although the callback returns promise, it is in the rejected state. Then the call to finally will returnPromise.resolve(1).then(...), follow the logic of successful callback, and the obtained value is 1. andP.resolve(fn())Will returnfn(), that isPromise.reject(2), follow the logic of failed callback,Note that we do not declare a failed callback hereTherefore, the default failure callback will be used to accept the 2 rejected by the previous promise and throw the 2 out. Therefore, the final call returns exactly a promise of reject 2. In this case, the final promise does not follow the state of the finally called promise, but depends on the execution result of the finally callback.

Recommended Today

C # regular implementation of UBB parsing class code

The parsed code can pass XHTML   one   Strict verification;It includes title, link, font, alignment, picture, reference, list and other functions  Ubb.ReadMe.htm UBB code description title [H1] title I [/ H1] Title I [H2] Title II [/ H2] Title II [H1] Title III [/ H1] Title III [H4] Title IV [/ H4] Title IV […]