ウェブサイト検索

javascript - 「コールバック地獄」とは何ですか、そしてRXはそれをどのように、なぜ解決するのですか?


JavaScriptやNode.jsを知らない人のために、「コールバック地獄」とは何かを説明する明確な定義と簡単な例を誰かが教えてくれますか?

「コールバック地獄の問題」はどのような状況で発生しますか?

なぜそれが起こるのですか?

「コールバック地獄」は常に非同期計算に関連していますか?

それとも「コールバック地獄」は、シングルスレッドのアプリケーションでも発生することがありますか?

私はCourseraでリアクティブコースを受講し、エリック・メイヤーは彼の講義の一つでRXが「コールバック地獄」の問題を解決すると言いました。私はCourseraのフォーラムで「コールバック地獄」とは何かを尋ねましたが、明確な答えは得られませんでした。

シンプルな例で「コールバック地獄」について説明した後、そのシンプルな例でRXがどのように「コールバック地獄の問題」を解決するかも示してもらえますか?

回答 #1

1)JavaScriptやNode.jsを知らない人にとって、『コールバック地獄』とは何ですか?

この別の質問には、JavaScriptのコールバック地獄のいくつかの例があります: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については何も知りませんが、通常この問題はプログラミング言語に非同期計算のネイティブサポートを追加することで解決されます。実装はさまざまで、async、ジェネレーター、コルーチン、callccなどが含まれます。

Pythonでは、前のループの例を次のような形で実装することができます:

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

myGen = myLoop()
 

これは完全なコードではありませんが、要点は「yield」が私たちのforループを一時停止し、誰かがmyGen.next()を呼び出すまで待つということです。重要なのは、再帰的なloop関数のようにロジックを「裏返す」必要がなく、forループを使ってコードを書き続けることができるという点です。

回答 #2

Rxがコールバック地獄をどのように解決するかという質問に対処するために:

まずはもう一度コールバック地獄について説明しましょう。

例えば、私たちが3つのリソース - 人物、惑星、銀河 - を取得するためにHTTPを使用しなければならない場合を想像してください。私たちの目的は、その人物が住んでいる銀河を見つけることです。まず人物を取得し、次に惑星、そして銀河を取得する必要があります。それは、3つの非同期操作に対して3つのコールバックがあることを意味します。

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));

「mergeMap」別名「flatMap」演算子を使えば、より簡潔にすることができます:

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

ご覧のとおり、コードはフラット化されており、単一の関数呼び出しチェーンが含まれています。「地獄のピラミッド」はありません。

したがって、コールバック地獄は回避されます。

念のために言うと、プロミスはコールバック地獄を避けるもう一つの方法ですが、プロミスは熱心(イーガー)であり、オブザーバブルのように遅延(レイジー)ではなく、(一般的に言って)簡単にキャンセルすることはできません。

回答 #3

ただ質問に答えてください:その簡単な例でRXが「コールバック地獄の問題」をどのように解決するかも教えてもらえますか?

魔法は flatMap です。@hugomg の例のために、Rx で次のコードを書くことができます:

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

まるで同期的なFPコードを書いているようですが、実際にはSchedulerを使えば非同期にすることができます。

回答 #4

コールバック地獄とは、非同期コードで関数コールバックの使用が不明瞭または追跡が難しくなるコードのことです。一般的に、間接のレベルが複数ある場合、コールバックを使用するコードは追跡が難しく、リファクタリングが難しく、テストも難しくなります。コードの臭い(コードスメル)は、複数の関数リテラルを渡すことによる複数レベルのインデントです。

これは、動作に依存関係がある場合、つまり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 は、B が aFinished を監視した後にのみ発生する bFinished イベントを監視するようにすることができます。その後、追加のステップを簡単に加えたり、この種の動作を拡張したりすることができ、単にテストケースでイベントを発信するだけで、コードが順序通りに実行されるかを簡単にテストすることができます。

関連記事