誰かがJavaScriptとnode.jsを知らない人にとって「コールバック地獄」とは何かを説明する簡単な例と一緒に明確な定義を与えることができますか?
「コールバック地獄の問題」はいつ(どのような設定で)発生しますか?
なぜ発生するのですか?
「コールバック地獄」は常に非同期計算に関連していますか?
または、「コールバック地獄」はシングルスレッドアプリケーションでも発生しますか?
Courseraでリアクティブコースを受講し、Erik Meijerが講義の1つで、RXが「コールバック地獄」の問題を解決すると述べました。 Courseraフォーラムで「コールバック地獄」とは何かを尋ねましたが、明確な答えは得られませんでした。
簡単な例で「コールバック地獄」を説明した後、そのシンプルな例でRXが「コールバック地獄の問題」をどのように解決するかを示してもらえますか?
この他の質問には、Javascriptコールバックhellの例がいくつかあります。 Node.jsで非同期関数の長いネストを回避する方法
Javascriptの問題は、計算を「フリーズ」し、「残り」を(非同期に)実行する唯一の方法は、「残り」をコールバック内に置くことであるということです。
たとえば、次のようなコードを実行するとします。
x = getData();
y = getMoreData(x);
z = getMoreData(y);
...
GetData関数を非同期にしたい場合はどうなりますか?つまり、値を返すのを待っている間に他のコードを実行する機会がありますか? Javascriptでの唯一の方法は、 継続渡しスタイル を使用して、非同期計算に関係するすべてを書き換えることです。
getData(function(x){
getMoreData(x, function(y){
getMoreData(y, function(z){
...
});
});
});
このバージョンが以前のものよりもいことをだれにも納得させる必要はないと思います。 :-)
コードに多くのコールバック関数がある場合!コード内のコードが多いほど、それらを操作するのは難しくなり、ループ、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でこの種のことを行う方法を尋ねる質問の数は、それがいかに混乱しているのかを証明しています:)
JavaScriptで非同期呼び出しが戻った後に実行されるように計算を遅延させる唯一の方法は、コールバック関数内に遅延コードを配置することであるために発生します。従来の同期スタイルで記述されたコードを遅延させることはできないため、どこでもネストされたコールバックが発生します。
非同期プログラミングは並行性に関係し、シングルスレッドは並列性に関係します。 2つの概念は実際には同じものではありません。
シングルスレッドコンテキストで並行コードを保持できます。実際、コールバック地獄の女王であるJavaScriptはシングルスレッドです。
特にRXについては何も知りませんが、通常、この問題はプログラミング言語で非同期計算のネイティブサポートを追加することで解決されます。実装はさまざまで、非同期、ジェネレータ、コルーチン、およびcallccを含めることができます。
Pythonでは、次の行に沿って何かを使用して、前のループの例を実装できます。
def myLoop():
for i in range(10):
doSomething(i)
yield
myGen = myLoop()
これは完全なコードではありませんが、アイデアは誰かがmyGen.next()を呼び出すまで「yield」がforループを一時停止することです。重要なことは、再帰的なloop
関数で行う必要があるような「裏返し」のロジックを必要とせずに、forループを使用してコードを記述できることです。
質問に答えてください。RXがその単純な例の「コールバック地獄問題」をどのように解決するかを示してください。
魔法はflatMap
です。 @hugomgの例では、Rxで次のコードを記述できます。
def getData() = Observable[X]
getData().flatMap(x -> Observable[Y])
.flatMap(y -> Observable[Z])
.map(z -> ...)...
同期のFPコードを書いているようですが、実際にはScheduler
で非同期にすることができます。
Rxがcallback hellをどのように解決するかという問題に対処するには:
最初に、コールバック地獄についてもう一度説明しましょう。
人、惑星、銀河の3つのリソースを取得するためにhttpを実行する必要がある場合を想像してください。私たちの目的は、人が住んでいる銀河を見つけることです。最初に人、次に惑星、そして銀河を取得する必要があります。これは、3つの非同期操作に対する3つのコールバックです。
getPerson(person => {
getPlanet(person, (planet) => {
getGalaxy(planet, (galaxy) => {
console.log(galaxy);
});
});
});
各コールバックはネストされています。各内部コールバックは、その親に依存しています。これは、「運命のピラミッド」スタイルのcallback hellにつながります。コードは>記号のように見えます。
RxJでこれを解決するには、次のようにします。
getPerson()
.map(person => getPlanet(person))
.map(planet => getGalaxy(planet))
.mergeAll()
.subscribe(galaxy => console.log(galaxy));
mergeMap
AKA flatMap
演算子を使用すると、より簡潔にすることができます。
getPerson()
.mergeMap(person => getPlanet(person))
.mergeMap(planet => getGalaxy(planet))
.subscribe(galaxy => console.log(galaxy));
ご覧のとおり、コードはフラット化され、メソッド呼び出しの単一のチェーンが含まれています。 「破滅のピラミッド」はありません。
したがって、コールバック地獄は回避されます。
あなたが疑問に思っていた場合、promisesはコールバック地獄を避ける別の方法ですが、promiseはlazyではなくlazyであり、observablesや(一般的に言えば)簡単にキャンセルすることはできません。
コールバック地獄とは、非同期コードでの関数コールバックの使用が不明瞭または従うのが困難になるコードです。一般に、複数レベルの間接参照がある場合、コールバックを使用するコードは、追跡が難しくなり、リファクタリングが難しくなり、テストが難しくなります。コードの匂いは、関数リテラルの複数の層を渡すことによる複数レベルのインデントです。
これは、動作に依存関係がある場合、つまり、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 {
...
}
}
});
これはしません。これらのコールバックをすべて渡す必要なく、非同期コードを決められた順序で実行するにはどうすればよいですか?
RXは「リアクティブエクステンション」の略です。私はそれを使用していませんが、グーグルはそれがイベントベースのフレームワークであることを示唆しています、それは理にかなっています。 イベントは、脆弱な結合を作成せずにコードを順番に実行する一般的なパターンです。 Cにイベント「bFinished」をリッスンさせることができます。これは、Bが「aFinished」をリッスンと呼ばれた後にのみ発生します。その後、簡単に追加のステップを追加したり、この種の動作を拡張したりできます。テストケースでイベントをブロードキャストするだけで、コードが順番に実行されることを簡単にテストできます。