如何处理嵌套的回调并避免 "回调地狱"?

180 阅读6分钟

JavaScript是一种奇怪的语言。偶尔,你不得不处理一个回调,而这个回调又在另一个回调中,又在另一个回调中。

人们亲切地称这种模式为回调地狱

它看起来有点像这样。

firstFunction(args, function() {

这就是JavaScript的魅力所在。看到嵌套的回调是令人匪夷所思的,但我不认为这是一个 "地狱"。如果你知道该如何处理,"地狱 "是可以管理的。

关于回调

我假设你知道什么是回调,如果你正在阅读这篇文章。如果你不知道,请在继续阅读之前先阅读这篇文章,了解回调的介绍。在那里,我们谈论了什么是回调,以及为什么在JavaScript中使用它们。

回调地狱的解决方案

回调地狱有四个解决方案:

  1. 写注释
  2. 将函数分割成更小的函数
  3. 使用承诺
  4. 使用Async/await

在我们深入研究解决方案之前,让我们一起构建一个回调地狱。为什么?因为看到firstFunction、secondFunction和thirdFunction太抽象了。我们想把它具体化。

构建一个回调地狱

让我们想象一下,我们正试图做一个汉堡包。为了做一个汉堡,我们需要经历以下步骤:

  1. 获取原料(我们假设这是一个牛肉汉堡包)
  2. 烹饪牛肉
  3. 获取汉堡包
  4. 把煮好的牛肉放在包子中间
  5. 端出汉堡

如果这些步骤是同步的,你将会看到一个类似于这样的函数:

const makeBurger = () => {

const burger = makeBurger();

然而,在我们的场景中,假设我们不能自己制作汉堡。我们必须指示一个帮手来制作汉堡的步骤。在我们指示完助手后,我们必须等待助手完成,然后再开始下一个步骤。

如果我们想在JavaScript中等待什么,我们需要使用回调。为了制作汉堡,我们必须先得到牛肉。我们只有在得到牛肉之后才能烹饪牛肉。

const makeBurger = () => {

要烹饪牛肉,我们需要把牛肉传给cookBeef函数。否则,就没有东西可做了然后,我们必须等待牛肉被煮熟。

一旦牛肉被煮熟了,我们就可以得到包子:

const makeBurger = () => {

拿到包子后,我们需要把肉饼放在包子中间。这就是一个汉堡的形成:

const makeBurger = () => {

最后,我们可以端出汉堡了!但我们不能从makeBurger返回汉堡,因为它是异步的。我们需要接受一个回调以提供汉堡:

const makeBurger = nextStep => {

// Make and serve the burger

(我在制作这个回调地狱的例子时很开心)。

回调地狱的第一个解决方案:写评论

makeBurger的回调地狱很容易理解。我们可以阅读它。它只是看起来不好看。

如果你是第一次读makeBurger,你可能会想 "为什么我们需要这么多回调来制作一个汉堡?这没有意义!"。

在这种情况下,你会想留下注释来解释你的代码:

// Makes a burger
const makeBurger = nextStep => {

现在,当你看到回调地狱时,你不会再想 "wtf?!",而是了解了为什么必须这样写。

回调地狱的第二个解决方案。将回调分成不同的函数

我们的回调地狱的例子已经是一个例子了。让我给你看看一步步的命令式代码,你就会明白为什么。

对于getBeef,我们的第一个回调,我们必须到冰箱里去拿牛肉。厨房里有两个冰箱。我们需要去右边的冰箱:

const getBeef = nextStep => {

要煮牛肉,我们需要把牛肉放进烤箱;把烤箱调到200度,然后等待20分钟:

const cookBeef = (beef, nextStep) => {

现在想象一下,如果你要在makeBurger中写出这些步骤......你可能会因为代码量太大而晕倒!

关于将回调分割成小函数的具体例子,你可以阅读我的回调文章中的这一小部分

回调地狱的第三个解决方案。使用承诺

我将假设你知道什么是承诺。如果你不知道,请阅读这篇文章

承诺可以使回调地狱更容易管理。你将拥有这样的代码,而不是你在上面看到的嵌套代码:

const makeBurger = () => {

// Make and serve burger

如果你利用诺言的单参数风格,你可以把上面的代码调整成这样。

const makeBurger = () => {

// Make and serve burger

更容易阅读和管理。

但问题是,你如何将基于回调的代码转换成基于承诺的代码。

将回调转换为承诺

为了将回调转换为承诺,我们需要为每个回调创建一个新的承诺。当回调成功时,我们可以解决这个承诺。或者,如果回调失败,我们可以拒绝该承诺。

const getBeefPromise = _ => {

  return new Promise((resolve, reject) => {

const cookBeefPromise = beef => {

  return new Promise((resolve, reject) => {

在实践中,回调可能已经为你写好了。如果你使用Node,每个包含回调的函数将有相同的语法:

  1. 回调将是最后一个参数
  2. 回调将总是有两个参数。而且这些参数的顺序是一样的。(首先是错误,其次是你感兴趣的东西)。
// The function that’s defined for you

// How you use the function

如果你的回调具有相同的语法,你可以使用像ES6 PromisifyDenodeify(去节点化)这样的库,将回调变成一个承诺。如果你使用Node v8.0及以上版本,你可以使用util.promisify

这三种方式都可以。你可以选择任何库来工作。不过,每种方法之间都有细微的差别。我将让你去查看他们的文档,了解如何操作。

回调地狱的第四个解决方案:使用异步函数

要使用异步函数,你首先需要知道两件事:

  1. 如何将回调转换为承诺(阅读上文)
  2. 如何使用异步函数如果你需要帮助,请阅读这个)。

有了异步函数,你就可以把makeBurger写得好像它又是同步的了

const makeBurger = async () => {

// Make and serve burger

这里我们可以对makeBurger做一个改进。你可能可以同时得到两个getBuns和getBeef的帮助器。这意味着你可以用Promise.all来等待他们两个。

const makeBurger = async () => {

// Make and serve burger

(注意:你也可以用Promise做同样的事情......但是语法没有像ync/await函数那样好,那样清晰)。

总结

回调地狱并不像你想象的那么可怕。有四种简单的方法来管理回调地狱:

  1. 写注释
  2. 将函数分割成更小的函数
  3. 使用承诺
  4. 使用Async/await

这篇文章最初发布于 我的博客. 如果你想获得更多的文章来帮助你成为一个更好的前端开发者,请 注册 我的 通讯