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


113

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

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

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

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

それとも、シングルスレッドアプリケーションでも「コールバック地獄」が発生するのでしょうか。

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

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

回答:


136

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)または、シングルスレッドのアプリケーションでも「コールバック地獄」が発生する可能性はありますか?

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

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

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

5)その簡単な例で、RXが「コールバック地獄の問題」をどのように解決するかを示してもらえますか。

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

Pythonでは、次の行に沿って、前のループの例を実装できます。

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

myGen = myLoop()

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


だからコールバック地獄は非同期設定でのみ発生するのですか?私のコードが完全に同期している(つまり、並行性がない)場合、私があなたの答えを正しく理解していれば、「コールバック地獄」は発生しません、そうですか?
jhegedus 14

コールバック地獄は、継続渡しスタイルを使用したコーディングがいかに煩わしいかと関係があります。理論的には、通常のプログラムの場合でもCPSスタイルを使用してすべての関数を書き換えることができますが(ウィキペディアの記事にはいくつかの例があります)、正当な理由により、ほとんどの人はそうしません。通常、強制された場合にのみ継続渡しスタイルを使用します。これは、Javascript非同期プログラミングの場合です。
hugomg 2014

ところで、リアクティブな拡張機能をググると、非同期構文を導入する言語拡張機能ではなく、Promiseライブラリに似ているという印象を受けました。Promiseは、コールバックのネストと例外処理を処理するのに役立ちますが、構文拡張と同じように整然としています。forループは依然としてコードを煩わしく、コードを同期スタイルからpromiseスタイルに変換する必要があります。
hugomg 2014

1
RXが一般によりよく機能する方法を明確にすべきです。RXは宣言型です。他のプログラムロジックに影響を与えることなく、後でイベントが発生したときにプログラムがイベントにどのように応答するかを宣言できます。これにより、メインループコードをイベント処理コードから分離できます。状態変数を使用する場合の悪夢である非同期イベントの順序付けなどの詳細を簡単に処理できます。RXは、3つのネットワーク応答が返された後に新しいネットワーク要求を実行するか、1つが返されない場合はチェーン全体をエラー処理するための最もクリーンな実装であることがわかりました。次に、それ自体をリセットし、同じ3つのイベントを待ちます。
colintheshots 14

もう1つの関連コメント:RXは基本的に継続モナドであり、私が誤っていない場合はCPSに関連します。これは、RXがコールバック/地獄の問題にどのように/なぜ良いのかを説明する場合もあります。
jhegedus 14年

30

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

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

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

同期FPコードを記述しているようですが、実際にはを使用して非同期にすることができますScheduler


26

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

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

人、惑星、銀河の3つのリソースを取得するためにhttpを実行しなければならないケースを想像してみてください。私たちの目的は、その人が住んでいる銀河を見つけることです。最初に人、次に惑星、次に銀河を取得する必要があります。これは、3つの非同期操作に対する3つのコールバックです。

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

各コールバックはネストされています。各内部コールバックは、その親に依存しています。これは、コールバック地獄の「運命のピラミッド」スタイルにつながります。コードは>記号のように見えます。

RxJでこれを解決するには、次のようにします。

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

mergeMapAKAのflatMapオペレータあなたはそれをより簡潔にすることができます:

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

ご覧のとおり、コードはフラット化されており、メソッド呼び出しのチェーンが1つ含まれています。「運命のピラミッド」はありません。

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

ご参考までに、promiseはコールバックの地獄を回避するもう1つの方法ですが、promiseは熱心であり、オブザーバブルのように怠惰ではなく、(一般的に言えば)簡単にキャンセルすることはできません。


私はJS開発者ではありませんが、これは簡単な説明です
Omar Beshary

15

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

これは、動作に依存関係がある場合、つまりAがBの前に発生する必要がある場合によく発生します。次に、次のようなコードを取得します。

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」をリッスンして呼び出された後にのみ発生します。その後、追加のステップを簡単に追加したり、この種の動作を拡張したり、テストケースでイベントをブロードキャストするだけで、コードが順番に実行されることを簡単にテストしたりできます。


1

コールバック地獄とは、別のコールバック内のコールバック内にいることを意味し、フルフィールドが必要なくなるまでn番目のコールに進みます。

set timeout APIを使用して偽のajax呼び出しの例を理解してみましょう。レシピAPIがあるとしましょう。すべてのレシピをダウンロードする必要があります。

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
            }, 1500);
        }
        getRecipe();
    </script>
</body>

上の例では、1.5秒後にタイマーが時間切れになると、コールバックのコードが実行されます。つまり、偽のajax呼び出しによって、すべてのレシピがサーバーからダウンロードされます。次に、特定のレシピデータをダウンロードする必要があります。

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

特定のレシピデータをダウンロードするために、最初のコールバック内にコードを記述し、レシピIDを渡しました。

ここで、IDが7638であるレシピの同じ発行元のすべてのレシピをダウンロードする必要があるとします。

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                    setTimeout(publisher=>{
                        const recipe2 = {title:'Fresh Apple Pie', publisher:'Suru'};
                        console.log(recipe2);
                    }, 1500, recipe.publisher);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

パブリッシャー名suruのすべてのレシピをダウンロードするという私たちのニーズを完全に満たすために、2番目のコールバック内にコードを記述しました。コールバック地獄と呼ばれるコールバックチェーンを作成したことは明らかです。

コールバックの地獄を避けたい場合は、js es6機能であるPromiseを使用できます。各Promiseは、Promiseがいっぱいになったときに呼び出されるコールバックを受け取ります。promiseコールバックには、解決するか拒否するかの2つのオプションがあります。あなたのAPI呼び出しを使用すると、決意を呼び出すとを介してデータを渡すことができます成功していると仮定決意、あなたが使用してこのデータを取得することができ、その後() 。ただし、APIが失敗した場合は、リジェクトを使用し、catchを使用してエラーをキャッチできます。解決のために常に使用し拒否のためにキャッチする約束を忘れないでください

以前のコールバック地獄問題をプロミスを使って解決しましょう。

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        getIds.then(IDs=>{
            console.log(IDs);
        }).catch(error=>{
            console.log(error);
        });
    </script>
</body>

次に、特定のレシピをダウンロードします。

<body>
    <script>
        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }
        getIds.then(IDs=>{
            console.log(IDs);
            return getRecipe(IDs[2]);
        }).
        then(recipe =>{
            console.log(recipe);
        })
        .catch(error=>{
            console.log(error);
        });
    </script>
</body>

これで、getRecipeなどの別のメソッド呼び出しallRecipeOfAPublisherを記述して、promiseを返すことができます。また、別のthen()を記述して、allRecipeOfAPublisherの解決プロミスを受け取ることができます。この時点で、自分で実行できることを願っています。

そこで、プロミスを作成して使用する方法を学びました。次に、es8で導入されたasync / awaitを使用して、プロミスを簡単に使用できるようにします。

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }

        async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

        getRecipesAw();
    </script>
</body>

上記の例では、バックグラウンドで実行されるため、非同期関数を使用しました。非同期関数内では、約束が返されるか、または約束である各メソッドの前にawaitキーワードを使用し、約束が満たされるまでその位置で待機するため、つまりgetIdsが解決されるまでコードを拒否するか、プログラムが拒否されると、IDが返されたときにその行の下のコードの実行が停止し、IDを指定してgetRecipe()関数を再度呼び出し、データが返されるまでawaitキーワードを使用して待機しました。これが最終的にコールバック地獄から回復した方法です。

  async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

awaitを使用するには、非同期関数が必要です。promiseを返すことができるため、resolve promiseに使用し、cathをreject promiseに使用します。

上記の例から:

 async function getRecipesAw(){
            const IDs = await getIds;
            const recipe = await getRecipe(IDs[2]);
            return recipe;
        }

        getRecipesAw().then(result=>{
            console.log(result);
        }).catch(error=>{
            console.log(error);
        });

0

コールバック地獄を回避できる1つの方法は、RXの「拡張バージョン」であるFRPを使用することです。

FRPの適切な実装Sodiumhttp://sodium.nz/)を見つけたので、最近FRPを使い始めました。

典型的なコードは次のようになります(Scala.js):

def render: Unit => VdomElement = { _ =>
  <.div(
    <.hr,
    <.h2("Note Selector"),
    <.hr,
    <.br,
    noteSelectorTable.comp(),
    NoteCreatorWidget().createNewNoteButton.comp(),
    NoteEditorWidget(selectedNote.updates()).comp(),
    <.hr,
    <.br
  )
}

selectedNote.updates()は(これは)が変更されたStream場合に起動され、それに応じて更新されます。selectedNodeCellNodeEditorWidget

そのため、の内容に応じて、selectedNode Cell現在編集Noteされているものが変わります。

このコードは、Callbackを完全に回避します。ほとんどの場合、Cacllbackはアプリの「外層」/「表面」にプッシュされ、ここで状態処理ロジックは外部の世界とインターフェースします。内部状態処理ロジック(状態マシンを実装)内でデータを伝播するために必要なコールバックはありません。

完全なソースコードはこちらです

上記のコードスニペットは、次の簡単な作成、表示、更新の例に対応しています。

ここに画像の説明を入力してください

このコードはサーバーにも更新を送信するため、更新されたエンティティへの変更はサーバーに自動的に保存されます。

すべてのイベント処理は、StreamsとCells を使用して処理されます。これらはFRPの概念です。ユーザー入力、テキストの編集、ボタンの押下、AJAX呼び出しが戻るなど、FRPロジックが外部の世界とインターフェースする場合にのみ、コールバックが必要です。

データフローは、FRP(Sodiumライブラリによって実装)を使用して宣言的に記述されるため、データフローを記述するためのイベント処理/コールバックロジックは必要ありません。

FRP(RXのより「厳密な」バージョン)は、状態を含むノードを含むことができるデータフローグラフを記述する方法です。イベントは、ノード(Cells と呼ばれる)を含む状態の状態変化をトリガーします。

ナトリウムはより高次のFRPライブラリです。つまり、flatMap/ switchプリミティブを使用すると、実行時にデータフローグラフを再配置できます。

私はSodiumブックを一読することをお勧めします。これは、FRPがすべてのコールバックを取り除く方法を詳細に説明しています。このコールバックは、いくつかの外部刺激に応じてアプリケーションの状態を更新することに関係するデータフローロジックを記述するために不可欠ではありません。

FRPを使用すると、外界との相互作用を説明するコールバックのみを保持する必要があります。つまり、FRPフレームワーク(Sodiumなど)を使用する場合、または「FRPのような」フレームワーク(RXなど)を使用する場合、データフローは機能的/宣言的な方法で記述されます。

SodiumはJavascript / Typescriptでも利用できます。


-3

コールバックとhellコールバックについての知識がなくても問題ありません。最初にコールバックしてhellをコールバックします。例:hellコールバックは、クラス内にクラスを格納できるようなものです。 C、C ++言語でネストされたものについて。ネストされたクラスは、別のクラス内のクラスを意味します。


「コールバック地獄」とは何かを示すコードスニペットと、「コールバック地獄」を削除した後のRxと同じコードスニペットが含まれている場合、回答はさらに役立ちます
rafa

-4

jazz.js https://github.com/Javanile/Jazz.jsを使用して ください

それはこのように単純化します:

    //連鎖タスクを順次実行します
    jj.script([
        //最初のタスク
        function(next){
            //このプロセスの最後に、2番目のタスクを「次へ」ポイントして実行します 
            callAsyncProcess1(next);
        }、
      // 2番目のタスク
      function(next){
        //このプロセスの最後にタスクを「次へ」ポイントして実行します 
        callAsyncProcess2(next);
      }、
      // thirt task
      function(next){
        //このプロセスの最後に(次の)を指す(ある場合) 
        callAsyncProcess3(next);
      }、
    ]);


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.