129

JavaScript と node.js を知らない人にとって「コールバック地獄」とは何かを説明する簡単な例とともに、誰かが明確な定義を与えることができますか?

「コールバック地獄問題」はいつ(どのような設定で)発生しますか?

なぜ発生するのですか?

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

または、シングルスレッドアプリケーションでも「コールバック地獄」が発生する可能性はありますか?

私は Coursera で Reactive Course を受講しましたが、Erik Meijer は講義の 1 つで、RX は「コールバック地獄」の問題を解決すると述べました。Coursera フォーラムで「コールバック地獄」とは何かを尋ねましたが、明確な答えはありませんでした。

簡単な例で「コールバック地獄」を説明した後、RX がその簡単な例で「コールバック地獄の問題」を解決する方法も示していただけますか?

4

8 に答える 8

149

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 で一連の非同期関数を実行する唯一の方法は、1 つが前の戻り値の後に実行される場合、再帰関数を使用することです。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) または、シングル スレッド アプリケーションでも「コールバック地獄」が発生する可能性はありますか?

非同期プログラミングは並行性と関係があり、シングルスレッドは並列性と関係があります。2 つの概念は実際には同じものではありません。

シングル スレッド コンテキストで並行コードを使用することもできます。実際、コールバック地獄の女王である JavaScript はシングル スレッドです。

並行性と並列性の違いは何ですか?

5) RX がその単純な例で「コールバック地獄の問題」を解決する方法も示してください。

特に RX については何も知りませんが、通常、この問題はプログラミング言語に非同期計算のネイティブ サポートを追加することで解決されます。実装はさまざまで、async、ジェネレーター、コルーチン、callcc などがあります。

Python では、前のループの例を次のようなもので実装できます。

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

myGen = myLoop()

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

于 2014-08-02T18:38:36.213 に答える
31

質問に答えてください。RX がその単純な例で「コールバック地獄の問題」を解決する方法も示していただけますか?

魔法はflatMap。@hugomg の例では、Rx に次のコードを記述できます。

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

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

于 2014-08-04T01:20:14.510 に答える
15

コールバック地獄とは、非同期コードでの関数コールバックの使用がわかりにくくなったり、追跡が困難になったりするコードです。一般に、間接的なレベルが複数ある場合、コールバックを使用するコードの追跡、リファクタリング、およびテストが難しくなる可能性があります。コードの臭いは、関数リテラルの複数のレイヤーを渡すことによる複数レベルのインデントです。

これは、動作に依存関係がある場合、つまり、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 は「リアクティブ エクステンション」の略です。私はそれを使用していませんが、グーグルはそれがイベントベースのフレームワークであることを示唆しており、それは理にかなっています. イベントは、脆弱な結合を作成せずにコードを順番に実行するための一般的なパターンです。B が「aFinished」をリッスンするように呼び出された後にのみ発生するイベント「bFinished」を C にリッスンさせることができます。その後、追加のステップを簡単に追加したり、この種の動作を拡張したりできます。また、テスト ケースでイベントをブロードキャストするだけで、コードが順番に実行されることを簡単にテストできます。

于 2014-08-02T18:38:52.273 に答える
-4

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

次のように単純化します。

    // チェーンされた順次タスクを実行します
    jj.script([
        // 最初のタスク
        関数(次) {
            // このプロセスの最後に 'next' で 2 番目のタスクを指定して実行します
            callAsyncProcess1(次);
        }、
      // 2 番目のタスク
      関数(次) {
        // このプロセスの最後に 'next' で 3 番目のタスクを指定して実行します
        callAsyncProcess2(次);
      }、
      // 3 番目のタスク
      関数(次) {
        // このプロセスの最後に 'next' ポイント (ある場合)
        callAsyncProcess3(次);
      }、
    ]);

于 2016-10-03T09:08:13.323 に答える