1
头图
This is the tenth article in the series of exploring the principles of JS native methods. This article will introduce how to write a Promise A+ specification , and implement the Promise related methods by the way.

Implement Promise/A+

the term

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

  • At the initial stage of the promise, the state has not yet settled and is in the pending state; it can be settled in the resolved state (fulfilled state), and the value of its resolved is represented by value; it can also be settled in the rejected state, and it can be represented by reason (rejection) The value of reject.
  • The success callback function accepted by the then method is called onFulfilled, and the failure callback function is called onRejected

Implement Promise constructor

Let's first try to implement a basic Promise constructor.

First, use three constants to represent the state of the promise instance:

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

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

We respectively define in the Promise constructor:

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

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

So the code is further expanded as follows:

function Promise(executor){
    // 保存 promise 实例的引用,方便在 resolve 函数和 reject 函数中访问
    let self = this
    self.status = PENDING
    self.value = null
    self.reason = null
    // 定义 resolve 函数
    function resolve(){ ... }
    // 定义 reject 函数
    function reject(){ ... }
    // 调用执行器函数
    executor(resolve,reject)
}

The function of the resolve function and the reject function is to accept value and reason as parameters respectively, and change the state of the promise based on these two values. However, in order to ensure the irreversibility of the promise state, the promise state must be determined to be pending before the state can be modified. So 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 resolve or reject may be called in the executor, thereby finally creating a promise instance with a settled state. But according to the specification, the executor itself may throw an exception when it executes. If so, you need to catch the exception and return a promise instance that rejects the exception. So modify the code 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
        }
    }
    // 捕获调用 executor 的时候可能出现的异常
    try{
        executor(resolve,reject)
    } catch(e) {
        reject(e)
    }
}

Implement the then method of the promise instance

1) Preliminary realization of the then method

All promise instances can call the then method. The then method is responsible for further processing the promise that the state has 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 the promise as parameters, respectively.

then method is always executed synchronously . Depending on the state of the promise when the then method is executed, there will be different processing logic:

(1) If the promise is resolved, execute the onFulfilled function

(2) If the promise is rejected, execute the onRejected function

(3) If the promise is in the pending state, the onFulfilled function and the onRejected function will not be executed for the time being. Instead, the two functions will be put into a cache array respectively, and then taken out from the array when the promise state is settled in the future Corresponding callback function execution

( Note: In fact, the execution of onFulfilled and onRejected is asynchronous, but we temporarily think that they are executed synchronously)

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

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

Promise.prototype.then = function (onFulfilled,onRejected) {
    // 因为是 promise 实例调用 then 方法,所以 this 指向实例,这里保存以备后用
    let self = this
    // 最终返回的 promise
    let promise2
    // 1)如果是 fulfilled 状态
    if(self.status === FULFILLED){
        return promise2 = new Promise((resolve,reject) => {
            onFulfilled(self.value)
        })
    }
    // 2)如果是 rejected 状态
    else if(self.status === REJECTED){
        return promise2 = new Promise((resolve,reject) => {
            onRejected(self.reason)
        })
    }
    // 3)如果是 pending 状态
    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 the Promise constructor: add two cache arrays

As you can see, there are two more cache arrays: onFulfilledCallbacks and onRejectedCallbacks . They are actually mounted on the promise instance, so modify the Promise constructor:

function Promise(executor){
    // ...省略其它代码...
    // 新增两个缓存数组
    self.onFulfilledCallbacks = []
    self.onRejectedCallbacks = []
}

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

Because when the then method is executed, the state of the previous promise has not been settled, and we don't know which callback function should be executed, so we choose to store the success callback and the failure callback in the cache array first. So when should the callback function be executed? It must be when the promise state is settled, and since the settlement of the promise state relies on the resolve function and the reject function, the timing of the execution of these two functions is the timing of the execution 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
        // 遍历缓存数组,取出所有成功回调函数执行
        self.onFulfilledCallbacks.forEach(fn => fn())
    }
}
function reject(reason){
    if(self.status === PENDING){
        self.status = REJECTED
        self.reason = reason
        // 遍历缓存数组,取出所有成功回调函数执行
        self.onRejectedCallbacks.forEach(fn => fn())
    }  
}

4) Improve the then method: exception capture

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

try...catch in all the places where callbacks are executed, and improve the then method as follows:

Promise.prototype.then = function (onFulfilled,onRejected) {
    // 因为是 promise 实例调用 then 方法,所以 this 指向实例,这里保存以备后用
    let self = this
    // 最终返回的 promise
    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) Improve the then method: achieve value penetration

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

// 传入非函数类型的参数
Promise.reject(1).then(null,{}).then(null,err => {
    console.log(err)                // 依然正常打印 1
})

// 没有传参数
Promise.resolve(1).then().then(res => {
    console.log(res)                // 依然正常打印 1
})

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

  • onFulfilled If it is not a function: define a function that returns value, pass the value down, and be captured by the subsequent success callback
  • onRejected If it is not a function: define a function that throws a reason, pass the reason down, and be captured by the subsequent failure callback

Therefore, the method of improving 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 }
    // ...省略其它代码...
}

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

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

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

The method of improving 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 }
    
    let self = this
    
    // 最终返回的 promise
    let promise2
    if(self.status === FULFILLED){
        return promise2 = new Promise((resolve,reject) => {
            try {
                let x = onFulfilled(self.value)
                // 用 resolvePromise 处理 then 的返回值
                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 is to determine the state of the promise returned after calling then based on the return value of the callback function , so the resolve call or reject call in resolvePromise will determine the final state of the promise returned

1) General idea

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

  1. First, determine whether the return value x of the callback function is equal to the return value promise2 after calling then. If it is equal, it will directly return a reject, and the reason is a TypeError. This is because the state of promise2 depends on x. If the two are the same object, it means that it needs to determine its own state, which is impossible.

    // 这样是会报错的,因为 then 的返回值等于回调函数的返回值
    let p = Promise.resolve(1).then(res => p)
  2. Then determine if x is a non-null object or function:

    1. If not: then x can never be a thenable, just resolve x directly
    2. If it is, then determine if x.then is 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 directly

The code implemented according to this idea is as follows:

function resolvePromise(promise2,x,resolve,reject){
    // 如果 promise2 和 x 是同一个对象,则会导致死循环
    if(promise2 === x){
        return reject(new TypeError('Chaining cycle'))
    }
    // 如果 x 是对象或者函数
    if(x !== null && typeof x === 'object' || typeof x === 'function'){
        // 如果 x 是一个 thenable
        if(typeof x.then === 'function'){
            //...继续处理...
        } 
        // 否则
        else {
            resolve(x)
        }
    }
    // 否则
    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 a promise), what should be done? Let's 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)
    })
})
// 打印 p1 和 p2 的结果,分别是:
Promise <fulfilled> 2
Promise <rejected> 2

As you can see, when the return value x of the callback function is a thenable, the promise returned after calling then will use 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 calls its then method, and pass in resolve and reject as the success callback and failure callback . Regardless of whether the state of x is settled, it will always call resolve or reject based on its own state at a certain moment, and will also pass in the value or reason of x, which is equivalent to calling resolve(value) or reject(reason) , so it can be determined The status of the promise returned after calling then.

But here is 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)
        }))
    })
})
// 打印 p1 和 p2 的结果,分别是:
Promise { <fulfilled> : 2}
Promise { <rejected> : Promise }

The difference here is that although the callback function also returns a promise, the internal resolve of this promise is still a promise. If you call 0610693db2eb78 directly according to the previous statement, the promise that is finally returned is a promise that resolves promises, but in fact, it should be a promise that resolve(value) resolve(value) cannot be used directly here, but 0610693db2eb7d should be called recursively until the innermost basic value is found as the final resolvePromise(promise2,value,resolve,reject)

However, if the promise returned by the callback function is still a promise, the promise returned is also a promise that rejects the promise. In this case, there is no need to recursively call to find the innermost basic value.

Therefore, the code for 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 to note

According to the specification, we can find that our resolvePromise function still needs improvement:

1) There are many different versions of Promise, and their specific behaviors may be different. In order to ensure the interoperability of different versions of Promise implementations and improve compatibility, some special cases will be handled in the resolvePromise method. include:

  • The then property of x may Object.defineProperty , and an exception will be thrown every time it gets. Therefore, you need to first try to get x.then and catch the possible exceptions-once caught, reject the exception (this means that the final return is a promise that rejects the exception)
  • When calling then, it will not be x.then , but by then.call(x) . Why is this? First of all, we have already obtained the let then = x.then to the then method through 0610693db2ecce, so the consideration here is not to get it repeatedly, but to directly use the then variable, but direct use will cause it to lose the this point, so it needs to be tied call Let this be x.

2) The success callback and failure callback passed to then may be executed multiple times. If so, the callback executed first shall prevail, and other executions will be ignored. Therefore, the variable called will be used to mark whether a callback has been executed

3) An exception may also be thrown when calling then. If so, reject the exception as well. But if the success callback or failure callback has been called when the exception is caught, there is no need to reject it.

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)
    }
}

Asynchronous execution of callback function

Finally, you also need to pay attention to the timing of execution of the then callback function.

If you only look at the implementation of the previous code, you will think that when the promise state is settled, the callback inside will be executed synchronously when the then is executed, but this is not the case— then is asynchronously executed . This specification also mentions:

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

Specifically, when executing then:

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

So the question is, is the execution of the callback function a micro task or a macro task?

You can look at the normative statement:

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 very clear, the A+ specification only clarifies that the callback function must be executed asynchronously, and does not require it to be a micro task or a macro task. In other words, there is no problem with relying on macro tasks to implement Promise, and theoretically it can also pass the A+ test. What really requires that Promise must rely on microtasks to implement is the HTML standard, which can also be found in related documents.

Therefore, if you want to simulate the asynchronous execution of the callback function, there are two ways. The first is based on the macro task, and setTimeout ; the second is based on the micro task, you can consider using queueMicrotask or process.nextTick .

1) Based on the realization of the 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 setTimeout outside the logic of the original callback function:

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
    let promise2
    if(self.status === FULFILLED){
        return promise2 = new Promise((resolve,reject) => {
            setTimeout(() => {
                try {
                    let x = onFulfilled(self.value)
                    // 用 resolvePromise 处理 then 的返回值
                    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)
                    }
               })
            })
        })
    }
}

In this way, setTimeout is called, only the callback function passed to it will be put into the macro task queue, which can be considered as putting the success callback or failure callback into the macro task queue.

2) Implementation based on microtasks

Similarly, if you want to implement Promise based on queueMicrotask , you can use 0610693db2f140 to wrap the execution of the callback function, so that its execution can be placed in a microtask 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
    let promise2
    if(self.status === FULFILLED){
        return promise2 = new Promise((resolve,reject) => {
            queueMicrotask(() => {
                try {
                    let x = onFulfilled(self.value)
                    // 用 resolvePromise 处理 then 的返回值
                    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 use process.nextTick() instead of queueMicrotask .

PS: Another thing to note is that the queueMicrotask method was introduced after Node v11, so you must pay attention to upgrading the version of Node, otherwise it will fail the A+ test.

Improve the resolve function

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

Let's look at the reject function first. When the new Promise creates an instance, if the parameter accepted by the reject function is also a promise, what will the returned instance be like? Try it with 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)
    }))
})
// 打印 Promise {fulfilled: 123}
p1.then(null,e => {
    console.log(e)              
})
// 打印 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 use this entire promise as the reason and return a promise in the rejected state. And the logic of the reject function we implemented earlier is exactly like this, which shows that there is no problem with the implementation of this function.


But the resolve function is different. Try it with 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)
    }))
})
// 打印 value 123
p1.then(
    (value) => {console.log('value',value)},
    (reason) => {console.log('reason',reason)}
)
// 打印 reason 123
p2.then(
    (value) => {console.log('value',value)},
    (reason) => {console.log('reason',reason)}
)

It can be seen that if the resolve function is passed a promise in the resolved state (the promises in the resolved state are the same for how many levels are nested here), then a promise with the innermost value of the resolve will be returned eventually; if the incoming state is a rejected state The promise will eventually return a "same" promise as it (the state is the same, and the reason is the same).

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

In fact, it is very simple, that is, to check whether the parameter passed to resolve is a promise, if it is, then continue to call the then method with this parameter. In this way, if the parameter is a promise in the rejected state, then 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 parameter state and the same reason; and if the parameter is For a promise in the resolved state, calling then means to call the success callback function resolve and pass in the value of the parameter, so as to ensure that the final return is a promise with the same parameter state and the same value-even if there are multiple resolved promises The nesting is okay, anyway, we can always get the innermost resolve value in the end.

Therefore, the revised 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

You can use the promises-aplus-test library to test the Promise we have implemented.

First install via npm:

npm install promises-aplus-test -D

Then add in the promise.js 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:

Successfully passed 872 test cases, indicating that the Promise we implemented complies with the A+ specification. If some test cases do not pass, they can be modified according to the specification.

Static methods and prototype methods that implement Promise

The Promise A+ specification does not require the implementation of Promise's static methods and prototype methods (except the then method), but with the previous foundation, it is not difficult to implement these methods. Let's implement them one by one below.

Promise.resolve()

Promise.resolve If the parameter accepted by db2f5e7 is a promise, it will eventually return the promise as it is; if it is a thenable, it will return a promise that adopts the thenable state; in other cases, it will always return a promise that resolves the given parameters.

The implementation is as follows:

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

PS: Why param.then(resolve,reject) make the returned promise continue to use the state of param? Because param will always execute a callback in then at a certain moment, and pass in the corresponding parameters-that is, execute resolve(value) or reject(reason) , and this execution is performed inside the returned promise, so the returned promise will definitely be used The status of param.

Promise.reject()

In any case, 0610693db2f667 will return a promise Promise.reject()

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

Promise.all()

Promise.all() accepted parameters:

  • When it is not iterable, return a promise in rejected state;
  • When iterable, if it is an empty iterable object, it returns a promise that resolves an empty array;
  • When iterable, if it is a non-empty iterable object:

    • For promises that do not contain rejected and pending states, a promise of a resolved result array is returned, and the result array contains the resolved value of each promise
    • Contains a promise in a rejected state, and returns an identical promise
    • A promise that does not contain a rejected state, but a promise that contains a pending state, returns a promise in a pending state

PS: Each member in the iterable object will be packed into a promise Promise.resolve()

Therefore, the implemented code is as follows:

Promise.all = (promises) => {
    // 判断是否可迭代
    let isIterable = (params) => typeof params[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] = value
                        if(count === promises.length);resolve(result)
                    },reject)
                }
            }
        }
    })
}

As you can see, we will traverse promises , use count to record the number of promises in the resolved state, and store their values in the result array, as long as we find that all members are promises in the resolved state, it will return A promise in the resolve result array; and as long as a promise in the rejected state is found, its reason will be used as the reason, and a promise in the rejected state will be returned; if there is a promise in the pending state, it is impossible to execute the resolve or reject, so Finally, a promise that is also pending will be returned.

Promise.race()

Similar to Promise.all() Promise.race() only requires that a promise state is settled, and will eventually return a promise like it. If an empty iterable object is passed in, it means that it will never get a promise in the desired state, but it will continue to wait, so it will eventually return a promise in the 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, there are only two situations in general:

  • One is that there is at least one promise in which the state is settled in the promises, then when this promise is encountered, it will be used to call then, and then resolve or reject will be called to make the final return state of the promise settled. It does not matter even if you encounter other promises that have settled in the state and execute the corresponding resolve or reject, because the state of the promise is irreversible
  • The other is that the state of all promises has not been settled, which means that the callback in then will never be executed, that is, it is impossible to execute resolve or reject, so the final return is a pending promise

Promise.allSettled()

Promise.all() result array, 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 pending promise in the promises, the real "allSettled" (all settled) cannot be reached, and a pending promise will eventually be returned.

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 settles in the rejected state, the catch method will be executed, so the catch method can be regarded as the then method without passing a success callback as a parameter:

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

Promise.prototype.finally()

There are two characteristics of the finally method:

  • Regardless of whether the previous promise is resolved or rejected, the callback function passed to finally can be executed
  • Finally, a promise will be returned at the end, and this promise will generally follow the state of the promise that called finally. Unless the finally callback function returns a promise in rejected state

The final realization 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 })
    )
}

Pay attention to a few points:

Promise.resolve() is not used directly here, because if written like this, then this finally method can only be compatible with our Promise version; and through the constructor of the promise instance, the Promise version corresponding to the instance can always be obtained

2) Because the state of the promise returned after calling finally depends on the promise instance that called finally, a this.then(...) is returned to facilitate obtaining the value or reason of the promise instance

3) In the success callback and failure callback of then, not only did fn were executed, but also the execution result was P.resolve() . This is mainly to deal with the situation where the execution result of fn may be a promise-in this case, it May affect the state of the last promise returned.

Through two examples to understand the purpose of this writing. For example:

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

According to our code, after calling finally, it will return Promise.resolve(1).then(...) , following the logic of successful callback, the value obtained is 1. And P.resolve(fn()) will return fn() , which is Promise.resolve(2) , follow the logic of successful callback, the callback returns value. The final call to finally returns happens to be a promise of resolve 1.

But if it is:

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

Note that although the callback returned here is also a promise, it is in a rejected state. Then after calling finally, it will return Promise.resolve(1).then(...) , follow the logic of successful callback, the value obtained is 1. And P.resolve(fn()) will return fn() , which is Promise.reject(2) , the logic of the failure callback, Note that we did not declare the failure callback , so we will use the default failure callback, accept the 2 that the previous promise rejected, and throw this 2 out , So the final call to finally returns happens to be 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.


Chor
2k 声望5.9k 粉丝