継続/コールバックを読みやすいコードにするにはどうすればよいですか?


10

概要:非同期コードとコールバックを使用しているにもかかわらず、コードを読みやすくするために使用できる、確立されたベストプラクティスのパターンはありますか?


非同期に多くのことを行い、コールバックに大きく依存するJavaScriptライブラリを使用しています。単純な "load A、load B、..."メソッドを書くのはかなり複雑になり、このパターンを使用するのは難しいようです。

(不自然な)例を挙げましょう。リモートWebサーバーから一連の画像を(非同期で)ロードしたいとします。C#/ asyncでは、次のように記述します。

disableStartButton();

foreach (myData in myRepository) {
    var result = await LoadImageAsync("http://my/server/GetImage?" + myData.Id);
    if (result.Success) {
        myData.Image = result.Data;
    } else {
        write("error loading Image " + myData.Id);
        return;
    }
}

write("success");
enableStartButton();

コードレイアウトは「イベントのフロー」に従います。最初に、開始ボタンが無効になり、次に画像が読み込まれ(awaitUIの応答性が維持されます)、その後、開始ボタンが再び有効になります。

JavaScriptでは、コールバックを使用して、これを思いつきました:

disableStartButton();

var count = myRepository.length;

function loadImage(i) {
    if (i >= count) {
        write("success");
        enableStartButton();
        return;
    }

    myData = myRepository[i];
    LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) { 
            if (success) {
                myData.Image = data;
            } else {
                write("error loading image " + myData.Id);
                return;
            }
            loadImage(i+1); 
        }
    );
}

loadImage(0);

欠点は明らかだと思います:ループを再帰呼び出しに作り直す必要がありました。最後に実行されるはずのコードは、関数の途中のどこかにあり、ダウンロードを開始するコード(loadImage(0))は一番下にあります。そしてそれは一般的に読み、従うのがはるかに困難です。醜くて気に入らない。

私がこの問題に最初に遭遇したのは自分ではないと確信しているので、私の質問は、非同期コードとコールバックを使用しているにもかかわらず、コードを読みやすくするために使用できる、確立されたベストプラクティスのパターンはありますか?


「非同期」呼び出しを順番に実行する必要がある特別な理由はありますか?これは他のコードの簡略版ですか?
イズカタ

@Izkata:その理由は、私がリモートサーバーに親切になりたいということです(=何百もの同時要求で攻撃しないでください)。これは、必須の要件ではありません。はい、それはコードの簡略化されたバージョンでLoadImageAsyncあり、実際にExt.Ajax.requestはSencha Touch への呼び出しです。
ハインツィ

1
ほとんどのブラウザーは、とにかくサーバーをハンマーで打つことを許可しません-それらはリクエストをキューに入れ、前のいずれかが完了すると次のリクエストを開始するだけです。
イズカタ


神!ここにたくさんの悪いアドバイスがあります。どんな量のデザインパターンもあなたを助けにはなりません。見async.jsはasync.waterfallあなたの答えです。
Salman von Abbas

回答:


4

プレーンなjsで、C#5が持つコールバックと同じレベルの簡潔さと表現力を実現できる可能性はほとんどありません。コンパイラーは、すべてのボイラープレートを作成する作業を行います。jsランタイムがそれを行うまでは、時々コールバックをあちこちに渡さなければなりません。

ただし、コールバックを線形コードのシンプルさのレベルまで下げる必要があるとは限りません。関数をスローすることは醜い必要はありません。この種のコードで作業する世界が存在し、そしてとなしasyncで正気を保ちawaitます。

たとえば、高次関数を使用します(私のjsは少し錆びているかもしれません):

// generic - this is a library function
function iterateAsync(iterator, action, onSuccess, onFailure) {
var item = iterator();
if(item == null) { // exit condition
    onSuccess();
    return;
}
action(item,
    function (success) {
        if(success)
            iterateAsync(iterator, action, onSuccess, onFailure);
        else
            onFailure();
    });
}


// calling code
var currentImage = 0;
var imageCount = 42;

// you know your library function expects an iterator with no params, 
// and an async action with the current item and its continuation as params
iterateAsync(
// this is your iterator
function () {   
    if(currentImage >= imageCount)
        return null;
    return "http://my/server/GetImage?" + (currentImage++);
},

// this is your action - coincidentally, no adaptor for the correct signature is necessary
LoadImageAsync,

// these are your outs
function () { console.log("All OK."); },
function () { console.log("FAILED!"); }
);

2

なぜあなたがこのようにそれをしているのをデコードするために少し時間を取ったが、これはあなたが望むものに近いかもしれないと思いますか?

function loadImages() {
   var countRemainingToLoad = 0;
   var failures = 0;

   myRepository.each(function (myData) {
      countRemainingToLoad++;

      LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) {
            if (success) {
                myData.Image = data;
            } else {
                write("error loading image " + myData.Id);
                failures++;
            }
            countRemainingToLoad--;
            if (countRemainingToLoad == 0 && failures == 0) {
                enableStartButton();
            }
        }
    );
}

disableStartButton();
loadImages();

まず、同時に実行できる限り多くのAJAX要求をオフにし、それらがすべて完了するまで待機してから、[開始]ボタンを有効にします。これは、順次待機よりも高速であり、理解しやすいと思います。

編集:これはあなたが.each()利用可能であることを前提としていることに注意してください、そしてそれmyRepositoryは配列です。代わりにここで使用するループの反復に注意してください(使用できない場合)。これは、コールバックのクロージャープロパティを利用しています。ただし、LoadImageAsync専門のライブラリの一部であると思われるため、何が利用できるかわかりません。Googleには結果が表示されません。


+1、私は.each()利用できます、そしてあなたがそれを言ったので、順番にロードを行うことは厳密には必要ではありません。私は間違いなくあなたの解決策を試してみます。(元のより一般的な質問に近いので、vskiの回答は受け入れますが)
Heinzi

@Heinziはそれがどのように異なるかについて同意しましたが、(私は思う)これはまた、異なる言語が同じことを処理する異なる方法を持っている方法の適切な例でもあります。別の言語に翻訳するときに何かが不快に感じる場合は、おそらく別のパラダイムを使用することによってそれを行う簡単な方法があるでしょう。
イズカタ

1

免責事項:この答えは具体的にはあなたの問題に答えるものではなく、質問に対する一般的な答えです:「非同期コードとコールバックを使用しているにもかかわらず、コードを読みやすくするために私が従うことができる確立されたベストプラクティスのパターンはありますか?」

私が知っていることから、これを処理するための「確立された」パターンはありません。ただし、ネストされたコールバックの悪夢を回避するために使用される2種類の方法を見てきました。

1 /匿名コールバックの代わりに名前付き関数を使用する

    function start() {
        mongo.findById( id, handleDatas );
    }

    function handleDatas( datas ) {
        // Handle the datas returned.
    }

このようにして、無名関数のロジックを別の関数に送信することにより、ネストを回避します。

2 /フロー管理ライブラリの使用。Stepを使用したいのですが、それは好みの問題です。ちなみに、これはLinkedInが使用するものです。

    Step( {
        function start() {
            // the "this" magically sends to the next function.
            mongo.findById( this );
        },

        function handleDatas( el ) {
            // Handle the datas.
            // Another way to use it is by returning a value,
            // the value will be sent to the next function.
            // However, this is specific to Step, so look at
            // the documentation of the library you choose.
            return value;
        },

        function nextFunction( value ) {
            // Use the returned value from the preceding function
        }
    } );

ネストされた多数のコールバックを使用する場合は、フロー管理ライブラリを使用します。これは、使用するコードが多い場合の方が読みやすいためです。


0

簡単に言うと、JavaScriptにはの構文糖はありませんawait
しかし、「終了」部分を関数の下部に移動するのは簡単です。そして、すぐに実行される無名関数を使用すると、その関数への参照を宣言することを回避できます。

disableStartButton();

(function(i, count) {
    var loadImage = arguments.callee;
    myData = myRepository[i];

    LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) { 
            if (!success) {
                write("error loading image " + myData.Id);

            } else {
                myData.Image = data;
                if (i < count) {
                    loadImage(i + 1, count);

                } else {
                    write("success");
                    enableStartButton();
                    return;

                }

            }

        }
    );
})(0, myRepository.length);

関数の成功コールバックとして「終了」部分を渡すこともできます。

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