【JavaScript】如何手写一个Promise

前言

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

手写 Promise 应该就是理解 Promise 逻辑的最好办法,所以虽然掘金上已经有无数手写 Promise 的文章,我还是打算通过类似做笔记的办法手写一遍 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); 

然而执行后却发现报错了

image.png

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

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

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

image.png

然而这里还有一个问题,对于原生的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的链式调用

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

猜你喜欢

转载自blog.csdn.net/qq_53225741/article/details/129379409