前言: 回想当初第一次看到“回调函数”这个名词的时候,真的快把我难哭了。所有视频教程在讲到某个知识点的时候,大概都会说一句:“啊,这里怎么办呢?这里我们就需要用到一个回调函数...”。
等等,喂,关键是你还没讲什么是回调函数啊!,你让我怎么往下继续听啊...于是留下我一个人看着后面几十分钟的视频,但是脑子里还在纠结这个“回调函数”到底是什么玩意。
注意: 本文的前提知识是你需要了解 href="https://juejin.cn/post/7172948984145641479">JS 的执行机制。我强烈建议你先熟悉前提知识点再往下看,因为我们最终的目的是手写 Promise ,但是之前讲解的这些知识点是需要你必须掌握的。
一. 函数
- 回调函数的基本概念我之前的文章虽然有些过,但是为了引入下文,在这里还是简单再提一嘴。我们先看一下 MDN 的解释。
2.什么意思呢?还拿我们上一章节的例子。一个 setTimeout 里,隔了1秒去控制台打印一个 conole.log() 。
3.让我们拆解一下。我们仔细看下面的一行代码,你发现了吗?我只不过是把声明一个箭头函数,变为了定义一个匿名函数。其实这两种写法的所产生的效果是一模一样的。(请暂时不要考虑箭头函数和普通函数的区别)
在这种情况下,上面的匿名函数和箭头函数统称为函数 setTimeout 的回调函数。你一定需要明白的事情就是 setTimemout 也是一个函数! 单独的函数是无法被叫做回调函数的,回调函数的概念是相互的。所以更确切的说法应该是:
首先这里的 箭头函数和匿名函数作为 setTimout 的参数, 然后在 setTimout 内部被调用,最后 箭头函数和匿名函数才被称为 setTimout 的回调函数。
4.我发现其实有很多新手是没有搞懂函数不同的声明方式,所以才导致你们迷惑的。我们这里先来讲一下函数常用的三种定义方式。
在这里虽然这三个函数的执行结果是一样的,但是它们的区别是不小的。使用 function 关键字声明一个函数。被叫做函数声明。 而下面的两种方法是被叫做函数表达式。
5. 虽然在使用方法上我们都是通过 "()" 去调用,但是其实它们还是有差别的。其中最重要的一个差别就是,使用函数声明定义的一个函数,该变量会被 js 解析器提升到代码开始执行之前。而使用函数表达式定义的函数则不会被提升。什么意思呢?
你会发现,我们竟然可以在定义 hello
函数之前去使用这个函数。而下面两个函数都报出了在赋值之前调用的错误。
6. 在这里我们再延伸一个知识点。我们在定义 hello
函数之前,首先定义一个 变量 name
。
虽然这里看起来 name
是在 hello
函数之前声明的,但是 hello
这个变量的声明其实是在变量 name
之前的。
7. 其原因用简单的描述就是:js 解析器会在到你这个.js
文件后,会优先去查找使用 function
声明的函数名称,然后把这些函数名保存在一个对象中,并且提前创建一个引用来-->指向这个函数体。 具体细节还需读者自主查询,本文重点不在这里。
8. 如果你读懂了上面的几个环节,那么我觉得这种写法你同样应该明白了。
那么我们趁热打铁,开启下一篇章。
二. 数据处理方式
- 我们都知道函数是为了帮助我们完成一些复杂的逻辑运算,可以反复调用的,并且函数是可以传递参数的。
比如上面的函数,允许我们传递两个数字类型的变量,然后该函数会把这两个数字的和返回给我们。
2. ok,上面的内容很简单,我想大家应该都明白。如果我们日常和后端打交道的数据传输没有延迟,那么自然而然我们对数据处理起来就会非常方便。什么意思呢?假设我们 向后端发送请求获得数据 ,这一过程是一瞬间完成的,那么我们就可以顺着我们的想法从上往下写非常顺畅。
如上面刚写的的函数一样,你可以理解为我们向后端服务器请求了一个数据,数据一瞬间就返回了,那么们就可以把这个 data 给返回出去。然后在下面用一个变量去接受一下,随之进行一些运算。
3. 就如上一篇文章讲的那样。很遗憾的是,我们在进行一些可能需要一定时间才可以完成的任务的时候,不可能是同步完成的,因为一行代码而阻塞后面整段代码的执行是非常不可取的事情。
4. 这里我们来简单模拟一个场景。
我们定义了一个普通对象 data
来模拟后端即将传递给我们的数据。我们假设这个数据的可能需要花费 1s 的时间才能返回。我们可能自然而然的想到,那不是和之前一样嘛,在 setTimeout
里返回不就行了吗。
你可以先思考一下上面的这段代码,userData
会拿到正确的 data
值吗?
5. 非常遗憾的是,我们是无法在同步的代码里第一时间拿到异步函数的返回值的。
那么造成上面的代码结果的原因是为什么?为什么是 undefined
呢?我们一步一步分析。
三. 理清思路
- 当下面画黄线的代码被执行的时候。紧接着下一步我们就需要马上跳进
getData
的函数体中去分析。
2. 进到这个函数里,首先就是执行 setTimeout 函数。 (我在这里再再再亿次强调,setTimeout 本身就是一个普通函数,没什么特别的。)
3. 但是 setTimeout 函数会把接收的回调函数给推送到任务队列里去排队。什么?你没看见回调函数?这不就是文章最开头说的函数的声明方式之一箭头函数吗?怎么这么快就忘了?自觉翻回上面去再看一遍⏰。
4. 紧接着 getData函数
执行完毕,看一眼,函数本身没有返回值,那么默认返回值为 undefined。
5. 这里需要特别注意! 我们如果在这里写了 return语句
,这样才能算是 getData函数的
返回值。
而在我们的 data
却是 setTimout 的回调函数的返回值,你能拿到才怪。
6. 那怎么办呢?我们继续往下看。
四. 回调函数
- 别着急往下看,经过上面的三个环节,我们先做个小测试 。
上面这段代码你看懂了吗?(下面是执行结果)
如果你对执行结果不是特别清楚,没关系。到这一步我只希望你能读懂下面这行代码,至少对于写法上,很清楚的知道为什么可以这样写
如果你暂时还没搞懂,那么我强烈建议你至少回过头再品味一下第一个标题 函数,这次要细读。
2.如果你读懂了,恭喜你 ,你距离理解这个名词更近了一步。接下来我们稍微改造一下代码。
3.上面这段代码乍一看很玄乎,别着急,我们慢慢分析。首先 modifyData函数
是我们提前设定好的一个处理后端数据的函数。(前提是如果我们能拿到后端给我们的数据)。
4.既然我们没有办法在同步函数里拿到 data 后再去处理,那么我们就干脆提前给后端代码函数 getData
一个参数。
一个我们提前设定好的处理函数。当 setTimeout 的回调函数拿到数据后,就帮我们执行这个函数,也就是modifyData 函数
。然后把 data 作为参数传递给modifyData函数
。
5. 我们在 setTimout 的回调函数中打印一下修改前后的值。
6. 我们会发现,我们之后的代码都在回调函数里去处理的话,就能完美实现需求。也就是后端传过来的数据我们可以拿到了,并且可以处理成我们想要的数据了。
结语
在这里我们就先不引入解回调地狱的概念了了,我害怕都的读者确实会绕迷糊。 我会在放在手写 Promise 之前,引入这个概念。 希望读者可以细细体会上面的代码,一旦学会这些概念,你会看到和之前不一样的 js 世界 。