五. 解决 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<any>; 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<any>; 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进阶之路》专栏,从第一篇开始就感触颇深,不知道我这种一点一点知识点碎碎的讲是否能符合现在快节奏的阅读习惯...我只能站在我的观众都是小白的角度去考虑每一个问题,包括之前的文章都是。
慢慢地从自己写博客中就逐渐体会到,阮一峰,阮大,张鑫旭, 旭神等众多博主在写出一篇尽量能让小白去理解他们想表达出的知识点的不易和艰辛,但也正是有了他们,我们才能避免踩更多的坑。我也是从一只小白走过来的,我也深知学习的不易,本篇忽略了 reject
方法,其实原理和 resolve
是一模一样的,还希望你自己举一反三推出来, reject
和 onRejected
的写法。如果你觉得本篇对你有帮助,还希望能分享给你一同学习路上的小伙伴。
正因为淋过雨,所以想为后来者撑一把伞☔️~
也在这里恭喜自己,《手写Promise》 完结撒花 🎉