前言: 阅读前需要拥有我们前面的五个支线任务的通关钥匙🔑 (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
已经近在咫尺了,你的登神长阶完成了几章呢?加油,一起进步呀!冲鸭~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。