Co to jest „piekło zwrotne” i jak i dlaczego RX go rozwiązuje?


113

Czy ktoś może podać jasną definicję wraz z prostym przykładem wyjaśniającym, czym jest „piekło zwrotne” dla kogoś, kto nie zna JavaScript i node.js?

Kiedy (w jakich ustawieniach) pojawia się „problem z piekłem zwrotnym”?

Dlaczego tak się dzieje?

Czy „piekło zwrotne” jest zawsze związane z obliczeniami asynchronicznymi?

A może „piekło zwrotne” może wystąpić również w aplikacji jednowątkowej?

Wziąłem kurs reaktywny w Coursera, a Erik Meijer powiedział na jednym ze swoich wykładów, że RX rozwiązuje problem „piekła zwrotnego”. Zapytałem na forum Coursera, co to jest „piekło zwrotne”, ale nie otrzymałem jednoznacznej odpowiedzi.

Po wyjaśnieniu „piekła oddzwonienia” na prostym przykładzie, czy możesz również pokazać, jak RX rozwiązuje „piekło oddzwonienia” na tym prostym przykładzie?

Odpowiedzi:


136

1) Co to jest „piekło zwrotne” dla kogoś, kto nie zna javascript i node.js?

To drugie pytanie ma kilka przykładów piekła zwrotnego JavaScript: jak uniknąć długiego zagnieżdżania funkcji asynchronicznych w Node.js

Problem w Javascript polega na tym, że jedynym sposobem na „zamrożenie” obliczenia i umożliwienie wykonania drugiej (asynchronicznej) „reszty” jest umieszczenie „reszty” wewnątrz wywołania zwrotnego.

Na przykład, powiedzmy, że chcę uruchomić kod, który wygląda następująco:

x = getData();
y = getMoreData(x);
z = getMoreData(y);
...

Co się stanie, jeśli teraz chcę, aby funkcje getData były asynchroniczne, co oznacza, że ​​mam szansę uruchomić inny kod, czekając, aż zwrócą swoje wartości? W Javascript jedynym sposobem byłoby przepisanie wszystkiego, co dotyka obliczeń asynchronicznych, przy użyciu stylu przekazywania kontynuacji :

getData(function(x){
    getMoreData(x, function(y){
        getMoreData(y, function(z){ 
            ...
        });
    });
});

Chyba nie muszę nikogo przekonywać, że ta wersja jest brzydsza od poprzedniej. :-)

2) Kiedy (w jakich ustawieniach) pojawia się „problem z piekłem zwrotnym”?

Kiedy masz dużo funkcji zwrotnych w swoim kodzie! Praca z nimi staje się trudniejsza, im więcej jest ich w kodzie, a szczególnie źle się dzieje, gdy trzeba wykonywać pętle, blokować try-catch i tym podobne.

Na przykład, o ile wiem, w JavaScript jedynym sposobem wykonania szeregu funkcji asynchronicznych, w których jedna jest uruchamiana po poprzednich zwrotach, jest użycie funkcji rekurencyjnej. Nie możesz użyć pętli for.

// we would like to write the following
for(var i=0; i<10; i++){
    doSomething(i);
}
blah();

Zamiast tego może być konieczne napisanie:

function loop(i, onDone){
    if(i >= 10){
        onDone()
    }else{
        doSomething(i, function(){
            loop(i+1, onDone);
        });
     }
}
loop(0, function(){
    blah();
});

//ugh!

Liczba pytań, które otrzymujemy tutaj w StackOverflow, pytając, jak zrobić tego rodzaju rzeczy, świadczy o tym, jak bardzo jest to zagmatwane :)

3) Dlaczego tak się dzieje?

Dzieje się tak, ponieważ w JavaScript jedynym sposobem na opóźnienie obliczeń, tak aby były one uruchamiane po powrocie wywołania asynchronicznego, jest umieszczenie opóźnionego kodu wewnątrz funkcji wywołania zwrotnego. Nie możesz opóźnić kodu, który został napisany w tradycyjnym stylu synchronicznym, więc wszędzie pojawiają się zagnieżdżone wywołania zwrotne.

4) Czy też „piekło zwrotne” może wystąpić również w aplikacji jednowątkowej?

Programowanie asynchroniczne ma do czynienia ze współbieżnością, podczas gdy pojedynczy wątek ma do czynienia z równoległością. Te dwie koncepcje w rzeczywistości nie są tym samym.

Nadal możesz mieć współbieżny kod w kontekście z pojedynczym wątkiem. W rzeczywistości JavaScript, królowa piekła zwrotnego, jest jednowątkowy.

Jaka jest różnica między współbieżnością a równoległością?

5) Czy mógłbyś również pokazać, jak RX rozwiązuje „problem piekła zwrotnego” na tym prostym przykładzie.

W szczególności nie wiem nic o RX, ale zwykle ten problem rozwiązuje się dodając natywną obsługę obliczeń asynchronicznych w języku programowania. Implementacje mogą się różnić i obejmować: async, generatory, coroutines i callcc.

W Pythonie możemy zaimplementować ten poprzedni przykład pętli za pomocą czegoś w rodzaju:

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

myGen = myLoop()

To nie jest pełny kod, ale idea jest taka, że ​​„yield” wstrzymuje naszą pętlę for do momentu, gdy ktoś wywoła myGen.next (). Ważną rzeczą jest to, że nadal moglibyśmy pisać kod przy użyciu pętli for, bez konieczności odwracania logiki „na lewą stronę”, tak jak musieliśmy to zrobić w tej loopfunkcji rekurencyjnej .


Więc piekło oddzwaniania może wystąpić tylko w ustawieniu asynchronicznym? Jeśli mój kod jest w pełni synchroniczny (tj. Nie ma współbieżności), to „piekło zwrotne” nie może wystąpić, jeśli dobrze rozumiem twoją odpowiedź, czy to prawda?
jhegedus

Piekło wywołania zwrotnego ma więcej wspólnego z tym, jak denerwujące jest kodowanie w stylu przekazywania kontynuacji. Teoretycznie nadal możesz przepisać wszystkie swoje funkcje w stylu CPS, nawet dla zwykłego programu (artykuł na Wikipedii zawiera kilka przykładów), ale nie bez powodu większość ludzi tego nie robi. Zwykle używamy stylu przekazywania kontynuacji tylko wtedy, gdy jesteśmy do tego zmuszeni, co ma miejsce w przypadku programowania asynchronicznego JavaScript.
hugomg

btw, wyszukałem w Google rozszerzenia reaktywne i mam wrażenie, że są one bardziej podobne do biblioteki Promise, a nie do rozszerzenia języka wprowadzającego składnię asynchroniczną. Obietnice pomagają radzić sobie z zagnieżdżaniem wywołań zwrotnych i obsługą wyjątków, ale nie są tak zgrabne, jak rozszerzenia składni. Pętla for jest nadal irytująca dla kodu i nadal musisz przetłumaczyć kod ze stylu synchronicznego na styl obietnicy.
hugomg

1
Powinienem wyjaśnić, w jaki sposób RX generalnie wykonuje lepszą pracę. RX jest deklaratywne. Możesz zadeklarować, w jaki sposób program będzie reagował na zdarzenia, które wystąpią później, bez wpływu na inną logikę programu. Pozwala to oddzielić główny kod pętli od kodu obsługi zdarzenia. Możesz łatwo obsługiwać szczegóły, takie jak porządkowanie zdarzeń asynchronicznych, które są koszmarem, gdy używasz zmiennych stanu. Zauważyłem, że RX jest najczystszą implementacją do wykonywania nowego żądania sieciowego po 3 zwróconych odpowiedziach sieciowych lub do obsługi błędu całego łańcucha, jeśli jeden nie zwraca. Następnie może się zresetować i czekać na te same 3 zdarzenia.
colintheshots

Jeszcze jeden powiązany komentarz: RX jest w zasadzie monadą kontynuacyjną, która odnosi się do CPS, jeśli się nie mylę, może to również wyjaśnić, jak / dlaczego RX jest dobre dla problemu z oddzwonieniem / piekłem.
jhegedus

30

Po prostu odpowiedz na pytanie: czy mógłbyś również pokazać, jak RX rozwiązuje „problem piekła zwrotnego” na tym prostym przykładzie?

Magia jest flatMap. Możemy napisać następujący kod w Rx dla przykładu @ hugomg:

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

To tak, jakbyś pisał synchroniczne kody FP, ale w rzeczywistości możesz uczynić je asynchronicznymi przez Scheduler.


26

Aby odpowiedzieć na pytanie, jak Rx rozwiązuje piekło zwrotne :

Najpierw opiszmy ponownie piekło oddzwaniania.

Wyobraź sobie przypadek, w którym musimy wykonać http, aby uzyskać trzy zasoby - osobę, planetę i galaktykę. Naszym celem jest znalezienie galaktyki, w której żyje człowiek. Najpierw musimy znaleźć osobę, potem planetę, a na końcu galaktykę. To trzy wywołania zwrotne dla trzech operacji asynchronicznych.

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

Każde wywołanie zwrotne jest zagnieżdżone. Każde wewnętrzne wywołanie zwrotne jest zależne od swojego rodzica. Prowadzi to do stylu „piramidy zagłady” w stylu callback hell . Kod wygląda jak znak>.

Aby rozwiązać ten problem w RxJs, możesz zrobić coś takiego:

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

Dzięki operatorowi mergeMapAKA flatMapmożesz uczynić to bardziej zwięzłym:

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

Jak widać, kod jest spłaszczony i zawiera pojedynczy łańcuch wywołań metod. Nie mamy „piramidy zagłady”.

W ten sposób unika się piekła zwrotnego.

Jeśli się zastanawiałeś, obietnice to kolejny sposób na uniknięcie piekła oddzwonienia, ale obietnice są chętne , nie leniwe jak obserwowalne i (ogólnie rzecz biorąc) nie możesz ich tak łatwo anulować.


Nie jestem programistą JS, ale to proste wyjaśnienie
Omar Beshary

15

Piekło zwrotne to każdy kod, w którym użycie funkcji zwrotnych w kodzie asynchronicznym staje się niejasne lub trudne do naśladowania. Generalnie, gdy istnieje więcej niż jeden poziom pośrednictwa, kod wykorzystujący wywołania zwrotne może stać się trudniejszy do śledzenia, trudniejszy do refaktoryzacji i trudniejszy do przetestowania. Zapach kodu to wielopoziomowe wcięcie spowodowane przekazywaniem wielu warstw literałów funkcyjnych.

Dzieje się tak często, gdy zachowanie ma zależności, tj. Gdy A musi się zdarzyć, zanim B musi się wydarzyć przed C. Następnie otrzymujesz taki kod:

a({
    parameter : someParameter,
    callback : function() {
        b({
             parameter : someOtherParameter,
             callback : function({
                 c(yetAnotherParameter)
        })
    }
});

Jeśli masz w swoim kodzie wiele zależności behawioralnych, takich jak ten, może to szybko stać się kłopotliwe. Zwłaszcza jeśli się rozgałęzia ...

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

To nie wystarczy. Jak możemy sprawić, by kod asynchroniczny był wykonywany w określonej kolejności bez konieczności przekazywania wszystkich tych wywołań zwrotnych?

RX jest skrótem od „reaktywnych rozszerzeń”. Nie używałem tego, ale Googling sugeruje, że jest to framework oparty na zdarzeniach, co ma sens. Zdarzenia to typowy wzorzec umożliwiający wykonanie kodu w kolejności bez tworzenia kruchych sprzężeń . Możesz zmusić C do nasłuchiwania zdarzenia „bFinished”, które ma miejsce dopiero po wywołaniu B nasłuchiwania „aFinished”. Następnie możesz łatwo dodać dodatkowe kroki lub rozszerzyć ten rodzaj zachowania i możesz łatwo sprawdzić , czy kod wykonuje się po kolei, po prostu transmitując zdarzenia w przypadku testowym.


1

Oddzwanianie do piekła oznacza, że ​​znajdujesz się w środku oddzwaniania lub w innym wywołaniu zwrotnym i przechodzi do n-tego połączenia, dopóki twoje potrzeby nie zostaną spełnione.

Przyjrzyjmy się przykładowi fałszywego wywołania ajax przy użyciu API set timeout, załóżmy, że mamy API receptury, musimy pobrać całą recepturę.

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

W powyższym przykładzie po upływie 1,5 sekundy, gdy licznik czasu wygaśnie, zostanie wykonany kod wywołania zwrotnego, innymi słowy, za pośrednictwem naszego fałszywego wywołania Ajax cała receptura zostanie pobrana z serwera. Teraz musimy pobrać dane konkretnego przepisu.

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

Aby pobrać dane konkretnego przepisu, napisaliśmy kod w naszym pierwszym wywołaniu zwrotnym i przekazaliśmy identyfikator przepisu.

Powiedzmy, że musimy pobrać wszystkie przepisy tego samego wydawcy przepisu o identyfikatorze 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>

Aby w pełni zaspokoić nasze potrzeby, czyli pobrać wszystkie przepisy wydawcy suru, napisaliśmy kod w naszym drugim oddzwonieniu. Jasne jest, że napisaliśmy łańcuch wywołań zwrotnych, który nazywa się piekło zwrotnych.

Jeśli chcesz uniknąć piekła oddzwaniania, możesz użyć Promise, która jest funkcją js es6, każda obietnica przyjmuje wywołanie zwrotne, które jest wywoływane, gdy obietnica jest pełna. obietnica callback ma dwie opcje: zostanie rozwiązana lub odrzucona. Załóżmy, że wywołanie interfejsu API zakończyło się pomyślnie, możesz wywołać metodę rozwiązywania i przekazywać dane przez rozwiązanie , możesz uzyskać te dane za pomocą metody then () . Ale jeśli twój interfejs API zawiódł, możesz użyć odrzucenia, użyj catch, aby złapać błąd. Pamiętaj obietnicę zawsze używać wtedy dla determinacji i połowu do odrzucenia

Rozwiążmy poprzedni problem z piekłem oddzwonienia, używając obietnicy.

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

Teraz pobierz konkretny przepis:

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

Teraz możemy napisać inną metodę call allRecipeOfAPublisher, taką jak getRecipe, która również zwróci obietnicę, i możemy napisać inną metodę then (), aby otrzymać obietnicę rozwiązania dla allRecipeOfAPublisher, mam nadzieję, że w tym momencie możesz to zrobić samodzielnie.

Więc nauczyliśmy się, jak konstruować i konsumować obietnice, teraz ułatwmy korzystanie z obietnic, używając async / await, które zostało wprowadzone w es8.

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

W powyższym przykładzie użyliśmy funkcji async, ponieważ będzie działać w tle, wewnątrz funkcji async użyliśmy słowa kluczowego await przed każdą metodą, która zwraca lub jest obietnicą, ponieważ czekamy na tę pozycję, aż obietnica zostanie spełniona, innymi słowy w poniżej kody, dopóki getIds nie zostaną rozwiązane lub odrzucone, program przestanie wykonywać kody poniżej tej linii, gdy zwrócone zostaną identyfikatory, a następnie ponownie wywołaliśmy funkcję getRecipe () z identyfikatorem i czekaliśmy za pomocą słowa kluczowego await, aż zwrócone zostaną dane. Więc tak w końcu doszliśmy do siebie z piekła oddzwaniania.

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

Aby użyć await, będziemy potrzebować funkcji asynchronicznej, możemy zwrócić obietnicę, więc użyj jej do rozwiązania obietnicy i odpowiedzi na obietnicę odrzucenia

z powyższego przykładu:

 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

Jedynym sposobem uniknięcia piekła zwrotnego jest użycie FRP, który jest „ulepszoną wersją” RX.

Niedawno zacząłem używać FRP, ponieważ znalazłem jego dobrą implementację o nazwie Sodium( http://sodium.nz/ ).

Typowy kod wygląda następująco (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()jest a, Streamktóry jest uruchamiany, jeśli selectedNode(co jest Cell) zmianą, NodeEditorWidgetto odpowiednio aktualizuje się.

Czyli w zależności od treści selectedNode Cellaktualnie edytowana Notezmieni się.

Ten kod całkowicie unika wywołań zwrotnych, prawie, Cacllback-s są wypychane do "zewnętrznej warstwy" / "powierzchni" aplikacji, gdzie logika obsługująca stan łączy się ze światem zewnętrznym. Nie ma wywołań zwrotnych potrzebnych do propagowania danych w ramach wewnętrznej logiki obsługi stanu (która implementuje maszynę stanu).

Pełny kod źródłowy jest tutaj

Fragment kodu powyżej odpowiada poniższemu prostemu przykładowi tworzenia / wyświetlania / aktualizowania:

wprowadź opis obrazu tutaj

Ten kod wysyła również aktualizacje do serwera, więc zmiany w zaktualizowanych jednostkach są automatycznie zapisywane na serwerze.

Cała obsługa zdarzeń jest obsługiwana za pomocą Streams i Cells. To są koncepcje FRP. Wywołania zwrotne są potrzebne tylko wtedy, gdy logika FRP łączy się ze światem zewnętrznym, na przykład wprowadzanie danych przez użytkownika, edycja tekstu, naciśnięcie przycisku, powrót połączenia AJAX.

Przepływ danych jest wyraźnie opisany, w sposób deklaratywny przy użyciu FRP (implementowanego przez bibliotekę Sodium), więc do opisu przepływu danych nie jest potrzebna logika obsługi zdarzeń / wywołań zwrotnych.

FRP (który jest bardziej „ścisłą” wersją RX) to sposób na opisanie wykresu przepływu danych, który może zawierać węzły zawierające stan. Zdarzenia wyzwalają zmiany stanu w stanie zawierającym węzły (zwanym Cells).

Sód jest biblioteką FRP wyższego rzędu, co oznacza, że ​​używając flatMap/ switchprymitywnego można zmienić kolejność wykresu przepływu danych w czasie wykonywania.

Polecam zajrzeć do książki Sodium , która szczegółowo wyjaśnia, w jaki sposób FRP pozbywa się wszystkich wywołań zwrotnych, które nie są niezbędne do opisu logiki przepływu danych, która ma związek z aktualizacją stanu aplikacji w odpowiedzi na pewne zewnętrzne bodźce.

Korzystając z FRP, należy zachować tylko te wywołania zwrotne, które opisują interakcję ze światem zewnętrznym. Innymi słowy, przepływ danych jest opisywany w sposób funkcjonalny / deklaratywny, gdy używa się struktury FRP (takiej jak Sodium) lub gdy używa się struktury „podobnej do FRP” (takiej jak RX).

Sód jest również dostępny dla Javascript / Typescript.


-3

Jeśli nie masz wiedzy na temat oddzwaniania do piekła i oddzwaniania do piekła, nie ma problemu. Pierwszą rzeczą jest to, że oddzwoń i oddzwoń do piekła. o tym zagnieżdżone w języku C, C ++ Zagnieżdżone Oznacza, że ​​klasa znajduje się w innej klasie.


Odpowiedź będzie bardziej pomocna, jeśli zawiera fragment kodu pokazujący, co to jest „piekło oddzwonienia” i ten sam fragment kodu z Rx po usunięciu „piekła zwrotnego”
rafa

-4

Użyj https://github.com/Javanile/Jazz.js jazz.js

to upraszcza w ten sposób:

    // uruchom zadanie sekwencyjne w łańcuchu
    jj.script ([
        // pierwsze zadanie
        function (next) {
            // na końcu tego procesu 'next' wskaż drugie zadanie i uruchom je 
            callAsyncProcess1 (następny);
        },
      // drugie zadanie
      function (next) {
        // na końcu tego procesu 'next' wskaż trzydzieste zadanie i uruchom je 
        callAsyncProcess2 (dalej);
      },
      // trzydzieste zadanie
      function (next) {
        // na końcu tego procesu 'następny' wskaż (jeśli masz) 
        callAsyncProcess3 (dalej);
      },
    ]);


rozważ ultrakompaktowy, taki jak ten github.com/Javanile/Jazz.js/wiki/Script-showcase
cicciodarkast
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.