web-dev-qa-db-ja.com

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

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


非同期に多くのことを行い、コールバックに大きく依存する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();
_

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

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) )は一番下にあり、一般的に読みにくく、読みづらくなります。醜くて気に入らない。

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

10
Heinzi

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

ただし、コールバックを線形コードの単純さのレベルまで下げる必要があるとは限りません。関数をスローすることは醜い必要はありません。 全体の世界 がこの種のコードで機能し、彼らはasyncawaitなしで正気を保ちます。

たとえば、高次関数を使用します(私の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!"); }
);
4
vski

whyデコードするのに少し時間がかかりましたが、これはあなたが望むものに近いと思いますか?

_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リクエストは同時に実行でき、すべてが完了するまで待機してから[開始]ボタンを有効にします。これは、順次待機よりも高速です。 、従う方がはるかに簡単です。

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

2
Izkata

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

私が知っていることから、これを処理するための「確立された」パターンはありません。ただし、ネストされたコールバックの悪夢を回避するために使用される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
        }
    } );

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

1

簡単に言うと、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);

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

0
Gipsy King