新手也能读懂JavaScript中的"异步"和"回调地狱"

286 阅读5分钟

序言

在实际开发中我们经常会遇到一个程序的运行需要接收上一个程序运行结果的参数才能完成,而且上一个程序的运行需要一定的时间,如果我们直接正常让函数依次执行,会导致结果的输出错乱。举个通俗易懂的例子,我们在用微信发信息的过程其实就是HTTP请求-响应模式的利用,而在传输的过程可能会由于网络的原因而丢包,如果网络不好,可能你发了一连串的信息,一直在等待响应,然后网络响应后,信息都发出去了但是顺序却错乱了。要解决这个很常见的问题,就得好好聊一聊我们今天要讲的主题异步回调地狱

异步

function a (){ //称a函数正在异步执行代码
    setTimeout(function(){
         console.log('我得先把菜做完才能刷抖音')
        },1000)
    }
    a()
    function b (){
         console.log("可以刷抖音啦!")
    }
    b()

在运行这段代码后,我们会看到"可以刷抖音啦!"这句话会先打印出来,然后是一秒后才会打印"我得先把菜做完才能刷抖音"。这是因为JavaScript中的setTimeout方法会安排一个回调函数在指定的时间间隔之后执行,但它不会阻塞后续的代码执行,这时候我们就称函数a正在异步执行代码。

但是这并不是我们想要的结果,我们希望执行顺序是先把菜做完才能刷抖音,那么我们思考思考有什么办法吗?

有朋友可能会觉得把函数b放在函数a里面,并且等函数a里面的setTimeout方法执行完毕在执行函数b,那我们可以将代码改成下面这种,来看看实际效果!

function a(cb){
    setTimeout(() => { 
        console.log("我得先把菜做完才能刷抖音");
        cb() 
    },1000)
}

function b(){ 
    setTimeout(() => {
        console.log("可以刷抖音啦!");
    }, 0);
}

    a(b) //输出 "我得先把菜做完才能刷抖音"  "可以刷抖音啦!"

在这段代码中,函数a接受一个回调函数cb作为参数,并在1秒后执行该回调函数。函数a的回调函数中打印了"我得先把菜做完才能刷抖音",然后调用了cb()

函数b是另一个异步函数,它在0毫秒后执行,打印了"可以刷抖音啦!"

最后,在调用a函数时,将函数b作为回调函数传递给了a函数。因此,执行顺序如下:

  1. 调用a函数,并将函数b作为回调函数传入。
  2. a函数内部设置了一个1秒的定时器,并打印了"我得先把菜做完才能刷抖音"。
  3. a函数的定时器到期后,执行回调函数cb(),即执行函数b。
  4. b函数设置了一个0毫秒的定时器,并打印了"可以刷抖音啦!"。

因此,最终的输出结果是:

我得先把菜做完才能刷抖音
可以刷抖音啦!

简单来说,函数a中的setTimeout方法仍然是在异步执行代码,并且是在1s后先输出"我得先把菜做完才能刷抖音",然后执行函数cb然后打印"可以刷抖音啦!"。所以最后结果达到我们所期望的亚子。

我们来思考一个问题:问:cb函数没有执行完毕之前,a算不算执行完毕?

答案是: a算执行完毕。因为我们是在函数a里面对cb函数进行回调,当函数a执行完毕后,它的执行上写文就会被移出调用栈,接着执行函数b,当函数b执行完毕,它的执行上下文也会被垃圾回收机制移出调用栈。

回调地狱

"回调地狱"是指在JavaScript中,通过多层嵌套的回调函数来处理异步操作所导致的代码结构复杂、难以维护的情况。这种情况通常发生在多个异步操作需要按照特定顺序执行,并且每个操作的结果都依赖于前一个操作结果时。

例如,下面是一个简单的回调地狱示例:

asyncOperation1(function(result1) {
    asyncOperation2(result1, function(result2) {
        asyncOperation3(result2, function(result3) {
            // 更多嵌套...
        });
    });
});

当我们回调函数嵌套的层数太多时,如果出现了BUG,我们不能确定是其中哪一层出了问题,因为只要有一个嵌套出现错误,它后面所有的回调函数执行结果都会受影响,于是我们需要对剩余部分每层嵌套排查,用这种回调代码来处理异步的操作是15年前的样式,大大增加了代码的可维护性难度,难怪以前的程序员头发少T_T,单排查这一个多层嵌套的回调函数就不得不熬夜加班了,所以随着时代的进步,我们也对之做出了相应的策略。

为了避免回调地狱,可以使用Promise对象或async/await关键字来改善代码结构。使用Promise可以将回调函数改为链式调用,提高代码的可读性可维护性。使用async/await可以更直观地编写异步代码,使其看起来更像同步代码,而不会产生过多的嵌套。

总结

今天我们对于异步执行这个概念和老一代方法---回调函数来解决异步有了基本的理解,但是回调地狱正如其名,恐怖如斯磨人心智,不用担心,在下一篇文章我会为大家详细讲解如何使用ES6新增特性---Promise对象以及它的常用方法来处理异步,并且完美的解决了代码的可维护性差代码复杂等问题。

结语

那么到了这里我们今天的文章就结束啦~

创作不易,如果感觉这个文章对你有帮助的话,点个赞吧♥

作者所有文章的源码,给作者的开源git仓库点个收藏吧: gitee.com/cheng-bingw…