2
头图

前言: 可能初次看到这个标题,你会有些惊讶。我们不是要实现“手写 Promise ”吗?怎么变成了手写“回调地狱”了?“我老早看视频学习的时候就知道,我们要避免写成“回调地狱的格式,怎么到你这还要手写这玩意?博主你老标题党了...”

我相信有很多学习前端的小伙伴百分百遇到过这样的面试题:

---“为什么我们要用 Promise 去代替传统的回调函数?”

我相信有很多人都可以随口回答出:“为了避免回调地狱,因为回调地狱会带来xxx的后果....

ok,那么现在我问你,假设现在面试官让你实现一个 “回调地狱”。你脑子里的代码会是怎样的呢?我建议你停下来思考三分钟🤔...

不要问为什么有这么令人无语的问题,因为这就是我实实在在的面试题之一。起初我觉得面试官在刁难我,然而当我真正理解了这个知识点以后,我非常感谢那位面试官,在去研究这个面试题答案的过程中,让我对 JS 有了更深层次的理解...所以在手写 Promise 之前,我希望你能先完成手写 回调地狱

往下阅读之前,请自觉领取并完成阅读本文的前置任务(0/2)

<hr/>

一. 手写回调地狱

  1. 我们假设现在有这样的一个场景:

    我们前端通过<input/>框,获取到了用户输入的年龄以后,前端需要把这个年龄数值传递给后端。然后后端拿到这个年龄数据经过处理计算,会在 1s 后返回一个年龄 +1 的结果给我们。
    image.png

    经过上篇的知识可得到,我们按上面的写法,result 将会是 undefined。我们如果想要拿到正确的数值,就需要给 addUserAge 传递一个回调函数。
  2. 所以正确的写法应该是这样:

    image.png
  3. 好的,现在我们拿到了后端传递给我们的第一次数据。
    QQ20221211-161726-HD.gif
  4. 接下来,我们所有的逻辑代码都需要在这个回调函数内部去写。
    image.png

    什么意思呢?凡事需要用到 age+1 这个值的代码,都需要写进 addUserAge 函数的回调函数里。
  5. 我们更具体的表现出上面这句话的含义。假设下面的某个页面场景,又需要向后端发起一次请求,让后端再把用户的年龄 +1,我们的页面才能呈现出正确的样式。这怎么办呢?

    ----注意: 这里为了方便区分,我们把第一次拿到的结果写为 result1 然后赋值给局部变量 age1,第二次的结果写为 result2,赋值给 age2,以此类推。
    image.png
    我们可以看到,第一秒返回了 数字2,再过一秒返回了 数字3
    2.gif
    (tips:我这里再次提示,如果你这一步没看懂,我真心希望你们回过头先去看我另外两篇支线任务的内容再来往下继续看。因为这里是一个难点,确实不是第一次看就能直接明白什么意思的,有难度的知识往往都是基础知识堆积而成的,一定要脚踏实地慢慢来)
  6. ok,我们两次的结果都正确拿到了。但是!我想你已经猜到了,我们在实际开发中,请求绝对不只两次。后面的页面,又又又需要我们再次将年龄 +1 然后才能正确展示,怎么办呢?注意!你后面的结果都是依赖上一步的结果进行的,所以我们又需要传递一个回调函数给 addUserAge 。可想而知,我们的后面的逻辑又只能在第三个 addUserAge 的回调函数内书写。
    image.png
    结果如下:
    3.gif
  7. 聪明的你也可能猜到,同理,后面的某个页面需要拿到 age+6 的结果怎么办呢?也就是我需要调用 addAge 函数 6 次,我们的代码结构就会变成下面的这个样子

    image.png
    4.gif
  8. 上面的代码是我为了清晰的展示才调整的空行,假如我们现在没有空行。
    image.png
    看到黄色的金字塔了吗?这就是我们俗称的回调地狱(又称死亡金字塔)的由来。
    你会发现这种代码读起来是真的又臭又长,可读性极差,可维护性极差。这还仅仅只是一个小的功能页面就已经堆叠成这样了,我们还没有做任何复杂的逻辑运算。

二. Promise 的出现

  1. 如你所见,用上面的“回调地狱”写法写出来的代码,毫不夸张的说就是屎山。你敢这样写,公司就不敢辞退你。因为这代码只有你能看懂!哈哈,开个笑话,接下来的日子里你看的懂还好,最糟糕的情况就是过了两天有可能连你自己都看不懂这坨代码了...🍦
  2. 这时候就迫切的需要一种解决方法来避免上面的书写方式。接下来有请我们的重量级嘉宾 Promise。👏我们先看这个单词的意思是什么。
    image.png
    承诺,保证”我们需要先理清设计者为什么要用 “承诺” 来表达这个构造函数。

    ⚠️注意:

    这里的“承诺”并不是指我保证给你一个“成功”的答复,而是指“不管成功还是失败”我都会通知你。更人性化的表达方式就是:我一定会给你一个答复,而不是我一定会给你一个“满意”的答复。
  3. 如果你明白了上面的这句话。那么接下来让我们通过写代码加深一下理解。首先 Promise 是一个类,那么们就可以通过 new 去调用。

    image.png
  4. 看到报错了吗?我们先看一下错误信息。

    image.png

    它好像提示我们少了一个参数,参数的名字叫 executor。那这个executor 又是个什么呢?🤔
  5. 真的不用太害怕,它就是一个普通的函数起了个洋气的名字而已。并且它是作为了参数传递给你 Promise 构造函数,那么它就是一个普通的回调函数而已。(真的,它就是一个普普通通的函数而已,不要把它想的太过神奇了。)
  6. OK,你说我少给你一个函数作为参数,那我给你不就行了吗?

    image.png

    好像确实没报错了。但是你是不是忽略了什么事情?我们回过头看一下这个回调函数的介绍。
    image.png
    英语不好的同学我强烈建议你去搜一下这段话的原意,我在这里简单的表达一下大概意思。
这个回调函数会用来初始化这个 promise 实例。这个回调函数会被传递两个参数,(is passed 注意这里需要理解是“被动语态”,这里是会“被传递”的意思。)一个叫 resolve的回调函数 一个叫 reject的回调函数
  1. 可以得到下面的写法。
    image.png

    这里需要特别注意,在这里 resolvereject 是实参而不是形参。什么意思呢?意思就是它是可以直接被调用的。它是被 Promise 传递过来的,形参是在 Promise 类里定义的。这点我们会在后面的《手写 Promise》 里解释

    image.png
  2. 好像还是有错,我们再看看。

    image.png

    这里它表达的意思不是特别好理解,我来简单解释一下。

    resolve 函数的参数,就是我们之前去请求后端获得的那个返回值
  3. 还记得我们之前的写法吗,我们是拿不到 result
    image.png
    接下来我们换一种写法,把它改造成 Promise 的写法。
    image.png
    注意:我们在这里为了模拟数据过了一会才能回来的场景,在实际项目中,setTimeout 那段函数其实就是我们向后端发请求的函数。
  4. 现在我们知道,这个改造的 addUserAge1 函数会返回一个 Promise 实例。

    image.png

    我们先看一下这个实例身上到底是什么样子的。

    image.png

    我们这里我们关键先看这两个属性,我们的结果好像被保存到了一个叫 [[PromiseResult]] 的属性中。还有一个属性叫做 [[PromiseSate]]

三. Promise 的三个状态

1.这里我们先说明, [[PromiseSate]] 有三种状态 pendding,fulfilledrejected。分别对应着数“据存放之前”,“数据存放成功”,“和数据存放失败”三种情况。

  1. “数据存放之前”是指你还没通过 resolve 或者 reject 去存放数据的时候。

image.png
image.png

  1. “数据存放成功”就是上面我们刚刚调用 resolve 的结果,这里不再重复。
  2. “和数据存放失败”就是我们调用 reject 函数保存的数据。
    image.png
    image.png
  3. 那既然我已经看到了放在 [[PromiseResult]] 的结果,我该怎么去取呢?非常简单。

    image.png

    Promise 实例身上为我们提供了一个 then 方法。并且会把 [[PromiseResult]] 存放的值,传递给 then 方法的第一个回调函数的参数里。

    image.png

    我们看一下结果:

    image.png

    如果读过我之前文章的小伙伴,这种写法不知道你还熟悉吗?

    image.png

    如果这个能看懂,我相信你读懂后面的 《手写Promise》 一定没问题!!🎁
  4. 如果是 reject 保存的数据,那么他会被传递给 then 方法的第二个回调函数的参数中。
    image.png

ok,我们休息一下,不再讲 cath 等这些方法了,让我们慢慢消化一下。

四. 回到最初的问题

  1. 现在我们大致了解了 Promise 的基本原理。我们之前说过,“回调地狱” 的产生是因为我需要多次请求后端的数据,多次嵌套调用了回调函数,才产生了 金字塔 一样的代码结构。那我们的 Promise 是怎么就避免了这个呢?
  2. 下面的内容我们先忽略 reject ,我们只要搞懂 resolvereject 我相信你会同样会马上理清楚。
    image.png
  3. 现在还是老问题,我下面的页面需要依靠 age1 继续 +1 才能得到正确的样式,我们先来看结论。

    image.png

    看一下结果:

    5.gif
  4. 你发现了什么吗?

    image.png

    我在这里 return 了另一个 Promise 实例,然后我就可以接着 then 下去。

    这里直接引出:then 方法的返回值会被当重新包装成 Promise 实例,完成“链式调用”
  5. 聪明的你就可以猜到,我们接下来如果多次 age+1 的话。我就一直重复 then--> return --> then --> return 就行了。

    image.png

    6.gif
  6. 可以看出上面的代码写法,并没有出现一直向右倾斜的情况,要比之前代码的可读性强非常多的。这其实就是 Promise 帮我们完成的事情。

结语:

其实 Promise 相关还有非常多的知识,如 cath,finally等这些, 我单独一章讲完等话,害怕有的读者会消化不了。所以就把核心的概念先抛出来,如果你顺利的读懂里本文,那剩下没讲到的那些方法,我希望你可以自行去了解,我只能做一个引路人把核心概念给你捋清楚,打渔还需你自己。

如果有的小伙伴感觉没太读懂,我还是希望你能回过头把这两篇文章读熟,读透,再返过来读本篇文章。

如果你跟进了后续的《手写 Promise》,我相信你会看到一个不一样的 JS 世界。🎁~


FFF方
451 声望12 粉丝