头图

前言: 阅读前需要拥有我们前面的五个支线任务的通关钥匙🔑 (0/5),

请完成你的登神长阶

本文紧接着前篇的主线任务,建议没看《前篇》的小伙伴先去看一下《前篇》再回过头看本篇内容。(没浏览过前面文章的小伙伴,建议在父母的陪同下完成阅读。📖)


一. 思考 MyPromise 现有的问题

  1. 如果你跟进了之前的文章,那么下面应该是你目前的代码
    image.png
  2. 虽然现在好像我们的逻辑都在顺着一步一步走,但是这里面其实有一个非常严重的 bug 。我们暂时先不揭秘,反过来我们先思考一下🤔,在原生的 Promise 如果我们在构造器函数内多次调用 resolve 函数的话,它保存的值是会以哪次为准呢?

    image.png

    image.png

    这里我们直接揭晓答案。结果是 Promise实例 只会保存第一次调用 resolve 函数保存的那个数据。
  3. 但是现在反过头来看看我们目前的逻辑。

    image.png

    在控制台输出的结果是:

    image.png

    造成这种情况的原因非常简单,因为按照上面的写法,我们相当于在 executor 里连续调用了三次 resolve 如下所示:

    image.png

    结果显而易见,它调用了三次,按照代码逻辑,它 result 前两次的值被覆盖了,它保存了最后一次调用 resolve 时存入的值。
  4. 这里的解决方法依旧非常简单,我们不需要借助其它东西,我想你也能大概猜出来。这里还是需要借助 #state 来判断我们是否继续执行过了 resolve

    image.png

    别忘了,我们在 constructor 刚刚执行的时候最先修改的就是 #state 的值,所以我们只需要在 resolvereject 函数执行之前,先判断当前 #state 是否不是 pending,如果不为 pendng 则说明之前已经执行过了,则直接返回,不进行任何其他操作。 reject 同理。我们再看一下控制台现在的的样子。

    image.png

    嗯,现在确实不会再执行后面的 resolve 函数了。

二. 异步数据的存储

  1. 写到这里你可能会发现,我们现在的 MyPromise 其实是一个假的,是一个只能保存同步数据的普通类而已。
  2. 因为我们现在的 Promise 是不能读取到异步存储到数据的。 这也是 Promise 的核心功能 保存异步数据 。因为我们向后端请求数据绝对不是一瞬间数据就过来的,而是会有时间的延迟,过一段时间才需要调用 resolve 去保存。
  3. 什么意思呢?我们先看原生的 Promise 如果使用异步代码执行 resolve 的话是什么情况。 我们在 executor 函数体内开启一个定时器,在一秒以后去执行 resolve 保存数据。

    image.png

    我们在控制台看一下输出结果:

    1.gif

    可以清楚的看到,我们在 then 方法过了一秒,成功读取到了 result 中的数据。
  4. 现在我们回过头看看我们的 MyPromise 是什么效果。

    image.png

    观察上面的代码,我们推算出在理想状态,控制台会输入一个数字 1。然而结果确实---控制台空空如也。

    image.png

    这是怎么回事呢?我们一步一步分析。
  5. 当我们的代码执行时,首先会去执行这一段代码。

    image.png
  6. 根据前面的知识可以得出,我们的 executor 函数会马上开始执行。

    image.png

    紧接着就遇到 setTimeout 函数。经过前面的学习,我们知道 setTimeout 的回调函数会被放进宏任务队列,结果就是我们的 resolve 被放进了宏任务队列去乖乖排队去了,
  7. 根据从上往下的执行顺序,马上就会去执行 then 方法。

    image.png
  8. 注意: 这时候我们需要去看 MyPromise 类里的执行情况,才能知道原因

    image.png

    当我们的 executor 执行后,我们的 this.resolve 会在一秒后才会执行,所以 state 的状态还是 pending。 而此时我们的 then 又是在主线程执行的代码。所以自然而然,then 函数不会有任何结果。
  9. 本着严谨的态度,我们进一步验证一下我们的推断是不是正确的。让我们在 then 方法执行的时候打印一下 this.state

    image.png

    看一下控制台:

    image.png

    果然是这个原因,那现在怎么办呢?🤔

三. 构思异步存储数据的思路

  1. 我们现在要明确一点,我们上面的代码 resolve 到底被调用了吗?会不会压根就是 resolve 没被调用才导致现在 then 拿不到数据呢?
  2. 我们在 resolve 里加一句打印,我们看看到底是不是这个原因。

    image.png
  3. 在控制台可以清楚的看到,虽然没有第一时间执行,但是我们的 resolve 是确确实实执行了的。
    2.gif
  4. 清楚了这一点,我们需要理清楚思路。既然你 resolve 是在一秒后才会执行。如果我是 then 函数我可能会这样想:“resolve 函数啊,如果你执行完毕了以后你再通知我该有多好啊,别让我一个人先走一步~”
  5. 顺着这个思路,我们就要在 resolve 这里构思有什么方法可以去通知 then

    image.png
  6. 这里我们再想想,我们的数据是在哪读取的?

    image.png

    没错,是在 then 函数的第一个回调函数 onFulfilled 里去读取的。那么有没有一种可能,我让你 resolve 去帮我执行这个 onFulfilled 函数不就更省心了吗?

    image.png

    这样我 then 函数坐享其成不是更美吗?
  7. 那么问题来了, then 函数的回调函数其实只能在 then 的作用域去调用,什么意思呢?我们给函数定义参数的时候,实际上是执行了下面的形参实参重新赋值的操作。所以我们的参数对外是压根看不见的。

    image.png

    换而言之,resolve 函数压根就不知道有 onFulfilled 这个函数!!!。
  8. 这就麻烦了,这怎么办呢?别急我们再定义一个变量,叫做 callBackFn,这个变量也是一个函数。

    image.png

    它用来干嘛呢?我们稍后揭秘,我想现在你的代码应该是下面这样子。

    image.png

四. 神奇的回调函数

  1. 我们由上面可知,我们主要是因为 then 方法在 state==='pending' 的时候,没办法做任何操作才无法拿到异步函数传递过来的数据的。

    image.png
  2. 注意接下来是全文重点: 当我在 state==='pending' 的时候,我把刚刚定义好的 callBackFn 函数值设置为 thenonFulfilled 回调函数的值。

    image.png

    接下来就是最神奇的一步操作,我再把 callBackFn 放到 resolve 函数拿到数据之后执行!

    image.png

    别着急,我们先试一下行不行再一步一步解释原因。还是之前的代码,按照下面代码的逻辑,我们应该会在两秒以后看到控制台输出 看看行不行 这个字符串。

    image.png

    我们看一下效果:

    1.gif
    什么情况,还真可以!!
  3. 不要觉得这是什么黑魔法,其实思路特别特别简单。顺着之前的思路,当我们的代码执行以后。首先会执行。

    image.png

    我们的 resolve 就被丢进了 宏任务队列里去了。

4.然后主线程继续往下走,就走到了 then 函数中。

image.png

接下来我们就有需要跳到 MyPromise 类中去看 then 函数调用后发生的情况。首先我们非常明确的是,这时候由于 resolve 还没执行,所以我们的 state 还是 pending 状态。
image.png

那么这时候就会走 then 函数的第一个判断逻辑。

image.png

它会将我们 then 函数的第一个参数 onFulfilled 赋值给我们之前定义好的 callBackFn 变量。

  1. 至此,任何其他事情还没发生,然后我们静等了两秒以后,resolve 函数从任务队列里被推进了主线程。我们需要转头去看 resolve 函数执行。

    image.png

    我们可以非常非常清晰的看到,谁在最后执行了?没错!!就是我们两秒之前 被赋值为 onFulfilled 函数的 callBackFn 函数!!!
  2. 千万不要迷,这里完完全全就等价于两秒后 resolve 函数帮我去调了 then 函数的 onfulfilled 函数。

    image.png

    只不过我们没有办法直接去调用定义在 then 函数作用域的那个 onFulfilled ,而是通过了一个中间变量的形式,“间接去调用了它”。这便是 JS 回调函数的魅力所在。

五. 修复 bug

  1. 但是现在我们又产生了一个新的问题,当我们的 resolve 又变成了同步赋值的时候,我们看看是什么后果。

    image.png

    看一下控制台输出什么?

    image.png
  2. 什么情况?我们的 callBacnFn 又不是一个 function了?
  3. 你需要清晰的知道,如果我们的 resolve 没有放进 setTimeout 里的执行的话,它就是一个同步代码,同步代码的话,它就会在 then 函数执行之前执行。

    image.png
  4. 反应在 MyPromise 类里面的执行过程就是。我们的 callBacnFn 在被赋值之前就被调用了,那肯定会报错啊,因为我们既没有给它赋初始值,又没有被 then 函数调用,所以它现在就是 undefined

    image.png
  5. 那怎么办呢?其实非常非常简单,我们只需要在执行 callBackFn 之前,判断一下它存在不存在就可以了。

    image.png

    由于是教学,我们写一种更加容易理解的代码,如果有我就执行,如果没有我就不执行,就是这么简单。

    image.png


    我们看一下控制台输出还有问题吗:

    image.png

    ok,没问题了~

结语:

至此距离我们完成自己的 MyPromise 类已经成功了一大半!我相信通过消化这一篇的内容,你会收获很多很多额外的知识。是不是有一种原来 Promise 不过如此的感觉~

其实有很多很多东西都是用很基本的函数,通过很巧妙的设计去完成一些看起来很复杂的逻辑。在下一章我们会迎来最后的几个关键点,如:微任务的创建then 函数的链式调用,希望你能坚持下去。🎁

如果你暂时还没读懂,没关系,我建议你先去看一下我们上面的几个支线任务 再回过头细细品味本篇内容。距离我们手撕 Promise 已经近在咫尺了,你的登神长阶完成了几章呢?加油,一起进步呀!冲鸭~


FFF方
453 声望12 粉丝