全部 / 前端 / 技术 · 2022年6月5日 0

29 – 回调函数和回调地狱

广告位招租 (vx: ghostcode, 备注:网站广告)

原文地址:https://dev.to/bhagatparwinder/callback-functions-callback-hell-79n

在之前我们讨论事件处理器时已经接触了一些回调函数的概念,这篇文章我们将深入的探究回调函数以及它们是如何解决异步编程,还有它们的缺点以及什么是回调地狱。

回调函数是被当做参数传递给其它函数的函数,回调函数可以在被调用的函数内执行一些任务。

function greeting(name) {
    console.log(`Hello ${name}`);
}

function getUserName(callback) {
    const name = prompt("Enter your name");
    callback(name);
}

getUserName(greeting);

这个例子发生了什么?

  1. getUserName 传入一个参数被调用,参数是 greet 函数;
  2. getUserName 让用户输入用户名且保存到变量 name 中;
  3. getUserName 调用回调函数且传入 name 作为参数;
  4. 我们可以把参数命名为任何变量名,而不一定要是 callback;
  5. 回调函数(geeting)传入参数 name 执行且打印出 "Hello name"。

以上是一个简单的回调函数的例子,具体来说它是同步回调。一切都被逐行执行,一个接一个。

同步和异步

注意:JavaScript 是单线程语言,只有一个线程执行代码。

其他语言可以同时启动多个线程和执行多个进程,但是 JavaScript 不行。当执行耗时操作例如磁盘 I/O 或是网络请求时这可能会是一个明显的缺点。

因为同一时刻只能执行一件事,用户必须等到耗时较长的任务执行完毕后才能进一步执行后续的动作。

JavaScript 的 事件循环、回调栈、回调队列以及 web 接口组成了它的异步。

  1. JavaScript 维护了一个栈来执行任务;
  2. 可能需要更多时间的动作被委托给网络API;
  3. 一旦费时的任务执行完毕,它会被添加到执行队列中;
  4. 只要栈中没有任务可以执行,JavaScript 引擎就会从队列中取出一个然后放到栈中执行。

回调如何推动异步编程的

有许多耗时任务像磁盘 I/O、网络请求和数据处理,这些需要放到异步中去执行。我们可以举一些直观的例子来解释说明:

console.log("Hello");
console.log("Hey");
console.log("Namaste");

当执行上面代码后,控制台打印如下:Hello Hey Namaste,这是正确的执行顺序。现在我们来引入setTimeout 来包裹 "Hey",期望等待 2 秒后打印 "Hey"。

console.log("Hello");

setTimeout(() => {
    console.log("Hey");
}, 2000);

console.log("Namaste");

令我们惊奇的是,它打印如下:"Hello Namaste Hey",期望的是先打印"Hello",等待 2 秒后打印 "Hey",最后打印 "Namaste"。

  1. 回调函数传递给 setTimeout 然后等待 2 秒后执行;
  2. JavaScript 不是阻塞等待 2 秒而根据事件循环原理把它委托给 web api;
  3. web api 等待 2 秒后把它移到回调队列中;
  4. 同时最后一个 console 打印执行;
  5. 一旦栈中没有什么可以执行的,setTimeout 就会被从队列中移动到栈中并执行。

及时 setTimeout 是等待 0 秒,打印的顺序依旧是 "Hello Namaste Hey"。奇怪的是 0 秒应该是立即执行,可事实上并非如此。它依旧会像上面提到的代码一样经历事件循环。执行如下代码:

console.log("Hello");

setTimeout(() => {
    console.log("Hey");
}, 0);

console.log("Namaste");

回调函数的缺点以及回调地狱

随着我们有更好的方法来解决异步操作,回调函数则变得越来越令人讨厌,其实我们没有必要这样对回调函数有敌意。当我们只有 1-2 个异步操作时,回调函数还是很好用的。

当我们需要处理多余 2 个异步任务链时,回调函数则显得捉襟见肘,让我们从例子来了解一下。

我们假设希望每间隔 2 秒打印输出问候语,输出如下:Hello Hey Namaste Hi Bonjour

setTimeout(() => {
    console.log("Hello");
    setTimeout(() => {
        console.log("Hey");
        setTimeout(() => {
            console.log("Namaste");
            setTimeout(() => {
                console.log("Hi");
                setTimeout(() => {
                    console.log("Bonjour");
                }, 2000);
            }, 2000);
        }, 2000);
    }, 2000);
}, 2000);

这种级联嵌套的代码称为回调地狱,很难调试也很难捕获错误,同样降低了代码的可读性。

在最后我们会留一张图,用于在以后的日子里时刻提醒大家关于回调地狱。后面的文章我们将谈论其余的异步方法:promise 、 async/await 和 observables。

前端黑板报