什么是“回拨地狱”以及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(下); }, ]);