Jak zachować czytelność kodu z kontynuacjami / wywołaniami zwrotnymi?

11

Podsumowanie: Czy istnieją pewne sprawdzone wzorce najlepszych praktyk, które mogę stosować, aby mój kod był czytelny pomimo korzystania z kodu asynchronicznego i funkcji zwrotnych?


Korzystam z biblioteki JavaScript, która wykonuje wiele czynności asynchronicznie i w dużej mierze opiera się na wywołaniach zwrotnych. Wydaje się, że napisanie prostej metody „load A, load B, ...” staje się dość skomplikowane i trudne do naśladowania przy użyciu tego wzorca.

Dam (wymyślony) przykład. Powiedzmy, że chcę załadować kilka obrazów (asynchronicznie) ze zdalnego serwera WWW. W C # / async napisałbym coś takiego:

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

Układ kodu przebiega zgodnie z „przebiegiem zdarzeń”: najpierw przycisk uruchamiania jest wyłączony, następnie obrazy są ładowane ( awaitzapewnia, że ​​interfejs użytkownika pozostaje responsywny), a następnie przycisk uruchamiania jest ponownie włączony.

W JavaScript, używając wywołań zwrotnych, wymyśliłem to:

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

Myślę, że wady są oczywiste: musiałem przerobić pętlę na wywołanie rekurencyjne, kod, który powinien zostać wykonany na końcu, znajduje się gdzieś w środku funkcji, kod rozpoczynający pobieranie ( loadImage(0)) znajduje się na samym dole, i ogólnie jest o wiele trudniej czytać i śledzić. Jest brzydka i nie podoba mi się.

Jestem pewien, że nie jestem pierwszym, który napotkał ten problem, więc moje pytanie brzmi: czy są jakieś dobrze ugruntowane wzorce najlepszych praktyk, które mogę zastosować, aby mój kod był czytelny pomimo korzystania z kodu asynchronicznego i wywołań zwrotnych?

Heinzi
źródło
Czy istnieje szczególny powód, dla którego wywołania „asynchroniczne” muszą być wykonywane sekwencyjnie? Czy to uproszczona wersja jakiegoś innego kodu?
Izkata
@Izkata: Powodem jest to, że chciałem być miły dla zdalnego serwera (= nie bombardować go setkami jednoczesnych żądań). To nie jest wymóg określony w kamieniu. Tak, jest to uproszczona wersja kodu, LoadImageAsyncw rzeczywistości jest połączeniem Ext.Ajax.requestz Sencha Touch.
Heinzi
1
Większość przeglądarek i tak nie pozwoli ci na młotkowanie serwera - po prostu ustawiają w kolejce żądania i uruchamiają następne po zakończeniu jednej z poprzednich.
Izkata
Bóg! wiele złych porad tutaj. Nie pomoże Ci żadna ilość wzorów. Zajrzyj do async.js , async.waterfallto twoja odpowiedź.
Salman von Abbas,

Odpowiedzi:

4

Jest bardzo mało prawdopodobne, że można osiągnąć za pomocą zwykłego js ten sam poziom zwięzłości i ekspresji w pracy z wywołaniami zwrotnymi, które ma C # 5. Kompilator wykonuje pracę, pisząc dla Ciebie całą tę płytkę, i dopóki środowiska wykonawcze js tego nie zrobią, nadal będziesz musiał od czasu do czasu przekazywać wywołanie zwrotne.

Jednak nie zawsze możesz sprowadzić wywołania zwrotne do poziomu prostoty kodu liniowego - rzucanie funkcjami nie musi być brzydkie, istnieje cały świat pracujący z tego rodzaju kodem, i zachowują rozsądek bez asynci await.

Na przykład użyj funkcji wyższego rzędu (mój plik js może być nieco zardzewiały):

// 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!"); }
);
vski
źródło
2

Trochę mi zajęło odkodowanie, dlaczego robisz to w ten sposób, ale myślę, że może to być blisko tego, czego chcesz?

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

Najpierw uruchamia tyle żądań AJAX, ile może zrobić jednocześnie, i czeka, aż wszystkie zostaną zakończone, zanim włączy przycisk Start. Będzie to szybsze niż sekwencyjne oczekiwanie i, jak sądzę, jest o wiele łatwiejsze do naśladowania.

EDYCJA : Zauważ, że zakłada to, że masz .each()dostępne i że myRepositoryjest to tablica. Uważaj, jakiej iteracji pętli używasz zamiast tego, jeśli nie jest dostępna - ta wykorzystuje właściwości zamknięcia dla wywołania zwrotnego. Nie jestem jednak pewien, co masz dostępne, ponieważ LoadImageAsyncwydaje się, że jest częścią specjalistycznej biblioteki - nie widzę żadnych wyników w Google.

Izkata
źródło
+1, mam .each()dostępne, a teraz, kiedy o tym wspominasz, ładowanie sekwencyjne nie jest absolutnie konieczne. Na pewno dam ci rozwiązanie. (Chociaż zaakceptuję odpowiedź Vskiego, ponieważ jest ona bliższa pierwotnemu, bardziej ogólnemu pytaniu).
Heinzi
@Heinzi Uzgodniono, jak to jest różne, ale (myślę) jest to również dobry przykład tego, jak różne języki mają różne sposoby radzenia sobie z tym samym. Jeśli coś wydaje się dziwne podczas tłumaczenia go na inny język, prawdopodobnie jest to łatwiejszy sposób, używając innego paradygmatu.
Izkata
1

Oświadczenie: ta odpowiedź nie odpowiada konkretnie na twój problem, jest ogólną odpowiedzią na pytanie: „Czy istnieją pewne dobrze znane wzorce najlepszych praktyk, które mogę zastosować, aby zachować czytelność mojego kodu pomimo używania kodu asynchronicznego i wywołań zwrotnych?”

Z tego, co wiem, nie ma „dobrze ugruntowanego” wzorca, aby sobie z tym poradzić. Jednak widziałem dwa rodzaje metod stosowanych w celu uniknięcia zagnieżdżonych koszmarów wywołania zwrotnego.

1 / Używanie nazwanych funkcji zamiast anonimowych wywołań zwrotnych

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

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

W ten sposób unikniesz zagnieżdżenia, wysyłając logikę funkcji anonimowej w innej funkcji.

2 / Korzystanie z biblioteki zarządzania przepływem. Lubię używać Step , ale to tylko kwestia preferencji. Nawiasem mówiąc, to ten, którego LinkedIn używa.

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

Korzystam z biblioteki zarządzania przepływem, gdy używam wielu zagnieżdżonych wywołań zwrotnych, ponieważ jest ona dużo bardziej czytelna, gdy używa jej dużo kodu.

Florian Margaine
źródło
0

Mówiąc najprościej, JavaScript nie ma cukru syntaktycznego await.
Ale przeniesienie części „końcowej” na dół funkcji jest łatwe; a dzięki natychmiastowemu wykonaniu funkcji anonimowej możemy uniknąć zadeklarowania odwołania do niej.

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

Możesz również przekazać część „end” jako wywołanie zwrotne sukcesu funkcji.

Cygański król
źródło