likes
comments
collection
share

【JavaScript】如何手写一个Promise

作者站长头像
站长
· 阅读数 68

前言

本文主要内容:通过编写一个 myPromise 对象来进一步了解 Promise 的运行机制

定义 myPromise

先来看看原生的Promise是如何定义的

const promise = new Promise((resolve, reject) =>{

};

所以我们要定义一个myPromise,首先是定义一个类,如下代码定义后进行了创建

class myPromise {

}

const mp = new myPromise((resolve, reject) => {
    
})

可以看到创建时填写了一个函数,而这个函数实际上是会被传入myPromise类的构造函数中作为参数的,如下

class myPromise {
    constructor(executor) {
        executor();
    }
}

const mp = new myPromise((resolve, reject) => {
    console.log("test");
})

也就是constructor接收了console.log("test")这个函数参数,然后进一步通过executor()调用了

而我们传入的这个函数是有 resolvereject 两个参数的,而这就需要在 myPromise 类中定义后传入,代码如下

class myPromise {
    constructor(executor) {
        executor(this.resolve,this.reject);
    }

    resolve() {

    }

    reject() {

    }
}

所以这样我们就将myPromise中的两个方法传给了构造函数中函数的两个参数

这里还有一个问题就是我们希望resolve作为私有方法被调用,所以加上#

因此,定义myPromise类的最终结果如下

class myPromise {
    constructor(executor) {
        executor(this.#resolve,this.#reject);
    }
    #resolve(value) {}
    #reject(reason) {}
}

编写同步执行的存取

定义好myPromise后,就要实现Promise的功能了,第一个就是能够通过resolve存储数据

这就需要在myPromise中定义一个变量result,然后通过resolve方法进行赋值,如下

class myPromise {
    #result;
    
    constructor(executor) {
        executor(this.#resolve,this.#reject);
    }
    
    #resolve(value) {
        this.#result = value;
    }
}

const mp = new myPromise((resolve, reject) => {
    resolve("test")
})
console.log(mp);

然而执行后却发现报错了

【JavaScript】如何手写一个Promise

这里就涉及一个 this 指向的问题,在今后的文章中会细讲,而这里我们需要将this绑定在myPromise上

    constructor(executor) {
        executor(this.#resolve.bind(this),this.#reject.bind(this));
    }

这样我们就成功地将resolve方法绑定到了myPromise上,再次执行看到数据已经存储

【JavaScript】如何手写一个Promise

然而这里还有一个问题,对于原生的Promise,执行多次resolve,由于PromiseState的存在,仅会保留第一次resolve的存储值。然而对我们目前的myPromise执行类似操作,会被不断覆盖

所以我们需要引入一个state变量,如果处于 pending 状态,state = 0;fulfilled 状态,state = 1;reject 状态,state = -1。

于是在调用resolve时,如果不等于pending则不继续执行;如果等于pending,则执行存储后给state赋值1。

#resolve(value) {
    if(!this.#state) return ;

    this.#result = value;
    this.#state = 1; 
}

这样我们就能实现不重复存储

存储了数据后,我们就可以着手读取数据

定义一个then方法,如下

then(onFulfilled, onRejected){
    if (this.#state === 1){
        onFulfilled(this.#result);
    }
}

通过判断state的状态来返回对应的result或者reason

所以这样我们就完成了同步执行代码的存取

异步执行代码的存取

虽然在上面我们已经实现了同步执行代码的存取,但是Promise的魅力是在处理异步数据上

当我们使用setTimeout来进行resolve的存储时,不出意料的无法读出数据,如下

const mp = new myPromise((resolve, reject) => {
    setTimeout(() => {
        resolve("test");
    },100);
})

原因在于,调用then方法时,resolve方法还未执行,则state的值仍为初始值0,所以无法进入判断

我们先不考虑then方法在resolve方法之前执行的问题,先让程序能够正常跑出结果

那么现在的问题就在于,让程序能够在数据传入 resolve 后,再次调用 onFulfilled() 这个回调函数,从而将数据传出

所以定义一个callback作为回调函数,在then方法执行时存储 onFulfilled,如下

if (this.#state === 0) {
    this.#callback = onFulfilled;
}

然后再在resolve方法中调用callback回调函数,如下

    #resolve(value) {
        if (this.#state !== 0) return;

        this.#result = value;
        this.#state = 1;

        this.#callback(this.#result);
    }

这样我们就解决了此时的问题

进行进一步的优化,将resolve编写如下

    #resolve(value) {
        if (this.#state !== 0) return;

        this.#result = value;
        this.#state = 1;

        this.callback && this.#callback(this.#result);
    }

resolve中添加了this.callback的短路机制,防止未调用then方法时,callback未定义的情况;

优化任务队列

当我们需要多次使用then方法读取myPromise中的值时,如下

const mp = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve("test");
    }, 1000)
})

mp.then((result)=>{
    console.log("result1",result);
})

mp.then((result)=>{
    console.log("result2",result);
})

mp.then((result)=>{
    console.log("result3",result);
})

得到的结果却只是最后一次运行的结果 result3,这是因为每次调用then方法都覆盖了callback函数,使得代码只能运行最后一次覆盖的情况

所以我们可以将callback定义为一个函数数组,#callbacks = [];

那么对于then中保存callback应该改为向数组中添加callback,如下

this.#callbacks.push(() => {
    onFulfilled(this.#result);
})

同样的,调用callbacks也应该遍历数组的每一个元素,如下

this.#callbacks.forEach(cb => {
    cb();
})

这样我们就能执行每一个then了

然而在优化任务队列中,我们知道原生的Promise是会将任务放到微任务队列的,所以我们也应当这样,通过queueMicrotask将任务放到微任务队列中,如下

#resolve(value) {
    if (this.#state !== 0) return;

    this.#result = value;
    this.#state = 1;

    queueMicrotask(() => {
        this.#callbacks.forEach(cb => {
            cb();
        })
    })
}

实现then的链式调用

现在我们的代码显然是不能实现链式调用的,因为链式调用的前提是每次调用then方法后,返回的也是个Promise,而myPromise.then是没有返回值的

所以我们需要让then有返回值,如下

then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
        if (this.#state === 0) {
            this.#callbacks.push(() => {
                resolve(onFulfilled(this.#result));
            })
        }else if (this.#state === 1) {
            queueMicrotask(() => {
                resolve(onFulfilled(this.#result));
            })
        }
    })
}

此时then中的回调函数中的返回值,会成为新的myPromise中的数据,也就是通过resolve再次存储onFulfilled(this.#result),这样我们就得到了新的加工好的myPromise

所以这样我们就能实现myPromise的链式调用

小结

通过编写myPromise类,实现了Promise的一些常用方法,涉及了回调函数、私有变量、this绑定、事件循环等知识点,让我对Promise存取异步数据的过程和原理有了更进一步的了解

转载自:https://juejin.cn/post/7205381513344630843
评论
请登录