手写“回调地狱”
前言: 可能初次看到这个标题,你会有些惊讶。我们不是要实现“手写 Promise ”吗?怎么变成了手写“回调地狱”了?“我老早看视频学习的时候就知道,我们要避免写成“回调地狱的格式,怎么到你这还要手写这玩意?博主你老标题党了...”
我相信有很多学习前端的小伙伴百分百遇到过这样的面试题:
---“为什么我们要用 Promise 去代替传统的回调函数?”
我相信有很多人都可以随口回答出:“为了避免回调地狱,因为回调地狱会带来xxx的后果....”
ok,那么现在我问你,假设现在面试官让你实现一个 “回调地狱”。你脑子里的代码会是怎样的呢?我建议你停下来思考三分钟🤔...
不要问为什么有这么令人无语的问题,因为这就是我实实在在的面试题之一。起初我觉得面试官在刁难我,然而当我真正理解了这个知识点以后,我非常感谢那位面试官,在去研究这个面试题答案的过程中,让我对 JS 有了更深层次的理解...所以在手写 Promise 之前,我希望你能先完成手写 回调地狱。
往下阅读之前,请自觉领取并完成阅读本文的前置任务(0/2)
一. 手写回调地狱
我们假设现在有这样的一个场景:
我们前端通过<input/>
框,获取到了用户输入的年龄以后,前端需要把这个年龄数值传递给后端。然后后端拿到这个年龄数据经过处理计算,会在 1s 后返回一个年龄 +1 的结果给我们。
经过上篇的知识可得到,我们按上面的写法,result 将会是 undefined。我们如果想要拿到正确的数值,就需要给 addUserAge 传递一个回调函数。
所以正确的写法应该是这样:
好的,现在我们拿到了后端传递给我们的第一次数据。
接下来,我们所有的逻辑代码都需要在这个回调函数内部去写。
什么意思呢?凡事需要用到 age+1 这个值的代码,都需要写进 addUserAge
函数的回调函数里。
我们更具体的表现出上面这句话的含义。假设下面的某个页面场景,又需要向后端发起一次请求,让后端再把用户的年龄 +1,我们的页面才能呈现出正确的样式。这怎么办呢?
----注意: 这里为了方便区分,我们把第一次拿到的结果写为 result1
然后赋值给局部变量 age1
,第二次的结果写为 result2
,赋值给 age2
,以此类推。
我们可以看到,第一秒返回了 数字2,再过一秒返回了 数字3
(tips:我这里再次提示,如果你这一步没看懂,我真心希望你们回过头先去看我另外两篇支线任务的内容再来往下继续看。因为这里是一个难点,确实不是第一次看就能直接明白什么意思的,有难度的知识往往都是基础知识堆积而成的,一定要脚踏实地慢慢来)
ok,我们两次的结果都正确拿到了。但是!我想你已经猜到了,我们在实际开发中,请求绝对不只两次。后面的页面,又又又需要我们再次将年龄 +1 然后才能正确展示,怎么办呢?注意!你后面的结果都是依赖上一步的结果进行的,所以我们又需要传递一个回调函数给 addUserAge
。可想而知,我们的后面的逻辑又只能在第三个 addUserAge
的回调函数内书写。
结果如下:
聪明的你也可能猜到,同理,后面的某个页面需要拿到 age+6 的结果怎么办呢?也就是我需要调用 addAge
函数 6 次,我们的代码结构就会变成下面的这个样子
上面的代码是我为了清晰的展示才调整的空行,假如我们现在没有空行。
看到黄色的金字塔了吗?这就是我们俗称的回调地狱(又称死亡金字塔)的由来。 你会发现这种代码读起来是真的又臭又长,可读性极差,可维护性极差。这还仅仅只是一个小的功能页面就已经堆叠成这样了,我们还没有做任何复杂的逻辑运算。
二. Promise 的出现
如你所见,用上面的“回调地狱”写法写出来的代码,毫不夸张的说就是屎山。你敢这样写,公司就不敢辞退你。因为这代码只有你能看懂!哈哈,开个笑话,接下来的日子里你看的懂还好,最糟糕的情况就是过了两天有可能连你自己都看不懂这坨代码了...🍦
这时候就迫切的需要一种解决方法来避免上面的书写方式。接下来有请我们的重量级嘉宾 Promise。👏我们先看这个单词的意思是什么。
“承诺,保证”我们需要先理清设计者为什么要用 “承诺” 来表达这个构造函数。
⚠️注意:
这里的“承诺”并不是指我保证给你一个“成功”的答复,而是指“不管成功还是失败”我都会通知你。更人性化的表达方式就是:我一定会给你一个答复,而不是我一定会给你一个“满意”的答复。
如果你明白了上面的这句话。那么接下来让我们通过写代码加深一下理解。首先 Promise 是一个类,那么们就可以通过 new 去调用。
看到报错了吗?我们先看一下错误信息。
它好像提示我们少了一个参数,参数的名字叫 executor
。那这个executor 又是个什么呢?🤔
真的不用太害怕,它就是一个普通的函数起了个洋气的名字而已。并且它是作为了参数传递给你 Promise 构造函数,那么它就是一个普通的回调函数而已。(真的,它就是一个普普通通的函数而已,不要把它想的太过神奇了。)
OK,你说我少给你一个函数作为参数,那我给你不就行了吗?
好像确实没报错了。但是你是不是忽略了什么事情?我们回过头看一下这个回调函数的介绍。
英语不好的同学我强烈建议你去搜一下这段话的原意,我在这里简单的表达一下大概意思。
这个回调函数会用来初始化这个 promise 实例。这个回调函数会被传递两个参数,(is passed 注意这里需要理解是“被动语态”,这里是会“被传递”的意思。)一个叫
resolve的回调函数
一个叫reject的回调函数
。
可以得到下面的写法。
这里需要特别注意,在这里 resolve
和 reject
是实参而不是形参。什么意思呢?意思就是它是可以直接被调用的。它是被 Promise 传递过来的,形参是在 Promise 类里定义的。这点我们会在后面的《手写 Promise》 里解释。
好像还是有错,我们再看看。
这里它表达的意思不是特别好理解,我来简单解释一下。
resolve
函数的参数,就是我们之前去请求后端获得的那个返回值
还记得我们之前的写法吗,我们是拿不到 result
的
接下来我们换一种写法,把它改造成 Promise 的写法。
注意:我们在这里为了模拟数据过了一会才能回来的场景,在实际项目中,setTimeout 那段函数其实就是我们向后端发请求的函数。