Przekaż tablicę Deferreds do $ .when ()

447

Oto wymyślony przykład tego, co się dzieje: http://jsfiddle.net/adamjford/YNGcm/20/

HTML:

<a href="#">Click me!</a>
<div></div>

JavaScript:

function getSomeDeferredStuff() {
    var deferreds = [];

    var i = 1;
    for (i = 1; i <= 10; i++) {
        var count = i;

        deferreds.push(
        $.post('/echo/html/', {
            html: "<p>Task #" + count + " complete.",
            delay: count
        }).success(function(data) {
            $("div").append(data);
        }));
    }

    return deferreds;
}

$(function() {
    $("a").click(function() {
        var deferreds = getSomeDeferredStuff();

        $.when(deferreds).done(function() {
            $("div").append("<p>All done!</p>");
        });
    });
});

Chcę „Wszystko gotowe!” pojawiać się po zakończeniu wszystkich odroczonych zadań, ale $.when()wydaje się, że nie wie, jak obsługiwać tablicę Odroczonych obiektów. „Wszystko gotowe!” dzieje się najpierw, ponieważ tablica nie jest obiektem odroczonym, więc jQuery idzie do przodu i zakłada, że ​​właśnie zostało zrobione.

Wiem, że można przekazać obiekty do funkcji jak, $.when(deferred1, deferred2, ..., deferredX)ale nie wiadomo, ile obiektów Odroczonych będzie w trakcie wykonywania w rzeczywistym problemie, który próbuję rozwiązać.

adamjford
źródło
Dodano nową, prostszą odpowiedź na to bardzo stare pytanie poniżej. Zdajesz nie trzeba używać tablicę lub $.when.applyw ogóle, aby uzyskać ten sam rezultat.
Gone Coding
wycofałem temat pytania, ponieważ był zbyt szczegółowy (to nie jest tylko problem AJAX)
Alnitak

Odpowiedzi:

732

Aby przekazać tablicę wartości do dowolnej funkcji, która normalnie oczekuje, że będą to osobne parametry, użyj Function.prototype.apply, więc w tym przypadku potrzebujesz:

$.when.apply($, my_array).then( ___ );

Zobacz http://jsfiddle.net/YNGcm/21/

W ES6 możesz zamiast tego użyć ... operatora rozkładania :

$.when(...my_array).then( ___ );

W obu przypadkach, ponieważ jest mało prawdopodobne, abyś wiedział z góry, ile parametrów formalnych .thenbędzie wymagał moduł obsługi, ten moduł obsługi musiałby przetworzyć argumentstablicę, aby pobrać wynik każdej obietnicy.

Alnitak
źródło
4
Działa to świetnie. :) Jestem zdumiony, że nie byłem w stanie pogłębić tak prostej zmiany przez Google!
adamjford
9
to dlatego, że jest to metoda ogólna, nie specyficzne $.when- f.apply(ctx, my_array)będzie nazywać fsię this == ctxi argumenty przedstawione do treści o my_array.
Alnitak,
4
@Alnitak: Jestem trochę zawstydzony, że nie wiedziałem o tej metodzie, biorąc pod uwagę, jak długo piszę teraz JavaScript!
adamjford
5
FWIW, link w odpowiedzi Eli na pytanie Earlera z dyskusją o przejściu $vs nulljako pierwszym parametrze jest wart przeczytania. W tym konkretnym przypadku nie ma to jednak znaczenia.
Alnitak
4
@Alnitak: Tak, ale $jest mniej typowy niż nulli jesteś bezpieczny, gdy $.whenzmiany implementacji (nie to, że jest to prawdopodobne w tym przypadku, ale dlaczego nie pozostawić thisdomyślnie niezmienionego).
Tomasz Zieliński,
109

Powyższe obejścia (dzięki!) Nie rozwiązują poprawnie problemu odzyskania obiektów dostarczonych do metody odroczonej, resolve()ponieważ jQuery wywołuje done()i fail()wywołania zwrotne z indywidualnymi parametrami, a nie tablicą. Oznacza to, że musimy użyć argumentspseudo-tablicy, aby wszystkie rozstrzygnięte / odrzucone obiekty zostały zwrócone przez tablicę odroczonych, co jest brzydkie:

$.when.apply($,deferreds).then(function() {
     var objects=arguments; // The array of resolved objects as a pseudo-array
     ...
};

Ponieważ przeszliśmy przez szereg odroczonych, byłoby miło odzyskać tablicę wyników. Byłoby również miło odzyskać rzeczywistą tablicę zamiast pseudo-tablicy, abyśmy mogli użyć metod takich jak Array.sort().

Oto rozwiązanie zainspirowane metodą when.js , when.all()która rozwiązuje te problemy:

// Put somewhere in your scripting environment
if (typeof jQuery.when.all === 'undefined') {
    jQuery.when.all = function (deferreds) {
        return $.Deferred(function (def) {
            $.when.apply(jQuery, deferreds).then(
                function () {
                    def.resolveWith(this, [Array.prototype.slice.call(arguments)]);
                },
                function () {
                    def.rejectWith(this, [Array.prototype.slice.call(arguments)]);
                });
        });
    }
}

Teraz możesz po prostu przekazać tablicę odroczonych / obietnic i odzyskać tablicę rozstrzygniętych / odrzuconych obiektów w swoim wywołaniu zwrotnym:

$.when.all(deferreds).then(function(objects) {
    console.log("Resolved objects:", objects);
});
crispyduck
źródło
6
Możesz użyć resolWith i odrzucać z tak, aby uzyskać te same oryginalne odroczenia, co „this” deferred.resolveWith (this, [Array.prototype.slice.call (arguments)]) itp.
Jamie Pate
1
Z twoim kodem jest tylko mały problem, gdy w tablicy jest tylko jeden element, tablica wyników zwraca właśnie ten wynik, zamiast tablicy z pojedynczym elementem (który zepsuje kod, który oczekuje tablicy). Aby to naprawić, użyj tej funkcji var toArray = function (args) { return deferreds.length > 1 ? $.makeArray(args) : [args]; }zamiast Array.prototype.slice.call.
Luan Nico,
Hm, wydaje się, że nie łapie żadnych 404.
t.mikael.d
Znalazłem powód, zamiast tego .fail powinien być .reject - aby mógł złapać 404.
t.mikael.d
38

Możesz zastosować whenmetodę do swojej tablicy:

var arr = [ /* Deferred objects */ ];

$.when.apply($, arr);

Jak pracujesz z tablicą jQuery Deferreds?

Eli
źródło
Właściwie widziałem to pytanie, ale wydaje mi się, że wszystkie dodatkowe szczegóły w tym pytaniu spowodowały, że odpowiedź na mój problem (który tam był) przeleciała mi nad głową.
adamjford
1
@adamjford, jeśli dzięki temu poczujesz się lepiej, znalazłem twoje pytanie łatwiejsze do skonsumowania (i po raz pierwszy podczas wyszukiwania w Google tego konkretnego problemu).
patridge
@patridge: Cieszę się, że to pomogło!
adamjford,
To świetna odpowiedź, ale nie było dla mnie jasne, jak to się odnosi do przykładu z pierwotnego pytania. Po zapoznaniu się z połączonym pytaniem stało się jasne, że wiersz „$ .when (deferreds) .done (function () {” należy po prostu zmienić na „$ .when.apply ($, deferreds) .done (function () { ". Prawda?
Garland Papież
7

Podczas wywoływania wielu równoległych wywołań AJAX masz dwie opcje obsługi odpowiednich odpowiedzi.

  1. Użyj synchronicznego połączenia AJAX / jeden po drugim / niezalecane
  2. Użyj Promises'tablicy i $.whenakceptuje promises, a jej wywołanie zwrotne .donejest wywoływane, gdy wszystkie promises są zwracane z odpowiednimi odpowiedziami.

Przykład

function ajaxRequest(capitalCity) {
   return $.ajax({
        url: 'https://restcountries.eu/rest/v1/capital/'+capitalCity,
        success: function(response) {
        },
        error: function(response) {
          console.log("Error")
        }
    });
}
$(function(){
   var capitalCities = ['Delhi', 'Beijing', 'Washington', 'Tokyo', 'London'];
   $('#capitals').text(capitalCities);

   function getCountryCapitals(){ //do multiple parallel ajax requests
      var promises = [];   
      for(var i=0,l=capitalCities.length; i<l; i++){
            var promise = ajaxRequest(capitalCities[i]);
            promises.push(promise);
      }
  
      $.when.apply($, promises)
        .done(fillCountryCapitals);
   }
  
   function fillCountryCapitals(){
        var countries = [];
        var responses = arguments;
        for(i in responses){
            console.dir(responses[i]);
            countries.push(responses[i][0][0].nativeName)
        }  
        $('#countries').text(countries);
   }
  
   getCountryCapitals()
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
  <h4>Capital Cities : </h4> <span id="capitals"></span>
  <h4>Respective Country's Native Names : </h4> <span id="countries"></span>
</div>

vinayakj
źródło
1
Twoja odpowiedź sięga zbyt daleko, podobnie jak Twoja edycja tytułu pytania. OP już wiedział, jak wykonać wywołania AJAX i uzyskać tablicę odroczonych obiektów. Jedynym punktem pytaniem było, jak przekazać tę tablicę do $.when.
Alnitak,
5
Pomyślałem, że wyjaśnienie na przykładach byłoby lepsze, z dostępnymi opcjami. I do tego nie uważam, by głosowanie w dół było konieczne.
vinayakj
2
głosowanie było za 1. sugerowaniem nawet synchronizacji (choć z zaleceniem, aby tego nie robić) 2. kodem niskiej jakości w przykładach (w tym for ... inna tablicy ?!)
Alnitak
1
1. Uzgodniony, powinien był mieć (not recommended)2. Nie zgadzam się - for ... injest w porządku, ponieważ tablica zawiera tylko te właściwości, które potrzebują (bez dodatkowych właściwości). i tak dzięki
vinayakj
1
re: 2 - problem polega na tym, że mogą zostać skopiowane przez inne osoby, które nie mogą dać takiej gwarancji lub są na tyle głupie, by je dodać Array.prototype. W każdym razie dla kodu niekrytycznego pod względem wydajności lepiej byłoby użyć .mapzamiast pętli for/ push, np. var promises = capitalCities.map(ajaxRequest); $.when.apply($, promises).then(fillCountryCapitals)- zadanie wykonane.
Alnitak,
6

Jako prostą alternatywę, która nie wymaga $.when.applyani array, możesz użyć następującego wzorca do wygenerowania pojedynczej obietnicy dla wielu równoległych obietnic:

promise = $.when(promise, anotherPromise);

na przykład

function GetSomeDeferredStuff() {
    // Start with an empty resolved promise (or undefined does the same!)
    var promise;
    var i = 1;
    for (i = 1; i <= 5; i++) {
        var count = i;

        promise = $.when(promise,
        $.ajax({
            type: "POST",
            url: '/echo/html/',
            data: {
                html: "<p>Task #" + count + " complete.",
                delay: count / 2
            },
            success: function (data) {
                $("div").append(data);
            }
        }));
    }
    return promise;
}

$(function () {
    $("a").click(function () {
        var promise = GetSomeDeferredStuff();
        promise.then(function () {
            $("div").append("<p>All done!</p>");
        });
    });
});

Uwagi:

  • Zrozumiałem to po tym, jak zobaczyłem czyjeś obietnice po kolei, używając promise = promise.then(newpromise)
  • Minusem jest to, że tworzy dodatkowe obietnice za kulisami, a wszelkie parametry przekazywane na końcu nie są bardzo przydatne (ponieważ są zagnieżdżone w dodatkowych obiektach). To, czego chcesz, jest jednak krótkie i proste.
  • Plusem jest to, że nie wymaga tablicy ani zarządzania tablicą.
Gone Coding
źródło
2
Popraw mnie, jeśli się mylę, ale twoje podejście efektywnie zagnieżdża $ .when ($ .when ($ .when (...))), więc rekurencyjnie zagnieżdżasz 10 poziomów głęboko, jeśli jest 10 iteracji. Nie wydaje się to bardzo równoległe, ponieważ musisz poczekać, aż każdy poziom zwróci zagnieżdżoną obietnicę dziecka, zanim będzie mogła zwrócić własną obietnicę - myślę, że podejście tablicowe w przyjętej odpowiedzi jest znacznie bardziej przejrzyste, ponieważ wykorzystuje zachowanie elastycznych parametrów metoda $ .when ().
Anthony McLin
@AnthonyMcLin: ma to na celu zapewnienie prostszej alternatywy dla kodowania, a nie lepszej wydajności (co jest nieznaczne w przypadku większości kodowań Async), podobnie jak w przypadku then()połączeń łańcuchowych w podobny sposób. Zachowanie $.whenpolega na zachowaniu się tak, jakby było równoległe (nie powiązane). Spróbuj, zanim wyrzucisz przydatną alternatywę, ponieważ działa :)
Gone Coding
2
@Alnitak: Konie na kursy. Z pewnością masz prawo do opinii, ale oczywiście sam jej nie wykorzystałeś. Moja własna opinia oparta jest na praktycznych zastosowaniach tej techniki. To działa i ma zastosowanie, więc dlaczego wyrzucić narzędzia z przybornika w oparciu o exaggerations jak „mnóstwo zastrzeżeń” (jeden) i „niczego nie rozwiązuje” (nieprawda - eliminuje przetwarzanie tablicy i upraszcza łańcuchowym równoległych obietnic gdzie powrotną wartości nie są potrzebne, które, jak powinieneś wiedzieć, rzadko są używane w przypadkach przetwarzania równoległego). Przegłosowane powinny być za „ta odpowiedź nie jest przydatna” :)
Gone Coding
1
Cześć @GoneCoding. Czy mogę prosić, abyście nie dodawali komentarza do swoich odpowiedzi? To nadaje się do komentowania, ale poza tym hałas odwraca uwagę od dobrych treści. Dzięki.
halfer
1
@halfer: Już nie publikuję, ale denerwuje mnie ignorancja przejawiająca się w czymkolwiek oryginalnym. Trzymam teraz wszystkie nowe pomysły dla siebie :)
Gone Coding
4

Chcę zaproponować inny przy użyciu $ .each:

  1. Możemy zadeklarować funkcję ajax w następujący sposób:

    function ajaxFn(someData) {
        this.someData = someData;
        var that = this;
        return function () {
            var promise = $.Deferred();
            $.ajax({
                method: "POST",
                url: "url",
                data: that.someData,
                success: function(data) {
                    promise.resolve(data);
                },
                error: function(data) {
                    promise.reject(data);
                }
            })
            return promise;
        }
    }
  2. Część kodu, w której tworzymy tablicę funkcji za pomocą ajax do wysyłania:

    var arrayOfFn = [];
    for (var i = 0; i < someDataArray.length; i++) {
        var ajaxFnForArray = new ajaxFn(someDataArray[i]);
        arrayOfFn.push(ajaxFnForArray);
    }
  3. I wywoływanie funkcji z wysyłaniem ajax:

    $.when(
        $.each(arrayOfFn, function(index, value) {
            value.call()
        })
    ).then(function() {
            alert("Cheer!");
        }
    )
Włodzimierz Jasiński
źródło
1

Jeśli dokonujesz transpozycji i masz dostęp do ES6, możesz użyć składni spreadu, która konkretnie stosuje każdy iterowalny element obiektu jako dyskretny argument, dokładnie tak, jak tego $.when()potrzebuje.

$.when(...deferreds).done(() => {
    // do stuff
});

MDN Link - Składnia spreadu

relikt
źródło
0

Jeśli używasz angularJS lub jakiegoś wariantu biblioteki obietnic Q, masz .all()metodę, która rozwiązuje dokładnie ten problem.

var savePromises = [];
angular.forEach(models, function(model){
  savePromises.push(
    model.saveToServer()
  )
});

$q.all(savePromises).then(
  function success(results){...},
  function failed(results){...}
);

zobacz pełne API:

https://github.com/kriskowal/q/wiki/API-Reference#promiseall

https://docs.angularjs.org/api/ng/service/$q

mastaBlasta
źródło
4
Jest to całkowicie nieistotne.
Benjamin Gruenbaum,
@BenjaminGruenbaum Jak to zrobić? Wszystkie biblioteki obietnic javascript mają podobny interfejs API i nie ma nic złego w wyświetlaniu różnych implementacji. Dotarłem do tej strony, szukając odpowiedzi na angular i podejrzewam, że wielu innych użytkowników wejdzie na tę stronę i niekoniecznie będzie w środowisku tylko jquery.
mastaBlasta
2
Mianowicie, ponieważ obietnice jQuery nie współużytkują tego API, jest to całkowicie niewłaściwe jako odpowiedź na przepełnienie stosu - istnieją podobne odpowiedzi dla Angulara i możesz o to zapytać. (Nie wspominając, powinieneś .maptutaj, ale no cóż).
Benjamin Gruenbaum
0

Miałem bardzo podobny przypadek, w którym publikowałem w każdej pętli, a następnie ustawiałem znaczniki HTML w niektórych polach na podstawie liczb otrzymanych z ajax. Następnie musiałem wykonać sumę (teraz zaktualizowanych) wartości tych pól i umieścić je w polu sumarycznym.

Problem polegał na tym, że próbowałem zsumować wszystkie liczby, ale nie otrzymałem jeszcze danych z wywołań asynchronicznych wywołań ajax. Musiałem ukończyć tę funkcjonalność w kilku funkcjach, aby móc ponownie użyć kodu. Moja zewnętrzna funkcja czeka na dane, zanim pójdę i zrobię coś z całkowicie zaktualizowanym DOM.

    // 1st
    function Outer() {
        var deferreds = GetAllData();

        $.when.apply($, deferreds).done(function () {
            // now you can do whatever you want with the updated page
        });
    }

    // 2nd
    function GetAllData() {
        var deferreds = [];
        $('.calculatedField').each(function (data) {
            deferreds.push(GetIndividualData($(this)));
        });
        return deferreds;
    }

    // 3rd
    function GetIndividualData(item) {
        var def = new $.Deferred();
        $.post('@Url.Action("GetData")', function (data) {
            item.html(data.valueFromAjax);
            def.resolve(data);
        });
        return def;
    }
Cameron Forward
źródło