【JavaScript】如何手写一个Promise
前言
本文主要内容:通过编写一个 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()
调用了
而我们传入的这个函数是有 resolve
和 reject
两个参数的,而这就需要在 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);
然而执行后却发现报错了
这里就涉及一个 this 指向的问题,在今后的文章中会细讲,而这里我们需要将this绑定在myPromise上
constructor(executor) {
executor(this.#resolve.bind(this),this.#reject.bind(this));
}
这样我们就成功地将resolve方法绑定到了myPromise上,再次执行看到数据已经存储
然而这里还有一个问题,对于原生的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