129

有人可以给出一个清晰的定义和一个简单的例子来解释什么是不了解 JavaScript 和 node.js 的人的“回调地狱”吗?

“回调地狱问题”何时(在什么样的设置中)发生?

为什么会发生?

“回调地狱”总是与异步计算有关吗?

或者“回调地狱”也可以在单线程应用程序中发生?

我参加了 Coursera 的反应式课程,Erik Meijer 在他的一次讲座中说 RX 解决了“回调地狱”的问题。我在 Coursera 论坛上问过什么是“回调地狱”,但没有得到明确的答案。

在用一个简单的例子解释了“回调地狱”之后,你能否在这个简单的例子中展示 RX 是如何解决“回调地狱问题”的?

4

8 回答 8

149

1) 对于不了解 javascript 和 node.js 的人来说,什么是“回调地狱”?

This other question有一些Javascript回调地狱的例子:How to Avoid long nesting of asynchronous functions in Node.js

Javascript 中的问题是,“冻结”计算并让“其余部分”稍后(异步)执行的唯一方法是将“其余部分”放入回调中。

例如,假设我想运行如下所示的代码:

x = getData();
y = getMoreData(x);
z = getMoreData(y);
...

如果现在我想让 getData 函数异步,这意味着我有机会在等待它们返回它们的值时运行一些其他代码,会发生什么?在 Javascript 中,唯一的方法是使用延续传递样式重写所有涉及异步计算的内容:

getData(function(x){
    getMoreData(x, function(y){
        getMoreData(y, function(z){ 
            ...
        });
    });
});

我认为我不需要说服任何人这个版本比以前的版本更丑。:-)

2)“回调地狱问题”何时(在什么样的设置中)发生?

当您的代码中有很多回调函数时!代码中的它们越多,使用它们就变得越困难,当你需要执行循环、try-catch 块和类似的事情时,它变得特别糟糕。

例如,据我所知,在 JavaScript 中,执行一系列异步函数的唯一方法是使用递归函数。您不能使用 for 循环。

// we would like to write the following
for(var i=0; i<10; i++){
    doSomething(i);
}
blah();

相反,我们可能需要最终编写:

function loop(i, onDone){
    if(i >= 10){
        onDone()
    }else{
        doSomething(i, function(){
            loop(i+1, onDone);
        });
     }
}
loop(0, function(){
    blah();
});

//ugh!

我们在 StackOverflow 上遇到的许多问题都在问如何做这种事情,这证明了它是多么令人困惑:)

3)为什么会发生?

这是因为在 JavaScript 中,延迟计算以使其在异步调用返回后运行的唯一方法是将延迟的代码放在回调函数中。您不能延迟以传统同步样式编写的代码,因此您最终会在任何地方出现嵌套回调。

4)或者“回调地狱”也可以在单线程应用程序中发生?

异步编程与并发有关,而单线程与并行有关。这两个概念其实不是一回事。

您仍然可以在单线程上下文中拥有并发代码。事实上,回调地狱的女王 JavaScript 是单线程的。

并发和并行有什么区别?

5)您能否在这个简单的例子中展示RX如何解决“回调地狱问题”。

我对 RX 一无所知,但通常这个问题可以通过在编程语言中添加对异步计算的本机支持来解决。实现可能会有所不同,包括:异步、生成器、协程和 callcc。

在 Python 中,我们可以使用以下代码来实现前面的循环示例:

def myLoop():
    for i in range(10):
        doSomething(i)
        yield

myGen = myLoop()

这不是完整的代码,但想法是“yield”会暂停我们的 for 循环,直到有人调用 myGen.next()。重要的是,我们仍然可以使用 for 循环编写代码,而无需像在那个递归loop函数中那样将逻辑“从里到外”转换出来。

于 2014-08-02T18:38:36.213 回答
31

只需回答这个问题:您能否在这个简单的示例中展示 RX 如何解决“回调地狱问题”?

神奇的是flatMap。对于@hugomg 的示例,我们可以在 Rx 中编写以下代码:

def getData() = Observable[X]
getData().flatMap(x -> Observable[Y])
         .flatMap(y -> Observable[Z])
         .map(z -> ...)...

这就像你正在编写一些同步的 FP 代码,但实际上你可以通过Scheduler.

于 2014-08-04T01:20:14.510 回答
30

为了解决 Rx 如何解决回调地狱的问题:

首先让我们再次描述回调地狱。

想象一个例子,我们必须通过 http 来获取三种资源——人、行星和星系。我们的目标是找到这个人居住的星系。首先我们必须找到这个人,然后是星球,然后是星系。这是三个异步操作的三个回调。

getPerson(person => { 
   getPlanet(person, (planet) => {
       getGalaxy(planet, (galaxy) => {
           console.log(galaxy);
       });
   });
});

每个回调都是嵌套的。每个内部回调都依赖于其父级。这导致了回调地狱的“末日金字塔”风格。代码看起来像 > 符号。

要在 RxJs 中解决这个问题,你可以这样做:

getPerson()
  .map(person => getPlanet(person))
  .map(planet => getGalaxy(planet))
  .mergeAll()
  .subscribe(galaxy => console.log(galaxy));

使用mergeMapAKAflatMap运算符,您可以使其更简洁:

getPerson()
  .mergeMap(person => getPlanet(person))
  .mergeMap(planet => getGalaxy(planet))
  .subscribe(galaxy => console.log(galaxy));

如您所见,代码是扁平化的,包含一个方法调用链。我们没有“末日金字塔”。

因此,避免了回调地狱。

如果你想知道,promise是另一种避免回调地狱的方法,但是 promises 是急切的,不像 observables 那样懒惰,而且(一般来说)你不能轻易取消它们。

于 2017-06-14T12:25:43.967 回答
15

回调地狱是在异步代码中使用函数回调变得晦涩或难以理解的任何代码。通常,当有多个间接级别时,使用回调的代码会变得更难遵循、更难重构和更难测试。由于传递了多层函数文字,代码异味是多层缩进。

当行为具有依赖关系时,通常会发生这种情况,即当 A 必须发生在 B 必须发生在 C 之前时。然后你会得到这样的代码:

a({
    parameter : someParameter,
    callback : function() {
        b({
             parameter : someOtherParameter,
             callback : function({
                 c(yetAnotherParameter)
        })
    }
});

如果你的代码中有很多这样的行为依赖项,它很快就会变得很麻烦。特别是如果它分支...

a({
    parameter : someParameter,
    callback : function(status) {
        if (status == states.SUCCESS) {
          b(function(status) {
              if (status == states.SUCCESS) {
                 c(function(status){
                     if (status == states.SUCCESS) {
                         // Not an exaggeration. I have seen
                         // code that looks like this regularly.
                     }
                 });
              }
          });
        } elseif (status == states.PENDING {
          ...
        }
    }
});

这不行。我们如何使异步代码以确定的顺序执行而不必传递所有这些回调?

RX 是“反应式扩展”的缩写。我没有使用它,但谷歌搜索表明它是一个基于事件的框架,这是有道理的。事件是使代码按顺序执行而不产生脆弱耦合的常见模式。您可以让 C 监听事件“bFinished”,该事件仅在 B 被称为监听“aFinished”之后才会发生。然后,您可以轻松地添加额外的步骤或扩展这种行为,并且可以通过仅在测试用例中广播事件来轻松地测试您的代码是否按顺序执行。

于 2014-08-02T18:38:52.273 回答
2

回调地狱意味着您在另一个回调内部的回调中,并且它会进行第 n 次调用,直到您的需求未被满足。

让我们通过一个使用 set timeout API 的虚假 ajax 调用示例来理解,假设我们有一个 recipe API,我们需要下载所有 recipe。

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
            }, 1500);
        }
        getRecipe();
    </script>
</body>

在上面的示例中,当计时器到期 1.5 秒后,内部回调代码将执行,换句话说,通过我们的虚假 ajax 调用,所有配方将从服务器下载。现在我们需要下载特定的配方数据。

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

为了下载特定的配方数据,我们在第一个回调中编写了代码并传递了配方 ID。

现在假设我们需要下载 ID 为 7638 的食谱的同一发布者的所有食谱。

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                    setTimeout(publisher=>{
                        const recipe2 = {title:'Fresh Apple Pie', publisher:'Suru'};
                        console.log(recipe2);
                    }, 1500, recipe.publisher);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

为了满足我们的需求,即下载出版商名称 suru 的所有配方,我们在第二个回调中编写了代码。很明显,我们编写了一个回调链,称为回调地狱。

如果你想避免回调地狱,你可以使用 Promise,它是 js es6 的特性,每个 Promise 都有一个回调,当 Promise 满时调用。承诺回调有两个选项,要么被解决,要么被拒绝。假设您的 API 调用成功,您可以调用 resolve 并通过 resolve 传递数据您可以使用then()获取此数据。但是如果您的 API 失败,您可以使用拒绝,使用catch来捕获错误。记住一个 promise 总是用then表示 resolve 和catch表示拒绝

让我们使用 Promise 解决之前的回调地狱问题。

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        getIds.then(IDs=>{
            console.log(IDs);
        }).catch(error=>{
            console.log(error);
        });
    </script>
</body>

现在下载特定的食谱:

<body>
    <script>
        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }
        getIds.then(IDs=>{
            console.log(IDs);
            return getRecipe(IDs[2]);
        }).
        then(recipe =>{
            console.log(recipe);
        })
        .catch(error=>{
            console.log(error);
        });
    </script>
</body>

现在我们可以编写另一个调用allRecipeOfAPublisher的方法,例如 getRecipe,它也将返回一个 Promise,我们可以编写另一个 then() 来接收 allRecipeOfAPublisher 的 resolve Promise,我希望此时您可以自己完成。

所以我们学习了如何构造和使用 Promise,现在让我们使用 es8 中引入的 async/await 来更轻松地使用 Promise。

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }

        async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

        getRecipesAw();
    </script>
</body>

在上面的例子中,我们使用了一个 async 函数,因为它会在后台运行,在 async 函数中,我们在每个返回或者是一个 promise 的方法之前使用了await关键字,因为在那个位置上等待直到这个 promise 完成,换句话说,在波纹管代码直到 getIds 完成解决或拒绝程序将在 ID 返回时停止执行该行下面的代码,然后我们再次使用 id 调用 getRecipe() 函数并使用 await 关键字等待直到数据返回。所以这就是我们最终从回调地狱中恢复过来的方式。

  async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

要使用await,我们需要一个异步函数,我们可以返回一个promise,所以使用then来解决promise和cath来拒绝promise

从上面的例子:

 async function getRecipesAw(){
            const IDs = await getIds;
            const recipe = await getRecipe(IDs[2]);
            return recipe;
        }

        getRecipesAw().then(result=>{
            console.log(result);
        }).catch(error=>{
            console.log(error);
        });
于 2020-06-17T15:04:25.047 回答
0

可以避免回调地狱的一种方法是使用 FRP,它是 RX 的“增强版”。

我最近开始使用 FRP,因为我找到了一个很好的实现,称为Sodium( http://sodium.nz/ )。

典型的代码如下所示(Scala.js):

def render: Unit => VdomElement = { _ =>
  <.div(
    <.hr,
    <.h2("Note Selector"),
    <.hr,
    <.br,
    noteSelectorTable.comp(),
    NoteCreatorWidget().createNewNoteButton.comp(),
    NoteEditorWidget(selectedNote.updates()).comp(),
    <.hr,
    <.br
  )
}

selectedNote.updates()is a如果(which is a ) 发生变化则Stream触发,然后相应地更新。selectedNodeCellNodeEditorWidget

因此,根据 的内容selectedNode Cell,当前编辑的内容Note会发生变化。

这段代码完全避免了 Callback-s,几乎,Cacllback-s 被推送到应用程序的“外层”/“表面”,其中状态处理逻辑与外部世界接口。在内部状态处理逻辑(实现状态机)中传播数据不需要回调。

完整的源代码在这里

上面的代码片段对应于以下简单的创建/显示/更新示例:

在此处输入图像描述

此代码还将更新发送到服务器,因此对更新实体的更改会自动保存到服务器。

Stream通过使用s 和s来处理所有事件处理Cell。这些是 FRP 概念。只有在 FRP 逻辑与外部世界交互时才需要回调,例如用户输入、编辑文本、按下按钮、AJAX 调用返回。

数据流是使用 FRP(由 Sodium 库实现)以声明方式显式描述的,因此不需要事件处理/回调逻辑来描述数据流。

FRP(它是 RX 的更“严格”版本)是一种描述数据流图的方式,它可以包含包含状态的节点。Cell事件在包含节点(称为s)的状态中触发状态更改。

Sodium 是一个高阶 FRP 库,这意味着使用flatMap/switch原语可以在运行时重新排列数据流图。

我建议看一下Sodium book,它详细解释了 FRP 如何摆脱所有回调,这些回调对于描述与更新应用程序状态以响应某些外部刺激有关的数据流逻辑不是必需的。

使用 FRP,只需要保留那些描述与外部世界交互的回调。换句话说,当使用 FRP 框架(例如 Sodium)或使用“类似 FRP”的框架(例如 RX)时,数据流以功能/声明方式描述。

Sodium 也可用于 Javascript/Typescript。

于 2019-12-27T01:43:58.500 回答
-4

使用 jazz.js https://github.com/Javanile/Jazz.js

它简化如下:

    // 运行顺序任务链
    jj.script([
        // 第一个任务
        功能(下一个){
            // 在这个过程结束时'next'指向第二个任务并运行它
            调用AsyncProcess1(下一个);
        },
      // 第二个任务
      功能(下一个){
        // 在这个过程结束时'next'指向第三个任务并运行它
        调用AsyncProcess2(下一个);
      },
      // 第三个任务
      功能(下一个){
        // 在此过程结束时“下一个”指向(如果有)
        调用AsyncProcess3(下一个);
      },
    ]);

于 2016-10-03T09:08:13.323 回答
-6

如果你没有回调和地狱回调的知识,那没有问题。唯一的事情是回调和回调地狱。例如:地狱回调就像我们可以在一个类中存储一个类。正如你所听到的关于嵌套在C、C++语言中的那个。嵌套的意思是一个类在另一个类里面。

于 2019-02-20T12:06:47.150 回答