先看几个例子
以下例子用的都是我自己写的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。
源码地址
最后
以上皆为个人理解,如有不对欢迎指点!!!
以上皆为个人理解,如有不对欢迎指点!!!
以上皆为个人理解,如有不对欢迎指点!!!