注:文章转载自:前端陌上寒
昨天我们介绍了javascript异步的相关内容,我们知道javascript以同步,单线程的方式执行主线程代码,将异步内容放入事件队列中,当主线程内容执行完毕就会立即循环事件队列,直到事件队列为空,当用产生用户交互事件(鼠标点击,点击键盘,滚动屏幕等待),会将事件插入事件队列中,然后继续执行。
处理异步逻辑最常用的方式是什么?没错这就是我们今天要说的—-回调

js回调函数

如你所知,函数是对象,所以可以存储在变量中,
所以函数还有以下身份:

  1. 可以作为函数的参数
  2. 可以在函数中创建
  3. 可以在函数中返回

当一个函数a以一个函数作为参数或者以一个函数作为返回值时,那么函数a就是高阶函数
什么是回调函数?
百度百科

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

维基百科

在计算机程序设计中,回调函数,或简称回调(Callback 即call then back 被主函数调用运算后会返回主函数),是指通过函数参数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。

回调函数,几乎每天我们都在用

  1. setTimeout(() => {
  2. console.log("这是回调函数");
  3. }, 1000);
  4. const hero=['郭靖','黄蓉']
  5. hero.forEach(item=>{
  6. console.log(item);
  7. })

回调函数解决了哪些问题

举一个简单的:

  1. let girlName = "裘千尺"
  2. function hr() {
  3. girlName = "黄蓉"
  4. console.log(`我是${girlName}`);
  5. }
  6. function gj() {
  7. console.log(`${girlName}你好,我是郭靖,认识一下吧`);
  8. }
  9. hr()
  10. gj()

输出,重点看输出顺序

  1. //=>我是黄蓉
  2. //=>黄蓉你好,我是郭靖,认识一下吧

上面的代码输出是没什么悬念的,不存在异步,都单线程同步执行,最后郭靖和黄蓉相识
如果这时候黄蓉很忙,出现了异步,会怎么样?

  1. let girlName = "裘千尺"
  2. function hr() {
  3. setTimeout(() => {
  4. girlName = "黄蓉"
  5. console.log('我是黄蓉');
  6. }, 0);
  7. }
  8. function gj() {
  9. console.log(`${girlName}你好,我是郭靖,认识一下吧`);
  10. }
  11. hr()
  12. gj()

输出,重点看输出顺序

  1. //=>裘千尺你好,我是郭靖,认识一下吧
  2. //=>我是黄蓉

虽然定时器是0ms,但是也导致了郭靖和黄蓉的擦肩而过,这不是我们期望的结果,hr函数存在异步,只有等主线程的内容走完,才能走异步函数
所以最简单的办法就是使用回调函数解决这种问题,gj函数依赖于hr函数的执行结果,所以我们把gj作为hr的一个回调函数

  1. let girlName = "裘千尺"
  2. function hr(callBack) {
  3. setTimeout(() => {
  4. girlName = "黄蓉"
  5. console.log('我是黄蓉');
  6. callBack()
  7. }, 0);
  8. }
  9. function gj() {
  10. console.log(`${girlName}你好,我是郭靖,认识一下吧`);
  11. }
  12. hr(gj)

输出,重点看输出顺序

  1. //=>我是黄蓉
  2. //=>黄蓉你好,我是郭靖,认识一下吧

⚠️:当回调函数作为参数时,不要带后面的括号!我们只是传递函数的名称,不是传递函数的执行结果
上面小🌰貌似的很简单,我们继续

嵌套回调和链式回调

我们把昨天的demo做一下升级
引入了lodash:处理按钮点击防抖
axios,集成了promise,但promise不是我们今天讨论的内容,我们只是使用了axios的ajax请求接口功能
easy-mock:接口数据,用来实现ajax请求(数据是假的,但是请求是真的)

嵌套回调

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>javascript回调</title>
  8. <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  9. <script src="https://cdn.bootcss.com/lodash.js/4.17.11/lodash.min.js"></script>
  10. </head>
  11. <body>
  12. <button>点击</button>
  13. <script>
  14. {
  15. const btn = document.querySelector('button')
  16. btn.onclick = () => {
  17. _.debounce(() => {
  18. axios.get('https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock')
  19. .then(data => {
  20. console.log("ajax返回成功");
  21. myData = data.data
  22. console.log(myData);
  23. })
  24. .catch(error => {
  25. console.log("ajax返回失败");
  26. })
  27. }, 500)()
  28. }
  29. }
  30. </script>
  31. </body>
  32. </html>

仔细看代码,不难发现,这是一个典型的嵌套回调,我们分析一下
第一层异步,用户交互,来自按钮的点击事件
第二层异步,按钮去抖,来自lodash下debounce的500ms延时
第三次异步,ajax请求,处理后台接口数据
拿到数据后我们没有继续做处理,在实际工作中可能还存在异步,还会继续嵌套,会形成一个三角形的缩进区域

image.png

再继续嵌套,就会形成所说的“回调地狱”,就是回调的层级太多了,代码维护成本会高很多
上面的🌰最多算是入门级回调地狱,我们看一下这个

  1. function funA(callBack) {
  2. console.log("A");
  3. setTimeout(() => {
  4. callBack()
  5. }, 10);
  6. }
  7. function funB() {
  8. console.log("B");
  9. }
  10. function funC(callBack) {
  11. console.log("C");
  12. setTimeout(() => {
  13. callBack()
  14. }, 100);
  15. }
  16. function funD() {
  17. console.log("D");
  18. }
  19. function funE() {
  20. console.log("E");
  21. }
  22. function funF() {
  23. console.log("F");
  24. }
  25. //从这里开始执行
  26. funA(() => {
  27. funB()
  28. funC(() => {
  29. funD()
  30. })
  31. funE()
  32. })
  33. funF()

(这段代码,带回调的都是异步逻辑)你能很快的看出这段代码的执行顺序吗?
顺序如下:A、F、B、C、E、D
一般正常人不会这么嵌套多层,层级一多,就会考虑拆分

链式回调

  1. const btn = document.querySelector('button')
  2. //监听按钮点击事件
  3. btn.onclick = () => {
  4. debounceFun()
  5. }
  6. //去抖动
  7. const debounceFun = _.debounce(() => {
  8. ajax()
  9. }, 500)
  10. //ajax 请求
  11. const ajax = function () {
  12. axios.get('https://easy-mock.com/mock/5b0525349ae34e7a89352191/example/mock')
  13. .then(data => {
  14. console.log("ajax返回成功");
  15. myData = data.data
  16. console.log(myData);
  17. })
  18. .catch(error => {
  19. console.log("ajax返回失败");
  20. })
  21. }

我相信很多人都会通过这种链式回调的方式处理异步回调,因为可读性比嵌套回调要搞,但是维护的成本可能要高很多
上面的栗子🌰,三个异步函数之间只有执行顺序上的关联,并没有数据上的关联,但是实际开发中的情况要比这个复杂,

回调函数参数校验

我们举一个简单的栗子

  1. let girlName = "裘千尺"
  2. function hr(callBack) {
  3. setTimeout(() => {
  4. girlName = "黄蓉"
  5. console.log('我是黄蓉');
  6. callBack(girlName)
  7. }, 0);
  8. }
  9. function gj(love) {
  10. console.log(`${girlName}你好,我是郭靖,认识一下吧,我喜欢${love}`);
  11. }
  12. hr(gj)

gj作为hr的回调函数,并且hr将自己的一个变量传递给gj,gj在hr的回调中执行,
仔细看这种写法并不严谨,
如果gj并不只是一个function类型会怎么样?
如果love的实参并不存在会怎么样?
况且这只是一个简单的栗子
所以回调函数中,参数的校验是很有必要的,回调函数链拉的越长,校验的条件就会越多,代码量就会越多,随之而来的问题就是可读性和可维护性就会降低。

还是回调函数的校验

但我们引用了第三方的插件或库的时候,有时候难免要出现异步回调的情况,一个🌰:
xx支付,当用户发起支付后,我们将自己的一个回调函数,传递给xx支付,xx支付比较耗时,执行完之后,理论上它会去执行我们传递给他的回调函数,是的理论上是这样的,我们把回调的执行权交给了第三方,隐患随之而来
第三方支付,多次调用我们的回调函数怎么办?
第三方支付,不调用我们的回调函数怎么办?
当我们把回调函数的执行权交给别人时,我们也要考虑各种场景可能会发生的问题

总结一下:
回调函数简单方便,但是坑也不少,用的时候需要多注意校验