javaScript 进阶之路 --- 《手写 Promise(中篇)》
手写 Promise(中篇)
前言: 阅读前需要拥有我们前面的五个支线任务的通关钥匙🔑 (0/5),
请完成你的登神长阶
本文紧接着前篇的主线任务,建议没看《前篇》的小伙伴先去看一下《前篇》再回过头看本篇内容。(没浏览过前面文章的小伙伴,建议在父母的陪同下完成阅读。📖)
一. 思考 MyPromise 现有的问题
-
如果你跟进了之前的文章,那么下面应该是你目前的代码

-
虽然现在好像我们的逻辑都在顺着一步一步走,但是这里面其实有一个非常严重的 bug 。我们暂时先不揭秘,反过来我们先思考一下🤔,在原生的
Promise如果我们在构造器函数内多次调用resolve函数的话,它保存的值是会以哪次为准呢?
这里我们直接揭晓答案。结果是 Promise实例 只会保存第一次调用 resolve函数保存的那个数据。 -
但是现在反过头来看看我们目前的逻辑。
在控制台输出的结果是:
造成这种情况的原因非常简单,因为按照上面的写法,我们相当于在 executor里连续调用了三次resolve如下所示:
结果显而易见,它调用了三次,按照代码逻辑,它 result前两次的值被覆盖了,它保存了最后一次调用resolve时存入的值。 -
这里的解决方法依旧非常简单,我们不需要借助其它东西,我想你也能大概猜出来。这里还是需要借助
#state来判断我们是否继续执行过了resolve。
别忘了,我们在 constructor刚刚执行的时候最先修改的就是#state的值,所以我们只需要在resolve和reject函数执行之前,先判断当前#state是否不是pending,如果不为pendng则说明之前已经执行过了,则直接返回,不进行任何其他操作。reject同理。我们再看一下控制台现在的的样子。
嗯,现在确实不会再执行后面的 resolve函数了。
二. 异步数据的存储
-
写到这里你可能会发现,我们现在的
MyPromise其实是一个假的,是一个只能保存同步数据的普通类而已。 -
因为我们现在的
Promise是不能读取到异步存储到数据的。 这也是Promise的核心功能 保存异步数据 。因为我们向后端请求数据绝对不是一瞬间数据就过来的,而是会有时间的延迟,过一段时间才需要调用resolve去保存。 -
什么意思呢?我们先看原生的
Promise如果使用异步代码执行 resolve 的话是什么情况。 我们在executor函数体内开启一个定时器,在一秒以后去执行resolve保存数据。
我们在控制台看一下输出结果:
可以清楚的看到,我们在 then方法过了一秒,成功读取到了result中的数据。 -
现在我们回过头看看我们的
MyPromise是什么效果。
观察上面的代码,我们推算出在理想状态,控制台会输入一个数字 1。然而结果确实---控制台空空如也。
这是怎么回事呢?我们一步一步分析。 -
当我们的代码执行时,首先会去执行这一段代码。

-
根据前面的知识可以得出,我们的
executor函数会马上开始执行。
紧接着就遇到 setTimeout函数。经过前面的学习,我们知道setTimeout的回调函数会被放进宏任务队列,结果就是我们的resolve被放进了宏任务队列去乖乖排队去了, -
根据从上往下的执行顺序,马上就会去执行
then方法。
-
注意: 这时候我们需要去看
MyPromise类里的执行情况,才能知道原因
当我们的 executor执行后,我们的this.resolve会在一秒后才会执行,所以state的状态还是pending。 而此时我们的then又是在主线程执行的代码。所以自然而然,then函数不会有任何结果。 -
本着严谨的态度,我们进一步验证一下我们的推断是不是正确的。让我们在
then方法执行的时候打印一下this.state。
看一下控制台:
果然是这个原因,那现在怎么办呢?🤔
三. 构思异步存储数据的思路
-
我们现在要明确一点,我们上面的代码
resolve到底被调用了吗?会不会压根就是resolve没被调用才导致现在then拿不到数据呢? -
我们在
resolve里加一句打印,我们看看到底是不是这个原因。
-
在控制台可以清楚的看到,虽然没有第一时间执行,但是我们的
resolve是确确实实执行了的。
-
清楚了这一点,我们需要理清楚思路。既然你
resolve是在一秒后才会执行。如果我是then函数我可能会这样想:“resolve函数啊,如果你执行完毕了以后你再通知我该有多好啊,别让我一个人先走一步~” -
顺着这个思路,我们就要在
resolve这里构思有什么方法可以去通知then。
-
这里我们再想想,我们的数据是在哪读取的?
没错,是在 then函数的第一个回调函数onFulfilled里去读取的。那么有没有一种可能,我让你resolve去帮我执行这个onFulfilled函数不就更省心了吗?
这样我 then函数坐享其成不是更美吗? -
那么问题来了,
then函数的回调函数其实只能在then的作用域去调用,什么意思呢?我们给函数定义参数的时候,实际上是执行了下面的形参被实参重新赋值的操作。所以我们的参数对外是压根看不见的。
换而言之,resolve函数压根就不知道有onFulfilled这个函数!!!。 -
这就麻烦了,这怎么办呢?别急我们再定义一个变量,叫做
callBackFn,这个变量也是一个函数。
它用来干嘛呢?我们稍后揭秘,我想现在你的代码应该是下面这样子。

四. 神奇的回调函数
-
我们由上面可知,我们主要是因为
then方法在state==='pending'的时候,没办法做任何操作才无法拿到异步函数传递过来的数据的。
-
注意接下来是全文重点: 当我在
state==='pending'的时候,我把刚刚定义好的callBackFn函数值设置为then中onFulfilled回调函数的值。
接下来就是最神奇的一步操作,我再把 callBackFn放到resolve函数拿到数据之后执行!
别着急,我们先试一下行不行再一步一步解释原因。还是之前的代码,按照下面代码的逻辑,我们应该会在两秒以后看到控制台输出 看看行不行这个字符串。
我们看一下效果:
什么情况,还真可以!! -
不要觉得这是什么黑魔法,其实思路特别特别简单。顺着之前的思路,当我们的代码执行以后。首先会执行。
我们的 resolve就被丢进了宏任务队列里去了。
4.然后主线程继续往下走,就走到了 then 函数中。
接下来我们就有需要跳到 MyPromise 类中去看 then 函数调用后发生的情况。首先我们非常明确的是,这时候由于 resolve 还没执行,所以我们的 state 还是 pending 状态。
那么这时候就会走 then 函数的第一个判断逻辑。
它会将我们 then 函数的第一个参数 onFulfilled 赋值给我们之前定义好的 callBackFn 变量。
-
至此,任何其他事情还没发生,然后我们静等了两秒以后,
resolve函数从任务队列里被推进了主线程。我们需要转头去看resolve函数执行。
我们可以非常非常清晰的看到,谁在最后执行了?没错!!就是我们两秒之前 被赋值为 onFulfilled函数的callBackFn函数!!! -
千万不要迷,这里完完全全就等价于两秒后
resolve函数帮我去调了then函数的onfulfilled函数。
只不过我们没有办法直接去调用定义在 then函数作用域的那个onFulfilled,而是通过了一个中间变量的形式,“间接去调用了它”。这便是 JS 回调函数的魅力所在。
五. 修复 bug
-
但是现在我们又产生了一个新的问题,当我们的
resolve又变成了同步赋值的时候,我们看看是什么后果。
看一下控制台输出什么?

-
什么情况?我们的
callBacnFn又不是一个function了? -
你需要清晰的知道,如果我们的
resolve没有放进setTimeout里的执行的话,它就是一个同步代码,同步代码的话,它就会在then函数执行之前执行。
-
反应在
MyPromise类里面的执行过程就是。我们的callBacnFn在被赋值之前就被调用了,那肯定会报错啊,因为我们既没有给它赋初始值,又没有被then函数调用,所以它现在就是undefined。
-
那怎么办呢?其实非常非常简单,我们只需要在执行
callBackFn之前,判断一下它存在不存在就可以了。
由于是教学,我们写一种更加容易理解的代码,如果有我就执行,如果没有我就不执行,就是这么简单。

我们看一下控制台输出还有问题吗:
ok,没问题了~
结语:
至此距离我们完成自己的 MyPromise 类已经成功了一大半!我相信通过消化这一篇的内容,你会收获很多很多额外的知识。是不是有一种原来 Promise 不过如此的感觉~
其实有很多很多东西都是用很基本的函数,通过很巧妙的设计去完成一些看起来很复杂的逻辑。在下一章我们会迎来最后的几个关键点,如:微任务的创建,then 函数的链式调用,希望你能坚持下去。🎁
如果你暂时还没读懂,没关系,我建议你先去看一下我们上面的几个支线任务 再回过头细细品味本篇内容。距离我们手撕 Promise 已经近在咫尺了,你的登神长阶完成了几章呢?加油,一起进步呀!冲鸭~
转载自:https://juejin.cn/post/7177014014898077756