什么是"回调地狱"以及RX解决它的方式和原因?

jhe*_*dus 100 javascript callback reactive-programming language-lawyer rxjs

有人可以给出一个明确的定义以及一个简单的例子来解释什么是不知道JavaScript和node.js的人的"回调地狱"?

何时(在什么样的设置中)出现"回调地狱问题"?

它为什么会发生?

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

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

我参加了Coursera的Reactive课程,Erik Meijer在他的一个讲座中说RX解决了"回调地狱"的问题.我问在Coursera论坛上什么是"回调地狱",但我没有得到明确答案.

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

hug*_*omg 121

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

这个问题有一些Javascript回调地狱的例子:如何避免在Node.js中长时间嵌套异步函数

Javascript中的问题是,"冻结"计算并让"其余部分"执行后者(异步)的唯一方法是将"其余部分"置于回调中.

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

x = getData();
y = getMoreData(x);
z = getMoreData(y);
...
Run Code Online (Sandbox Code Playgroud)

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

getData(function(x){
    getMoreData(x, function(y){
        getMoreData(y, function(z){ 
            ...
        });
    });
});
Run Code Online (Sandbox Code Playgroud)

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

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

当你的代码中有很多回调函数时!在你的代码中使用它们的次数越多越难,当你需要做循环,try-catch块等事情时,它会变得特别糟糕.

例如,据我所知,在JavaScript中,执行一系列异步函数的唯一方法是在前一次返回之后运行一个异步函数,这是使用递归函数.你不能使用for循环.

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

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

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

//ugh!
Run Code Online (Sandbox Code Playgroud)

我们在StackOverflow上询问如何做这类事情的问题数量证明了它是多么令人困惑:)

3)为什么会发生?

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

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

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

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

并发和并行有什么区别?

5)你能否请你展示RX如何在这个简单的例子中解决"回调地狱问题".

我对RX一无所知,但通常通过在编程语言中添加对异步计算的本机支持来解决这个问题.这可以非常多,可以有不同的名称(异步,生成器,协同程序,callcc,...).例如,在Python中,我们可以使用以下内容实现前一个循环示例:

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

myGen = myLoop()
Run Code Online (Sandbox Code Playgroud)

这不是完整的代码,但是想法是"yield"暂停我们的for循环,直到有人调用myGen.next().重要的是我们仍然可以使用for循环编写代码,而不需要像我们在递归loop函数中那样做出"内向外"的逻辑.

  • 回调地狱更多地与使用连续传递风格的代码有多烦人有关。理论上,即使对于常规程序,您仍然可以使用 CPS 风格重写所有函数(维基百科文章有一些示例),但出于充分的理由,大多数人不会这样做。通常我们只在被迫时才使用延续传递风格,Javascript 异步编程就是这种情况。 (2认同)

zsx*_*ing 29

回答这个问题:请问你能否说明RX如何在这个简单的例子中解决"回调地狱问题"?

神奇的是flatMap.我们可以在Rx中为@ hugomg的例子编写以下代码:

def getData() = Observable[X]
getData().flatMap(x -> Observable[Y])
         .flatMap(y -> Observable[Z])
         .map(z -> ...)...
Run Code Online (Sandbox Code Playgroud)

这就像你正在编写一些同步FP代码,但实际上你可以使它们异步Scheduler.


gho*_*nts 22

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

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

想象一下,如果我们必须做http以获得三种资源 - 人,行星和星系.我们的目标是找到人生活的星系.首先,我们必须得到人,然后是行星,然后是星系.这是三个异步操作的三个回调.

getPerson(person => { 
   getPlanet(person, (planet) => {
       getGalaxy(planet, (galaxy) => {
           console.log(galaxy);
       });
   });
});
Run Code Online (Sandbox Code Playgroud)

每个回调都是嵌套的.每个内部回调都依赖于其父级.这导致了"厄运金字塔"式的回调地狱.代码看起来像一个>符号.

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

getPerson()
  .map(person => getPlanet(person))
  .map(planet => getGalaxy(planet))
  .mergeAll()
  .subscribe(galaxy => console.log(galaxy));
Run Code Online (Sandbox Code Playgroud)

使用mergeMapAKA flatMap操作符,您可以使其更简洁:

getPerson()
  .mergeMap(person => getPlanet(person))
  .mergeMap(planet => getGalaxy(planet))
  .subscribe(galaxy => console.log(galaxy));
Run Code Online (Sandbox Code Playgroud)

如您所见,代码是扁平的,包含一个方法调用链.我们没有"厄运金字塔".

因此,避免了回调地狱.

如果你想知道,承诺是避免回调地狱的另一种方式,但是承诺是渴望的,而不是像可观察者那样懒惰而且(一般来说)你不能轻易取消它们.


Jim*_*Kye 14

回调地狱是在异步代码中使用函数回调变得模糊或难以遵循的任何代码.通常,当存在多个间接级别时,使用回调的代码可能变得更难以遵循,更难以重构,并且更难以测试.由于传递了多层函数文字,代码气味是多级缩进.

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

a({
    parameter : someParameter,
    callback : function() {
        b({
             parameter : someOtherParameter,
             callback : function({
                 c(yetAnotherParameter)
        })
    }
});
Run Code Online (Sandbox Code Playgroud)

如果你的代码中有很多行为依赖,那么它会很快麻烦.特别是如果它分支......

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 {
          ...
        }
    }
});
Run Code Online (Sandbox Code Playgroud)

这不行.我们怎样才能使得异步代码以确定的顺序执行而不必传递所有这些回调?

RX是"反应式扩展"的缩写.我没有使用它,但谷歌搜索表明它是一个基于事件的框架,这是有道理的.事件是使代码按顺序执行而不会产生脆弱耦合的常见模式.你可以让C听'bFinished'这个事件,这个事件只有在B被叫做'aFinished'之后才会发生.然后,您可以轻松添加额外的步骤或扩展此类行为,并且只需在测试用例中广播事件,即可轻松测试代码的执行顺序.