从实现到理解的Promise

475 阅读11分钟

前言

下面的内容为Promise的JS全实现,但是效率并不如原生Promise,因为原生Promise使用了微任务,而为了模拟同样的效果,只能使用宏任务setTimeout来替代。

为了防止在原生的Promise对象上进行修改,导致某些错误的功能误打误撞正确了,所以使用一个新的名称,取名为Bromise。 同时一下内容可以通过promise aplus test。并且能通过MDN上各种用例。

同时,如果你能看完并且理解,那么任何Promise的顺序问题,都不再是问题。 同时,任何Promise的实现问题,也不会再是问题。

我的理解

Promise在js中的本质其实就是一个具有自动处理多个回调的高阶语法糖。嗯,其实就是可以解决因为callback地狱导致的多层嵌套问题,实质上其实是把递归转化为了迭代来进行逻辑处理。

在Promise中,传入构造函数中的函数将会被立刻执行,而其他的内容,则会被安排微任务处理阶段进行处理。同时微任务的执行时机为宏任务结束后,所以,他会比任何在本次循环中的setTimeout或者其他类似的宏任务都优先执行。 所以这还涉及到一个小问题,如果当前代码循环中,不交出控制权会怎么样? 例如

let bool = false
setTimeout(()=>{
    console.log(bool)
})
while(1){
    bool = !bool
}

其实你执行一次就会发现,你已经永远的失去了他的控制权,而且setTimeout内容将会永远的不能触发。这是因为js本质其实是在不断的循环解决任务。你也可以认为是这样。

while(1){
    let currencyMacro = World.macros
    World.macros = []
    macroTakss(currencyMacro)
    let currencyMicro = World.micros
    World.currencyMicro = []
    microTasks(currencyMicro)
}

而如果我们直接创建一个文件,并且运行,那么这些代码一定是运行在macroTasks中。而如果调用了setTimeout,则将会在下次的macroTasks运行。 所以说,当你在while中触发了无限循环。导致js无法进入下一个循环流程。那么你将永远的停留在那不可到达的彼岸。

因此,可以很明显的了解到promise的部分执行流程。同时你也可以写出20%有关输出先后的问题。那么还剩下80%。 举个例子(出自掘金)

new Promise((resolve, reject) => {
    console.log("外部promise");
    resolve();
})
    .then(() => {
        console.log("外部第一个then");
        return new Promise((resolve, reject) => {
            console.log("内部promise");
            resolve();
        })
            .then(() => {
                console.log("内部第一个then");
            })
            .then(() => {
                console.log("内部第二个then");
            });
    })
    .then(() => {
        console.log("外部第二个then");
    });
    /** output:
        外部promise
        外部第一个then
        内部promise
        内部第一个then
        内部第二个then
        外部第二个then
    */
new Promise((resolve, reject) => {
    console.log("外部promise");
    resolve();
})
    .then(() => {
        console.log("外部第一个then");
        new Promise((resolve, reject) => {
            console.log("内部promise");
            resolve();
        })
            .then(() => {
                console.log("内部第一个then");
            })
            .then(() => {
                console.log("内部第二个then");
            });
    })
    .then(() => {
        console.log("外部第二个then");
    });
    /** output:
        外部promise
        外部第一个then
        内部promise
        内部第一个then
        外部第二个then
        内部第二个then
    */

如果你不知道为什么少了一个return就导致后面两行的输出顺序发生了改变,那么请君与我一同研究一下promise的写法。

实现Bromise

constructor then resolvePromise

const PENDING = "pending"
const Fulfilled = "fulfilled"
const Reject = "reject"

function Bromise(fn) {
    let self = this
    this.status = PENDING
    this.onResolve = []
    this.onReject = []

    function onResolve(value) {
        if (self.status != PENDING) {
            return
        }

        self.status = Fulfilled
        self.value = value
        self.onResolve.forEach(cb => cb());
    }

    function onReject(reason) {
        if (self.status != PENDING) {
            return
        }
        self.reason = reason
        self.status = Reject
        self.onReject.forEach(cb => cb());
    }

    try {
        fn(onResolve, onReject)
    } catch (e) {
        onReject(e)
    }
}

上面的代码非常简单,可以说是promise最简单的部分。其中的fn其实是我们在创建Promise时候传入的执行函数。比如:

new Promise((resolve,reject)=>{
    // doSomething
    resolve()
})

显而易见的,我们每当完成任务的时候,都会调用传入的参数resolve,而这个resolve则是构造函数中的onResolve方法。这个方法会依次执行挂在this.onResolve,this.onReject上面的函数。而这个两个数组,模拟的是微任务中的promise部分。 接着是第二个重要的部分resolvePromise,这个resolvePromisePromise.resolve并不是一个函数,请注意。

function resolveBromise(promise, x, resolve, reject) {
    if (promise === x) {
        reject(new TypeError("Chaining cycle"))
        return
    }

    if (!(x && typeof x === 'object' || typeof x === 'function')) {
        resolve(x)
    }

    let used;
    try {
        let then = x.then
        if (typeof then === 'function') {
            then.call(x, (y) => {
                if (used) return
                used = true
                resolveBromise(promise, y, resolve, reject)
            }, (r) => {
                if (used) return
                used = true
                reject(r)
            })
        }else{
            if (used) return
            used = true
            resolve(x)
        }

        
    } catch (e) {
        if (used) return
        used = true
        reject(e)
    }
}

其实和Promise.resolve的实际效果差不多,但是他保证了第一个参数promise和第二个参数x不是同一个对象,因此不会出现无限resolvePromise。 上面这段代码的主要作用是把不是thenable的值resolve返回,把是thenable的运行后继续这个判断,直到返回值。

接下来实现最重要的then,整个Promise其实就这三个部分,至于其他的catch,finally,都是一些小封装。

Bromise.prototype.then = function (onResolve, onReject) {
    onResolve = typeof onResolve === 'function' ? onResolve : value => value
    onReject = typeof onReject === 'function' ? onReject : reason => { throw reason }

    let self = this
    let result = new Bromise((resolve, reject) => {
        if (self.status === PENDING) {
            self.onResolve.push(() => {
                setTimeout(() => {
                    try {
                        resolveBromise(result, onResolve(self.value), resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            })

            self.onReject.push(() => {
                setTimeout(() => {
                    try {
                        resolveBromise(result, onReject(self.reason), resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            })
        }else{
            setTimeout(() => {
                try {
                    let callTarget = self.status === Fulfilled?onResolve:onReject
                    resolveBromise(result, callTarget(self.status === Fulfilled?self.value:self.reason), resolve, reject)
                } catch (e) {
                    reject(e)
                }
            });
        }
    })
    return result
}

注意这个方法,如果想要理解,那么要注意几点。 1.Promise会立刻执行构造时传入的函数,因此,then中构造的Promise会立刻执行,但是内容均为创建微任务,也就是说,then的内容,被立刻的分到了下一个微任务循环中 2.每当你调用then,你都会获得一个新的Promise,因此,你接下来如果继续调用then,只会和上个Promise关联 3.虽然setTimeout有先来后到的顺序机制,但是注意,在一个Promise结束的时候,他会立刻执行他两个任务序列,也就是this.onResolve,this.onReject,但是,还请注意,这两个序列的具体内容,将会在下一个微任务中运行 4.当你在一个then中返回了一个Promise,这么那个then自身的Promise状态,将会由返回值来决定。

请注意,这个4将会是很多很多问题的关键。那么为什么return的Promise会接管原本的Promise的状态? 答案就在then的代码中。

try {
    let callTarget = self.status === Fulfilled?onResolve:onReject
    resolveBromise(result, callTarget(self.status === Fulfilled?self.value:self.reason), resolve, reject)
} catch (e) {
    reject(e)
}
// 如果你把它简化一下。假设默认的只会出现Fulfilled状态。那么这段代码应该为
try {
    resolveBromise(result, onResolve(self.value), resolve, reject)
} catch (e) {
    reject(e)
}
//而上文提到过,resolvePromise的作用是 “把不是thenable的值resolve返回,把是thenable的运行后继续这个判断,直到返回值。”
//因此,作为return返回的Promise,此时将会被注入到self.value中,并且传递到resolvePromise当中

所以,我们可以变相的认为,因为return <Thenable>的逻辑,导致Thenalbe的逻辑被强制的提升。也就是说,任务执行提前了。 通俗易懂的说,外层then的状态,将会由return的Promise的状态来决定。称之为紧急提升。可以认为是强制下沉

Promise的内部布局

所以说,你可以简单的认为有这样几个队列。

  • Next:将会在下次微任务循环执行时执行
  • OnResolve:将会在自身Promise执行resolve完成后下一个微任务循环执行
  • OnReject:将会在自身Promise执行reject完成后下一个微任务循环执行 请注意,OnResolve和OnReject只会有一个队列被执行。
new Promise((r,j)=>{ //Promise1
    r()
}).then(value=>{//Promise2
    return new Promise((r,j)=>{//Promise3
        r()
    }).then(value=>{//Promise4
        
    }).then(value=>{//Promise5
        console.log('over')
    })
})

我们来分析一下上面的代码。很明显的可以看出他们的关联链应该是Promise1->Promise2->Promise3->Promise4->Promise5,而我们return new Promise的返回值,其实应该是Promise5。所以Promise2的执行结束时间被强制的下沉到了Promise5的结束时间。 但是如果没有return,而是直接new Promise,会如何?

new Promise((r,j)=>{ //Promise1
    r()
}).then(value=>{//Promise2
    new Promise((r,j)=>{//Promise3
        r()
    }).then(value=>{//Promise4
        
    }).then(value=>{//Promise5
        console.log('over')
    })
}).then(value=>{//Promise6
    console.log('first')
})

此时的关联链应该是Promise1->Promise2,因为没有返回Promise3(Promise5),所以导致Promise2的结束时间会在then执行完成后立刻结束,而不需要等待其他的Promise运行。这就是为什么如果不return会产生不一样的运行结果。 那么为什么在上面的例子中,输出顺序是first -> over
因为当如果then的内部没有进行thenable返回,那么就会按照注册时间顺序进行执行。如上文的代码。他们的注册时间顺序应该是。 1.Promise2(Next) 2.Promise4(Next) 3.Promise5(Wait for Promise4) 4.Promise6(Wait for Promise2) 你可以很明显的发现,3和4的执行时间其实是依赖于1和2的。在下一次微任务循环时,会先执行1,然后触发4的任务入队。接着是2,然后触发3的任务入队。也就是会变成这样。 1.Promise6(Next) 2.Promise5(Next) 所以,这就解释了为什么没有return <Thenable>内容时,执行顺序会不一样。

到这里,如果看完并且理解了内容。那么我可以保证你能100%的解决任何的Promise问题。前提是你要能自己实现一个Promise并且让他通过promise-aplus-tests(NPM),你可以在任何一个版本上修改。也可以一步一步的按照promise aplus的规范进行逐步编写。 如果这里有人想问,Promise2和Promise4为什么是Next,而不是Wait for啊? 原因是因为,Promise内部有一个状态判别,如果当前的Promise已经resolve或者reject了,那么接下来的then会被直接推入Next中。而new Promise的时候,传入的fn是会被立刻执行的。所以说,如果在fn中立刻resolve或者reject。那么这个Promise的状态会被立刻标记为fulfilled或者reject。而如果当前Promise的状态为pending,那么接下来的一个then内容会被推入到onResolve和onReject队列中。等待Promise的状态变动。

catch finally resolve

有了上面内容的理解,其实对于catchfinally就更好理解了。而resolve其实就是一个对任何值类型的直接封装📦,让他变成一个Promise。

Bromise.prototype.resolve = function(value){
    if(value instanceof Bromise){
        return value
    }
    return new Bromise((resolve,reject)=>{
        if(value&&value.then&&typeof value.then == 'function'){
            setTimeout(()=>{
                value.then(resolve,reject)
            })
        }else{
            resolve(value)
        }
    })
}
// 一个只有onReject的then就是catch
Bromise.prototype.catch = function(fn){
    return this.then(null,fn)
}

//是不是非常的眼熟,其实他就是then的默认执行方法前加上了一个fn的中间件
Bromise.prototype.finally = function(fn){
    return this.then((value)=>{
        return Bromise.resolve(fn()).then(()=>{
            return value
        })
    },reason=>{
        return Bromise.resolve(fn()).then(()=>{
            throw reason
        })
    })
}

all race allSettled any

有了以上代码知识,实现这些功能估计都不在话下了。具体内容我放在了下面的完整代码中。但是我推荐各位自己尝试实现。并且使用MDN上的范例进行测试,以加深印象。

完整代码

const PENDING = "pending"
const Fulfilled = "fulfilled"
const Reject = "reject"

function Bromise(fn) {
    let self = this
    this.status = PENDING
    this.onResolve = []
    this.onReject = []

    function onResolve(value) {
        if (self.status != PENDING) {
            return
        }

        self.status = Fulfilled
        self.value = value
        self.onResolve.forEach(cb => cb());
    }

    function onReject(reason) {
        if (self.status != PENDING) {
            return
        }
        self.reason = reason
        self.status = Reject
        self.onReject.forEach(cb => cb());
    }

    try {
        fn(onResolve, onReject)
    } catch (e) {
        onReject(e)
    }
}

Bromise.prototype.then = function (onResolve, onReject) {
    onResolve = typeof onResolve === 'function' ? onResolve : value => value
    onReject = typeof onReject === 'function' ? onReject : reason => { throw reason }

    let self = this
    let result = new Bromise((resolve, reject) => {
        if (self.status === PENDING) {
            self.onResolve.push(() => {
                setTimeout(() => {
                    try {
                        resolveBromise(result, onResolve(self.value), resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            })

            self.onReject.push(() => {
                setTimeout(() => {
                    try {
                        resolveBromise(result, onReject(self.reason), resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            })
        }else{
            setTimeout(() => {
                try {
                    let callTarget = self.status === Fulfilled?onResolve:onReject
                    resolveBromise(result, callTarget(self.status === Fulfilled?self.value:self.reason), resolve, reject)
                } catch (e) {
                    reject(e)
                }
            });
        }
    })
    return result
}

function resolveBromise(promise, x, resolve, reject) {
    if (promise === x) {
        reject(new TypeError("Chaining cycle"))
        return
    }

    if (!(x && typeof x === 'object' || typeof x === 'function')) {
        resolve(x)
    }

    let used;
    try {
        let then = x.then
        if (typeof then === 'function') {
            then.call(x, (y) => {
                if (used) return
                used = true
                resolveBromise(promise, y, resolve, reject)
            }, (r) => {
                if (used) return
                used = true
                reject(r)
            })
        }else{
            if (used) return
            used = true
            resolve(x)
        }

        
    } catch (e) {
        if (used) return
        used = true
        reject(e)
    }
}

Bromise.resolve = function(value){
    if(value instanceof Bromise){
        return value
    }

    return new Bromise((s,j)=>{
        if(value&&value.then&&typeof value.then === 'function'){
            setTimeout(()=>{
                value.then(s,j)
            })
        }else{
            s(value)
        }
    })
}

Bromise.reject = function(value){
    return new Bromise((x,y)=>{
        y(value)
    })
}

Bromise.prototype.catch = function(onReject){
    return this.then(null,onReject)
}

Bromise.prototype.finally = function(cb){
    return this.then(value=>{
        return Bromise.resolve(cb()).then(()=>{
            return value
        })
    },reason=>{
        return Bromise.resolve(cb()).then(()=>{
            throw reason
        })
    })
}

Bromise.all = function(promises){
    return new Bromise((s,j)=>{
        let index = 0
        let result = [];
        if(promises.length == 0){
            return s(result)
        }

        let deal = function (i,data){
            result[i] = data
            if(++index == promises.length){
                return s(result)
            }
        }

        for(let i = 0;i<promises.length;i++){
            Bromise.resolve(promises[i]).then((value)=>{
                deal(i,value)
            },err=>{
                j(err)
            })
        }
    })
}

Bromise.race = function(promises){
    return new Bromise((resolve,reject)=>{
        for(let one of promises){
            Bromise.resolve(one).then(value=>{
                resolve(value)
            },err=>{
                reject(err)
            })
        }
    })
}

Bromise.any = function(promises){
    return new Bromise((resolve,reject)=>{
        if(promises.length === 0) return reject()
        let pending = 0
        for(let one of promises){
            if(!(one instanceof Bromise)) continue
            ++pending
            one.then(value=>{
                resolve(value)
            },err=>{
                if(--pending == 0) reject(new Error("No Promise in Promise.any was resolved"))
            })
        }

        if(pending == 0){
            resolve()
        }
    })
}

Bromise.allSettled = function(promises){
    return new Bromise((resolve,reject)=>{
        let result = []
        if(promises.length === 0){
            return resolve(result)
        }

        for(let one of promises){
            Bromise.resolve(one).then(value=>{
                result.push({ status: "fulfilled", value: value })
            },reason=>{
                result.push({ status: "rejected", reason: reason })
            }).finally(()=>{
                if(result.length >= promises.length){
                    resolve(result)
                }
            })
        }
    })
}

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

module.exports = Bromise

2020-11-10补充 Promise的值穿透

Promise的值穿透发生在一种情况下。当调用Promise.then的时候,如果传入的参数并非为一个函数,那么就会将当前Promise的value作为结果,传入到下一个then之中。而如果下一个then的内容依然不是函数,那么会继续的进行穿透。

具体代码体现为:

then
...
    onResolve = typeof onResolve === 'function' ? onResolve : value => value //当传入类型不为function时,自动的变成一个穿透函数
...

非常大意,只纠结于Promise的运行流程,从而忘记了一些基础的处理,惭愧。

2020-11-11补充 queueMicroTask

你可以把代码中所有的setTimeout改为queueMicroTask(nodejs > v11) || process.nextTick(only node js),这样你会获得更快的速度。具体表现在使用promise-aplus-test进行测试时,可以比setTimeout快2s。

原文链接