百分百还原Promise

546 阅读8分钟

先看几个例子

以下例子用的都是我自己写的MyPromise,换成Promise将得到完全一样的结果。

const MyPromise = require('./MyPromise')

let p1 = new MyPromise(resolve => resolve(4))
let p2 = new MyPromise(resolve => resolve(5))

p1.then(res => console.log('res1 => ', res))
p2.then(res => console.log('res2 => ', res))
p1.then(res => console.log('res3 => ', res))

// 答案
// res1 =>  4
// res2 =>  5
// res3 =>  4


// NewPromiseResolveThenableJobTask测试

const MyPromise = require('./MyPromise')

async function async1() {
    console.log("async1 start");
    await async2();
    console.log("async1 end");
}
async function async2() {
    console.log("async2");
    return MyPromise.resolve().then(() => {
        console.log("async2-inner");
    });
}

console.log("script start");
setTimeout(function () {
    console.log("settimeout");
});

async1();
new MyPromise(function (resolve) {
    console.log("promise1");
    resolve();
})
    .then(function () {
        console.log("promise2");
    })
    .then(function () {
        console.log("promise3");
    })
    .then(function () {
        console.log("promise4");
    });
console.log("script end");


// script start
// async1 start
// async2
// promise1
// script end
// async2-inner
// promise2
// promise3
// async1 end
// promise4
// settimeout

// NewPromiseResolveThenableJobTask测试
const MyPromise = require('./MyPromise')

MyPromise.resolve().then(() => {
    console.log(0);
    return MyPromise.resolve(4)
}).then(res => {
    console.log(res);
})

MyPromise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() => {
    console.log(6);
})


// 0
// 1
// 2
// 3
// 4
// 5
// 6


// HostPromiseRejectionTracker测试


const MyPromise = require('./MyPromise')

let p = new MyPromise((_, reject) => {
    reject(1)  // unhandler promise reject
})
setTimeout(() => {
    p.catch(err => console.log(err))
}, 0)

本文看点

我的promise是百分之百还原了原生promise的行为,把上面四个例子换成原生promise可以得到完全一样的结果,如果上面的四个例子有其中一个是和结果不一样的,那么这篇文章就值得你一看。

本人花了两天时间研究了下手撕promise,主要参考资料就是原生promise,完全一模一样的表现,里面涉及到一些v8源码的逻辑比如HostPromiseRejectionTracker、NewPromiseResolveThenableJobTask等主要参考juejin.cn/post/705520… ,里面的一些v8的c++源码解读,以及promise.resolve怎么把promise.then的回调push到microtask中,对本次手撕promise的设计思路有很大的帮助,特此感谢!!

如果有小伙伴看不懂上面几个例子怎么执行出来的可以看看,可以说是读懂本文的前提条件。当然我也会在实现到具体功能时进行解析。

tips:promise的三种状态以及变化这种基础到不能再基础的这里就不提了,不然全文篇幅太长了,代码中会直接写上,看不懂代码为什么这么写的小伙伴可以私聊也可以直接百度。

设计思路

先从第一个例子入手,我猜大部分小伙伴得到的应该都是445,如果第一个例子得到445的同学注意这里,之所以得到445的原因就是你们把onFulfilled全都放在了该promise中,resolve的时候一次性的清理掉全部了,如果要防止这种情况,保证454按注册顺序执行的话,很明显要把所有的onFulfilled放在同一个有序队列中,但他们是两个promise的onFulfilled,所以,我干脆把onFulfilled的任务队列放到原型中,所有的promise实例共享这个任务队列。

    // 事件队列
    static taskQueue = []

但这些任务终究是要回归到对应的promise上的,并且resolve一个promise还需要把then返回的新的promise的处理函数给push到microtask中,所以我对于这个事件队列的数据结构设计如下:

        [
            {
                // promise实例
                instance: this,
                // 失败的回调
                onRejected,
                // 成功的回调
                onFulfilled,
                // 返回的新的promise
                next,
            },
            ...
        ]

剩下的三个例子则是一些promise的特殊情况,会有对应的函数进行处理,稍后再提。

初步设计

根据以上的思路以及promise基础,得到一下代码: 注意then方法里的代码已经实现了异步resolve的功能了。



class MyPromise {
    #status = 'pending'
    #result = null
    #error = null
    // 事件队列
    static taskQueue = []
    // resolve调用
    #resolve = function (res) {
        // 如果状态没改变,那就改变状态并保存结果(promise状态只能更改一次)
        if (this.#status === 'pending') {
            this.#result = res
            this.#status = 'fulfilled'
        }

    }
    // 与上面resolve同理
    #reject = function (err) {
        if (this.#status === 'pending') {
            this.#error = err
            this.#status = 'rejected'
        }

    }

    // 构造器:
    constructor(cb = () => { }) {
        // 同步执行回调函数
        cb(this.#resolve.bind(this), this.#reject.bind(this))

    }

    // then方法:
    then(onFulfilled = (e) => e, onRejected) {
        let next = new MyPromise()
        // 添加两个监听器
        MyPromise.taskQueue.push({
            onRejected,
            onFulfilled,
            instance: this,
            next,
        })

        // 如果promise已经返回结果,那就执行对应的监听器
        if (this.#status === 'fulfilled') this.#resolve()
        if (this.#status === 'rejected') this.#reject()

        // 返回新的promise
        return next
    }
}

怎么触发任务

promise再resolve或reject的时候,就会把promise.then的回调给push到microtask中,那么注意这里,我把任务队列taskQueue遍历一遍,如果promise实例状态是fulfilled的我就把onFulfilled push到microtask中,promise实例状态是rejected的我就把onRejected push到microtask中,如果promise实例状态还是pending的我就按顺序给它放回taskQueue。

上面这个逻辑,就是得到例子一的454的关键所在,仔细品。而且我并不是立即执行回调函数,而是使用queueMicrotask把它push到microtask中,这也正是promise.then的回调函数都是微任务效果的由来。

题外话:有的描述说是promise.then是微任务,但个人认为是promise.then的回调函数是微任务,原因很简单,promise.then()是带括号的,是我们调用的方法,是立即执行的,我们是调用,并不是声明方法或注册方法,它的调用时机已经很明确了,promise.then做的事情就是注册监听器,等promise resolve或者reject的时候才把监听器push到microtask中。

由此逻辑得到一下方法


    // 用于清理事件队列的方法
    #clearTaskStack = function () {
        // 用于记录pending的promise
        let unfinishPs = []
        for (let i = 0; i < MyPromise.taskQueue.length; i++) {
            let item = MyPromise.taskQueue[i]
            // 如果已经resolve
            if (item.instance.#status === 'fulfilled') {
                // 创建微任务
                queueMicrotask(() => {
                    // 执行监听器
                    let res = item.onFulfilled(item.instance.#result)
                    item.next.#resolve(res)
                })
            }
            // 如果已经reject
            else if (item.instance.#status === 'rejected') {
                // 创建微任务
                queueMicrotask(() => {
                    // 执行监听器
                    item.onRejected(item.instance.#error)
                    // resolve下一个promise
                    item.next.#resolve()
                })
            }
            else {
                unfinishPs.push(item)
            }
        }
        // 只留下pending的promise
        MyPromise.taskQueue = unfinishPs
    }

同时!!!,resolve和reject的时候触发

    #resolve = function (res) {
        // 如果状态没改变,那就改变状态并保存结果(promise状态只能更改一次)
        if (this.#status === 'pending') {
            this.#result = res
            this.#status = 'fulfilled'
        }
        this.#clearTaskStack()
    }
    // 与上面resolve同理
    #reject = function (err) {
        if (this.#status === 'pending') {
            this.#error = err
            this.#status = 'rejected'
        }
        this.#clearTaskStack()
    }

到这里我们就已经有一个可以解决异步问题+链式调用的thenable promise了,接下来我们就要解决例子二三四的问题了。

NewPromiseResolveThenableJobTask

一个p1 = promise,当onFulfilled返回值是一个p2 = promise时,v8源码会调用NewPromiseResolveThenableJobTask,这个方法就是以微任务的方式,把返回的promise自动调用一次then方法,拿到p2的值,再把这个值当作p1的resolve的参数进行调用。想看源码的可以去看一下上面分享的文章,而我自己js实现的如下:

// NewPromiseResolveThenableJobTask
    #NewPromiseResolveThenableJobTask = function (resPromise, nextPromise) {
        queueMicrotask(() => {
            resPromise.then(res => {
                // resolve下一个promise
                nextPromise.#resolve(res)
            })
        })
    }

然后还要再clearTaskStack中调用(其中如果返回thenable对象的处理也添加上去了,因为thenable对象就是个普通对象,它的then的回调方法并不是微任务方式调用的,所以这里手动创建一个微任务即可)


    // 用于清理事件队列的方法
    #clearTaskStack = function () {
        // 用于记录pending的promise
        let unfinishPs = []
        for (let i = 0; i < MyPromise.taskQueue.length; i++) {
            let item = MyPromise.taskQueue[i]
            // 如果已经resolve
            if (item.instance.#status === 'fulfilled') {
                // 创建微任务
                queueMicrotask(() => {
                    // 执行监听器
                    let res = item.onFulfilled(item.instance.#result)
                    // 如果监听器返回是个promise,应该在生产一个微任务,且取值
                    if (res instanceof MyPromise) {
                        // NewPromiseResolveThenableJobTask
                        this.#NewPromiseResolveThenableJobTask(res, item.next)
                    }
                    // 如果是个thenable对象,也要调用它的then拿到值,且then也会创建一个微任务(这里只是看着原生promise的表现进行猜测),但我们这里的res.then如果是thenable对象的话是不会创建微任务的,所以要手动加一个微任务
                    else if (res && res.then) {
                        res.then(res => {
                            queueMicrotask(() => {
                                // resolve下一个promise
                                item.next.#resolve(res)
                            })
                        })
                    }
                    else {
                        // resolve下一个promise
                        item.next.#resolve(res)
                    }
                })
            }
            // 如果已经reject
            else if (item.instance.#status === 'rejected') {
                // 创建微任务
                queueMicrotask(() => {
                    // 执行监听器
                    item.onRejected(item.instance.#error)
                    // resolve下一个promise
                    item.next.#resolve()
                })
            }
            else {
                unfinishPs.push(item)
            }
        }
        // 只留下pending的promise
        MyPromise.taskQueue = unfinishPs
    }

这个时候你们再去试一试例子二和例子三,看看能否得出相同的结果。

HostPromiseRejectionTracker

HostPromiseRejectionTracker 用于跟踪 Promise 的 rejected,当我们调用一个 Promise 的状态为 reject 且未为其绑定 onRejected 的处理函数时, JavaScript会抛出错误。当然这个检测也是微任务的形式。这个的实现要改的地方比较多,而且一般使用都会带catch,不catch我们也不希望它出什么错,所以实现这个我也不能说他好在哪,但毕竟人家v8都实现了,我们也尽可能的还原吧。

我的一个思路是,它是再reject之后的一个微任务中进行检测的,那我就把所有的promise实例收集起来,在其中一个实例状态reject之后触发,生成一个微任务push到microtask中,而这个任务实际做的事情就是遍历所有的promise实例,找到已经rejected的实例,然后判断它是否具有onRejected。

而判断一个promise是否具有onRejected,我想过一个办法是遍历taskQueue事件队列,看能否找到对应的onRejected,但这里的onRejected执行完就会被删掉,而且onRejected是微任务,HostPromiseRejectionTracker检测本身也是个微任务,为了防止让整个逻辑变得复杂起来,于是我换一个方法:在catch的时候,直接改变该promise实例的状态,然后遍历所有promise实例只需要直接判断这个实例的状态即可,也少去了去遍历taskQueue事件队列这一层循环以及更多的判断。

#hasRejectHandler = false
 // 构造器:
    constructor(cb = () => { }) {
        // 收集promise实例
        MyPromise.instanceQueue.push(this)
        // 同步执行回调函数
        cb(this.#resolve.bind(this), this.#reject.bind(this))

    }
// then方法:
    then(onFulfilled = (e) => e, onRejected) {
        // 如果有onRejected处理函数,改变promise的标记状态
        if (onRejected) this.#hasRejectHandler = true
        let next = new MyPromise()
        // 1.添加两个监听器
        MyPromise.taskQueue.push({
            onRejected,
            onFulfilled,
            instance: this,
            next,
        })

        // 3.如果promise已经返回结果,那就执行对应的监听器
        if (this.#status === 'fulfilled') this.#resolve()
        if (this.#status === 'rejected') this.#reject()

        return next
    }

至此,我们的promise已经还原完毕,里面的各种生成微任务也完美复刻原生的promise。

源码地址

gitee.com/luoyisen/my…

最后

以上皆为个人理解,如有不对欢迎指点!!!

以上皆为个人理解,如有不对欢迎指点!!!

以上皆为个人理解,如有不对欢迎指点!!!