什么是“回拨地狱”以及RX如何解决这个问题?

有人可以给一个清晰的定义,一个简单的例子,解释什么是一个“callback地狱”的人不知道JavaScript和node.js?

什么时候(在什么样的设置)发生“callback地狱问题”?

为什么会发生?

“callback地狱”总是与asynchronous计算有关吗?

或者也可以在单线程应用程序中“callback地狱”?

我在Coursera参加了Reactive Course,Erik Meijer在他的一个演讲中表示,RX解决了“回拨地狱”的问题。 我问在Coursera论坛上什么是“回拨地狱”,但我没有明确的答案。

通过一个简单的例子解释“callback地狱”之后,你能否展示RX如何解决这个简单例子中的“callback地狱问题”?

1)什么是不知道javascript和node.js的人的“回拨地狱”?

这个其他的问题有一些Javascriptcallback地狱的例子: 如何避免在Node.js中长期嵌套asynchronous函数

在Javascript中的问题是,“冻结”一个计算和“rest”执行后者(asynchronous)的唯一方法是把“其余的”在一个callback。

例如,说我想运行看起来像这样的代码:

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

如果现在我想让getData函数是asynchronous的,这意味着我有机会运行一些其他代码,而我正在等待它们返回它们的值呢? 在JavaScript中,唯一的方法是使用延续传递样式来重新编码接触asynchronous计算的所有内容:

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

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

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

当你的代码中有很多callback函数的时候! 在代码中使用它们越来越困难,当你需要做循环,try-catch块和类似的东西时,它会变得特别糟糕。

例如,就我所知,在JavaScript中执行一系列asynchronous函数的唯一方法就是在先前的返回之后运行一个asynchronous函数,该函数使用recursion函数。 你不能使用for循环。

 //we would like to write 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! 

JavaScript标签问如何做这个问题的数量应该是多么混乱的certificate:)

3)为什么会发生?

这是因为在JavaScript中,延迟计算以便在asynchronous调用返回后运行的唯一方法是将延迟的代码放入callback函数中。 你不能延迟用传统的同步风格编写的代码,所以你最终得到嵌套的callback。

4)还是“callback地狱”也出现在一个单线程的应用程序?

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

您仍然可以在单个线程环境中使用并发代码。 实际上,callback地狱的女王JavaScript是单线程的。

并发VS并行 – 有什么区别?

5)你能否也请示出RX如何解决这个简单例子中的“callback地狱问题”。

我对RX一无所知,但通常通过在编程语言中添加对asynchronous计算的本地支持来解决此问题。 这可以非常多,可以有不同的名称(asynchronous,生成器,协程,callcc,…)。 对于一个糟糕的具体的例子,在Python中,我们可以使用生成器来编码循环的例子,其中包括:

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

这不是完整的代码,但想法是“yield”暂停我们的for循环,直到有人调用myGen.next()。 重要的是,我们仍然可以使用for循环来编写代码,而不需要像在recursionloop函数中所做的那样将“内外”逻辑closures。

只要回答这个问题:你能否也请示出RX如何解决这个简单例子中的“callback地狱问题”?

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

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

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

callback地狱是在asynchronous代码中使用函数callback变得模糊或难以遵循的任何代码。 一般来说,当间接程度不止一个级别时,使用callback的代码可能变得难以遵循,难以重构,而且更难以testing。 由于传递了多层函数文字,代码异味是多级缩进。

这种情况经常发生在行为具有依赖关系的时候,也就是说,在B必须发生在C之前,A必须发生。那么你得到如下的代码:

 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 { ... } } }); 

这不会做。 我们怎样才能使asynchronous代码以确定的顺序执行,而不必传递所有这些callback呢?

RX是“被动扩展”的缩写。 我没有使用它,但谷歌认为这是一个基于事件的框架,这是有道理的。 事件是使代码顺序执行而不产生脆性耦合的常见模式 。 你可以让C监听事件'bFinished',这个事件只发生在B被称为'aFinished'之后。 然后,您可以轻松地添加额外步骤或扩展此类行为,并且可以通过在testing用例中仅广播事件来轻松地testing代码的执行顺序。

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

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

设想一个案例,我们必须做http来获得三个资源 – 人,星球和星系。 我们的目标是find人类居住的星系。首先,我们必须得到人,然后是星球,然后是星系。 这是三个asynchronous操作的callback。

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

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

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

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

使用mergeMap AKA flatMap操作符可以使其更加简洁:

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

正如你所看到的,代码是扁平化的,并且包含一个单一的方法调用链。 我们没有“末日金字塔”。

因此,回避地狱是可以避免的。

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

它简化如下:


     //连续运行连续任务
     jj.script([
         //第一个任务
        函数(next){
             //在这个过程结束时'next'指向第二个任务并运行它 
             callAsyncProcess1(下);
         },
       //第二项任务
      函数(next){
         //在这个过程结束时'下一个'指向任务并运行它 
         callAsyncProcess2(下);
       },
       //任务
      函数(next){
         //在这个过程结束时'next'指向(如果有) 
         callAsyncProcess3(下);
       },
     ]);