前言: 最后的章节终于要来了,不知道有没有人真正跟进下去这个进阶系列。最初我的本意只是想直接从 《手撕 Promise》 开始。但是我想了想,这样的话对一些基础知识不是特别牢固的小伙伴不太友好,于是就创建了该专栏尽量从头开始讲给那些真正想从我的博文中学习到那么一丢丢东西的人
阅读本文前需要拥有我们前面的六个进阶任务的通关钥匙 (0/6)
请跟随本文完成你登神长阶的最后一步
一. 分析 MyPromise 现有的问题
- 如果你跟进了前篇的知识,那么你目前的代码应该是下面这个样子。
- 虽然看起来即实现了存储同步数据的功能,又实现了存储异步数据的功能,但是到目前为止,我们的 MyPromise 还是一个假的 Promise 。什么意思呢?
- 我们先看一下原生的 Promise 存储一个同步数据时的样子。
根据之前的知识,在主线程的console.log('我在主线程,我应该是第一执行')
肯定是会比Promise
实例的then
方法的console.log
执行的快的,因为一个是在主线程,一个还得在微任务队列里排一会队。我们可以很快得出,在控制台的结果应该如下: - 现在我们测试一下我们的刚刚写好的
MyPromise
的情况是什么样子的。如果我们的 MyPromise 是理想状态下的效果,resolve
方法应该会被放入微任务队列。所以下面的代码按照理想情况应该是先输出console.log(我应该是第一次执行)
这行代码,然后输出数字 1。
然而结果却是: - 这该怎么办呢?
二. 微任务的创建
- 如果你之前认真读过我的《宏任务和微任务》这篇文章,那么你一定知道创建一个微任务其实非常非常简单。没错,就是使用 window 对象身上的
queueMicrotask
函数。 - 说干就干,我们的数据是在哪读的?你还记得吗?没记起来我提醒你一下,应该是在
then
方法的第一个回调函数onFulfilled
函数中的吧?那不就非常简单了吗?
我直接在state==='fulfilled'
的时候,也就是数据填充好以后,我将onFulFilled
函数从原来的同步执行代码逻辑转变为放入微任务队列去执行。哦哦,对了,别忘了我们resolve
函数也执行了一个特殊的onFulfilled
函数。
那么这里同理,把读取数据的任务放入微任务队列里去执行。 - 这时候我们测试一下,看看是不是我们想的那样。
- 上面是保存同步数据的情况,我们再测试一下保存异步数据的情况。
下面是控制台输出结果:
完美!是我们想达到的效果~
三. 多次调用then方法的结果
- 看似目前我们这个
MyPromise
好像有那味儿了。但是我们分析一下下面的一种情况。我们还是根据原生Promise
来推断。 - 注意! 这里不是
then
方法的链式调用,而是一个Promise
实例多次调用then
方法的写法。(这里不要和链式调用搞混了)
让我们看一下控制台的情况:
可以看到,我们执行了三次then
方法,它就帮我们执行三次读取数据的操作。然而反过头来看一下我们的MyPromise
是什么行为。 - 按照我们理想的效果,控制台应该会在2秒后输出三个
我是 MyPromise
。
ok我们测试一下:
可以看到,我们的控制台在2秒后只输出了一次MyPromise
。这是怎么回事呢?
四. 分析 Bug 产生的原因
- 不着急,我们一步一步分析代码的执行顺序。首先会执行
MyPromise
里executor
函数的代码。 - 然后代码会执行到
setTimout
,然后会把resolve
放入宏任务队列里去等待2秒。 - ok,接下来该执行下面三个连续的
then
方法。
注意接下来是全文第一个重点: 关键点就在于我们三个then
是同步代码,一定要搞清楚这回事。明白了这一点,我们就需要去看MyPromise
类里到底发生了什么情况。 - 由于我们的
resolve
还在任务队列里排着队,那么在两秒内这时候的state
百分之一百还是pending
状态。
可以我们的then
是要继续走的啊,这里需要接着往下我们看then
方法里的逻辑。由于我们这时候的state==='pending'
所以下面的逻辑我们压根不用考虑,只需要考虑第一个if
语句里的代码即可。 - 关键点就在于这里这段代码。
我执行的三次then
在这里相当于执行了三次赋值操作。什么意思呢? - 没看懂?没关系,我接下来这种写法你一定可以看明白什么意思。
你觉得控制台会为你分别执行三次吗?
看懂了这个例子,其实你就明白为什么我们的结果只会输出我们最后一次调用的then
方法的结果了。因为们前面两次都被第三次所覆盖了。
所以我们才会看到下面个结果,只输出了一个result3
。
五. 解决 Bug
- 那现在这个事情怎么解决呢?没错,数组!我们只需要将原来的
callBackFn
由一个单纯的变量,改造成一个数组。 - 紧接着去改造我们的
then
方法。
稍等,我想你有很大概率会按照上面的写法这样去写,其实这样是非常错误的,你这样的逻辑是把onFulfilled(this.$result)
函数执行的结果推进数据中去,而不是把一个函数推进数组中去。所以正确的写法应该是我们用箭头函数包装一层,如下所示。
上面的代码含义是,如果我在保存异步数据,由于的state
不能第一时间从pending
状态改变,那我就先把一个箭头函数推进一个叫callBackFnArray
的数组中去。 - 那么去哪里执行我们数据里堆积的回调函数呢?这里我们就需要去改造一下我们的
resolve
函数。
我们需要把上面的老代码改造成下面的样子。
我们用数组身上的forEach
得到每个箭头函数,然后依次调用即可。 - ok,我们还是测试这个例子看一下我们的思路是否正确。
下面是控制台输出效果。
是的,我们成功解决了这个棘手的问题。
六. then 函数可以链式调用的原因
- 我们知道原生 Promise 最大的特点就是可以允许我们连续
then
下去。如下所示。
我们不能光知道可以这样用,还要明确一点为什么可以这样用。首先,我们then
函数的调用者是不是一个 Promise 实例啊? - 那第二个
then
方法能调用,是不是就意味着这串代码也是一个 Promise?
怎么验证呢?非常简单,我们打印一下它的返回值不就可以了吗?说干就干,我们用一个变量test
去接收第一次then
方法的返回值结果看看是个什么。
我们看一下控制台。果然是一个 Promise - 分析完原生的 Promise,我们再分析一下我们的
MyPromise
。
很不幸的是,我们的then
返回的是一个undefined
。undefined
身上有then
方法吗?显然是没有的,如何解决?我们接着往下看。目前为止,你的代码应该是这个样子。为了方便你们进行下一步,我直接贴上源码,如果你们有哪一步没跟上,可以根据我现在的代码做出一些调整。
class MyPromise {
#result: any;
#state: "pending" | "fulfilled" | "rejected";
#callBackFnArray: Array;
constructor(executor: Function) {
this.#callBackFnArray = [];
this.#state = "pending";
executor(this.resolve.bind(this), this.reject.bind(this));
}
resolve(value) {
if (this.#state !== "pending") return;
this.#result = value;
this.#state = "fulfilled";
if (this.#callBackFnArray.length > 0) {
queueMicrotask(() => {
this.#callBackFnArray.forEach((onFulfilled) => {
onFulfilled();
});
});
} else {
return;
}
}
reject(value) {
if (this.#state !== "pending") return;
this.#result = value;
this.#state = "rejected";
}
then(onFulfilled, onRejected) {
if (this.#state === "pending") {
this.#callBackFnArray.push(() => {
onFulfilled(this.#result);
});
} else if (this.#state === "fulfilled") {
queueMicrotask(() => {
onFulfilled(this.#result);
});
} else if (this.#state === "rejected") {
onRejected(this.#result);
}
}
}
六. 实现 then 函数的链式调用
既然我们知道了要想实现
then
方法的链式调用,那么then
方法本身的返回值就也需要是一Promise
实例。那还想什么呢,我们直接看then
函数的结构体。
我们直接返回一个新的MyPromise
实例,然后最最最重要的关键点,也是我们之前一直在强调的一点,你一定要记住!MyPromise 的参数
execurtor
是一个会立即执行的普通函数。- 既然会立即执行,我们把之前
then
函数里的那逻辑代码放进去是不是没有任何影响?操作非常简单,只需要把之前的代码复制粘贴进我们的executor
函数内即可。< /br>
结果如下: - 这仅仅是第一步,看过我之前分析- 《手写“回调地狱”》的读者都知道。我们下一个
then
方法读取的结果是上一个onFulfilled
或者onRejected
的返回值。那么我们在这里如何读取呢? - 注意我上面这两段代码的含义。
这个函数的执行结果是不是就代表着onFulfilled
函数执行完毕的返回值呢?当下面这个两个箭头函数执行的时候,就会将onFulfilled(this.#result)
的结果通过回调函数的形式传递给我们。
这里确实是一个难点,需要读者深刻去体会下一个resolve
为了去保存数据而又作为了一个回调函数的意思。 - 最后我们测试一下是否可以正常链式调用。
经过千辛万苦,最后我们完成了then
的链式调用
七. 源码
在这里贴出我们最终实现的 MyPromise
源码。
class MyPromise {
#result: any;
#state: "pending" | "fulfilled" | "rejected";
#callBackFnArray: Array;
constructor(executor: Function) {
this.#callBackFnArray = [];
this.#state = "pending";
executor(this.resolve.bind(this), this.reject.bind(this));
}
resolve(value) {
if (this.#state !== "pending") return;
this.#result = value;
this.#state = "fulfilled";
if (this.#callBackFnArray.length > 0) {
queueMicrotask(() => {
this.#callBackFnArray.forEach((onFulfilled) => {
onFulfilled();
});
});
} else {
return;
}
}
reject(value) {
if (this.#state !== "pending") return;
this.#result = value;
this.#state = "rejected";
}
then(onFulfilled, onRejected?) {
return new MyPromise((resolve, reject) => {
if (this.#state === "pending") {
this.#callBackFnArray.push(() => {
resolve(onFulfilled(this.#result));
});
} else if (this.#state === "fulfilled") {
queueMicrotask(() => {
resolve(onFulfilled(this.#result));
});
} else if (this.#state === "rejected") {
onRejected(this.#result);
}
});
}
}
八. 结语
其实在我写 《js进阶之路》专栏,从第一篇开始就感触颇深,不知道我这种一点一点知识点碎碎的讲是否能符合现在快节奏的阅读习惯...我只能站在我的观众都是小白的角度去考虑每一个问题,包括之前的文章都是。
慢慢地从自己写博客中就逐渐体会到,阮一峰,阮大,张鑫旭, 旭神等众多博主在写出一篇尽量能让小白去理解他们想表达出的知识点的不易和艰辛,但也正是有了他们,我们才能避免踩更多的坑。我也是从一只小白走过来的,我也深知学习的不易,如果你觉得本篇对你有帮助,还希望能分享给你一同学习路上的小伙伴。
正因为淋过雨,所以想为后来者撑一把伞☔️~
也在这里恭喜自己,《手写Promise》 完结撒花