JavaScript 中的回调地狱 (Callback Hell)2025年3月18日 | 阅读 11 分钟 JavaScript 是一种异步(非阻塞)的单线程编程语言,这意味着一次只能运行一个进程。 在编程语言中,回调地狱通常指一种使用异步调用编写代码的低效方式。它也称为“金字塔的诅咒”。 JavaScript 中的回调地狱指的是一个过度嵌套回调函数被执行的情况。它降低了代码的可读性和可维护性。当处理异步请求操作时,例如进行多个 API 请求或处理具有复杂依赖关系的事件时,通常会出现回调地狱的情况。 为了更好地理解 JavaScript 中的回调地狱,首先需要了解 JavaScript 中的回调和事件循环。 JavaScript 中的回调JavaScript 将所有内容都视为对象,例如字符串、数组和函数。因此,回调的概念允许我们将函数作为参数传递给另一个函数。回调函数会先完成执行,然后父函数才会执行。 回调函数是异步执行的,它允许代码继续运行,而无需等待异步任务完成。当多个异步任务组合在一起,并且每个任务都依赖于前一个任务时,代码结构就会变得复杂。 让我们来理解回调的用途和重要性。假设我们有一个函数,它接受三个参数:一个字符串和两个数字。我们希望根据具有多个条件的字符串文本得到一些输出。 考虑下面的示例 输出 30 20 上面的代码可以正常工作,但我们需要添加更多任务才能使代码具有可扩展性。条件语句的数量也会不断增加,这将导致代码结构混乱,需要进行优化和提高可读性。 所以,我们可以用更好的方式重写代码,如下所示: 输出 30 20 输出仍然是相同的。但在上面的例子中,我们定义了单独的函数体,并将函数作为回调函数传递给 expectedResult 函数。因此,如果我们想扩展 expected results 的功能,我们可以创建另一个具有不同操作的函数体,并将其用作回调函数,这样更容易理解并提高代码的可读性。 在支持的 JavaScript 功能中,还有其他不同的回调示例。一些常见的例子是事件监听器,以及 map、reduce、filter 等数组函数。 为了更好地理解这一点,我们应该了解 JavaScript 的值传递和引用传递。 JavaScript 支持两种数据类型:原始类型和非原始类型。原始数据类型包括 undefined、null、string 和 boolean,它们不能被更改,或者说相对于而言是不可变的;非原始数据类型包括数组、函数和对象,它们可以被更改,即是可变的。 引用传递传递实体的引用地址,例如函数可以作为参数。因此,如果函数内部的值被更改,它将改变函数外部的原始值。 相比之下,值传递的概念不会改变函数体外部的原始值。相反,它会通过使用它们的内存将值复制到两个不同的位置。JavaScript 通过引用来识别所有对象。 在 JavaScript 中,addEventListener 监听 click、mouseover 和 mouseout 等事件,并将第二个参数作为函数,该函数将在事件触发后执行。此函数使用引用传递概念,并使用不带括号的方式传递。 考虑以下示例;在此示例中,我们将 greet 函数作为参数传递给 addEventListener 作为回调函数。当触发 click 事件时,将调用它。 Test.html 输出 ![]() 在上面的示例中,我们将 greet 函数作为参数传递给 addEventListener 作为回调函数。当触发 click 事件时,将调用它。 类似地,filter 也是回调函数的一个例子。如果使用 filter 来迭代数组,它将接受另一个回调函数作为参数来处理数组数据。考虑以下示例;在此示例中,我们使用 greater 函数来打印数组中大于 5 的数字。我们在 filter 方法中使用 isGreater 函数作为回调函数。 输出 [ 10, 6, 7 ] 上面的示例显示 greater 函数在 filter 方法中用作回调函数。 为了更好地理解 JavaScript 中的回调和事件循环,让我们讨论同步和异步 JavaScript。 同步 JavaScript让我们了解同步编程语言的特点。同步编程具有以下特点: 阻塞执行:同步编程语言支持阻塞执行技术,这意味着它会阻塞后续语句的执行,而现有语句将被执行。因此,它实现了语句的可预测和确定性执行。 顺序流程:同步编程支持顺序执行流程,这意味着每个语句都按顺序执行,一个接一个。语言程序会等待一个语句完成后再进行下一个。 简单性:通常,同步编程被认为易于理解,因为我们可以预测其执行流程的顺序。通常,它是线性的,易于预测。小应用程序在此类语言上开发效果很好,因为它们可以处理关键的操作顺序。 直接错误处理:在同步编程语言中,错误处理非常容易。如果某个语句在执行过程中发生错误,它会抛出一个错误,程序可以捕获它。 总而言之,同步编程有两个核心特征,即一次只执行一个任务,并且只有在当前任务完成后才会处理下一组后续任务。从而,它遵循顺序代码执行。 这种编程行为,当一个语句正在执行时,语言会产生一个阻塞代码的情况,因为每个任务都必须等待前一个任务完成。 但是,当人们谈论 JavaScript 时,它是否是同步的还是异步的,一直是一个令人费解的答案。 在上面讨论的示例中,当我们将一个函数用作 filter 函数中的回调时,它是同步执行的。因此,它被称为同步执行。filter 函数必须等待 greater 函数完成其执行。 因此,回调函数也称为阻塞回调,因为它会阻塞调用它的父函数的执行。 主要是,JavaScript 被认为是单线程同步且阻塞的。但是,使用一些方法,我们可以使其在不同场景下异步工作。 现在,让我们了解异步 JavaScript。 异步 JavaScript异步编程语言专注于提高应用程序的性能。在这些场景中可以使用回调。我们可以通过以下示例分析 JavaScript 的异步行为: 从上面的示例中,setTimeout 函数以回调和时间(以毫秒为单位)作为参数。回调将在指定时间(此处为 1 秒)后被调用。总而言之,该函数将等待 1 秒执行。现在,看看下面的代码: 输出 first Second greet after 1 second 从上面的代码来看,setTimeout 之后的 log 消息将首先被执行,而计时器则会经过。因此,它会花费一秒钟,然后在 1 秒的时间间隔后出现问候消息。 在 JavaScript 中,setTimeout 是一个异步函数。每当我们调用 setTimeout 函数时,它会注册一个回调函数(此处为 greet),以便在指定的延迟后执行。但是,它不会阻塞后续代码的执行。 在上面的示例中,log 消息是同步语句,会立即执行。它们不依赖于 setTimeout 函数。因此,它们会执行并将各自的消息记录到控制台,而无需等待 setTimeout 中指定的延迟。 同时,JavaScript 中的事件循环会处理异步任务。在这种情况下,它会等待指定的延迟(1 秒)过去,并且在该时间过去后,它会获取回调函数(greet)并执行它。 因此,setTimeout 函数之后的其他代码在后台运行时也在执行。这种行为允许 JavaScript 在等待异步操作完成的同时执行其他任务。 我们需要了解调用堆栈和回调队列才能处理 JavaScript 中的异步事件。 考虑下图 ![]() 从上图可以看出,典型的 JavaScript 引擎由堆内存和调用堆栈组成。调用堆栈在推送到堆栈时会执行所有代码,而不会等待。 堆内存负责在运行时根据需要为对象和函数分配内存。 现在,我们的浏览器引擎包含多个 Web API,如 DOM、setTimeout、console、fetch 等,引擎可以通过全局 window 对象访问这些 API。在下一步,一些事件循环充当守门员,它从回调队列中获取函数请求并将它们推送到堆栈。这些函数,例如 setTimeout,需要一定的等待时间。 现在,回到我们的示例 setTimeout 函数;当遇到函数时,计时器会在回调队列中注册。之后,其余代码被推送到调用堆栈并执行。一旦函数达到其计时器限制,它就会过期,回调队列会推送具有指定逻辑并注册在超时函数中的回调函数。因此,它将在指定时间后执行。 回调地狱场景现在,我们已经讨论了回调、同步、异步以及与回调地狱相关的其他主题。让我们了解 JavaScript 中的回调地狱是什么。 当多个回调嵌套在一起时,这种情况被称为回调地狱,因为它的代码形状看起来像一个金字塔,也被称为“金字塔的诅咒”。 回调地狱使代码更难理解和维护。我们通常在 Node.js 中工作时会看到这种情况。例如,考虑以下示例: 在上面的示例中,getUserData 接受一个用户名,该用户名依赖于 article list 或需要从 article 中的 getArticles 响应中提取。getAddress 也有类似的依赖关系,它依赖于 getUserData 的响应。这种情况称为回调地狱。 回调地狱的内部工作可以通过以下示例来理解: 让我们理解我们需要执行任务 A。为了执行任务 A,我们需要从任务 B 获取一些数据。类似地;我们有不同的任务,它们相互依赖并异步执行。因此,它会创建一系列回调函数。 让我们了解 JavaScript 中的 Promise,以及它们如何创建异步操作,从而允许我们避免编写嵌套回调。 JavaScript Promise在 JavaScript 中,Promise 在 ES6 中引入。它是一个带有语法封装的对象。由于其异步行为,它是避免为异步操作编写回调的另一种方式。如今,fetch() 等 Web API 是使用 Promise 实现的,它提供了访问服务器数据的有效方法。它还提高了代码的可读性,并且是避免编写嵌套回调的一种方式。 现实生活中的 Promise 表示两个人或更多人之间的信任,并保证某事一定会发生。在 JavaScript 中,Promise 是一个对象,它确保在未来(需要时)产生一个单一值。JavaScript 中的 Promise 用于管理和处理异步操作。 Promise 返回一个对象,该对象确保并表示异步操作的完成或失败及其输出。它是值的代理,而无需知道确切的输出。它对于 异步 操作很有用,可以提供最终的成功值或失败原因。因此,异步方法像同步方法一样返回。值。 一般来说,Promise 有以下三种状态:
Promise 的语法 以下是编写 Promise 的示例: 这是一个编写 Promise 的示例。 在上面的示例中,我们可以看到如何有效地使用 Promise 从服务器发起请求。我们可以观察到,与回调相比,上面的代码可读性得到了提高。Promise 提供了 .then() 和 .catch() 等方法,允许我们在成功或失败的情况下处理操作状态。我们可以为 Promise 的不同状态指定情况。 JavaScript 中的 Async/Await这是另一种避免使用嵌套回调的方法。Async/Await 允许我们更有效地使用 Promise。我们可以避免使用 .then() 或 .catch() 方法链。这些方法也依赖于回调函数。 Async/Await 可以与 Promise 精确地一起使用以提高应用程序的性能。它内部解析 Promise 并提供结果。而且,它比 () 或 catch() 方法更具可读性。 我们不能将 Async/Await 与普通的回调函数一起使用。要使用它,我们必须通过在 function 关键字之前编写 async 关键字来使函数异步。但是,它内部也使用链式调用。 以下是 Async/Await 的示例: 要使用 Async/Await,必须使用 async 关键字指定函数,并在函数内部编写 await 关键字。async 将停止其执行,直到 Promise 被解析或拒绝。一旦 Promise 被处理,它就会恢复。解析后,await 表达式的值将被存储在持有它的变量中。 总结总而言之,我们可以通过使用 Promise 和 async/await 来避免嵌套回调。除此之外,我们还可以遵循其他方法,例如写注释,将代码拆分成单独的组件也可能很有帮助。但是,如今,开发人员更倾向于使用 async/await。 结论JavaScript 中的回调地狱指的是一个过度嵌套回调函数被执行的情况。它降低了代码的可读性和可维护性。当处理异步请求操作时,例如进行多个 API 请求或处理具有复杂依赖关系的事件时,通常会出现回调地狱的情况。 为了更好地理解 JavaScript 中的回调地狱。 JavaScript 将所有内容都视为对象,例如字符串、数组和函数。因此,回调的概念允许我们将函数作为参数传递给另一个函数。回调函数会先完成执行,然后父函数才会执行。 回调函数是异步执行的,它允许代码继续运行,而无需等待异步任务完成。 |
我们请求您订阅我们的新闻通讯以获取最新更新。