Передати масив відкладених до $ .when ()


447

Ось надуманий приклад того, що відбувається: 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>");
        });
    });
});

Я хочу "Все готово!" З'являється після того, як усі відкладені завдання виконані, але, $.when()здається, не знають, як обробляти масив відкладених об'єктів. "Все зроблено!" відбувається в першу чергу, тому що масив не є відкладеним об'єктом, тому jQuery йде вперед і передбачає, що це просто зроблено.

Я знаю, що хтось може передати об'єкти у функцію на кшталт, $.when(deferred1, deferred2, ..., deferredX)але невідомо, скільки буде відкладених об'єктів при виконанні фактичної проблеми, яку я намагаюся вирішити.



Нижче додано нову, простішу відповідь на це дуже старе питання. Вам не потрібно використовувати масив або $.when.applyвзагалі отримати такий же результат.
Пройшов кодування

відкинув тему питання, оскільки це було занадто конкретним (це не лише проблема AJAX)
Alnitak

Відповіді:


732

Щоб передати масив значень будь-якій функції, яка зазвичай очікує їх окремості параметрів, використовуйте Function.prototype.apply, тому в цьому випадку вам потрібно:

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

Дивіться http://jsfiddle.net/YNGcm/21/

У ES6 ви можете використовувати ... оператор розповсюдження замість цього:

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

У будь-якому випадку, оскільки навряд чи ви будете заздалегідь знати, скільки формальних параметрів .thenвимагатиме обробник, цьому оброблювачеві потрібно буде обробити argumentsмасив, щоб отримати результат кожної обіцянки.


4
Це працює, приголомшливо. :) Я вражений, що не зміг заглибити таку просту зміну через Google!
adamjford

9
це тому , що це універсальний метод, який не належить до $.when- f.apply(ctx, my_array)буде називати fз this == ctxі аргументами , встановлених на утримання в my_array.
Альнітак

4
@Alnitak: Мене трохи бентежить, що я не знав про цей метод, враховуючи, як довго я зараз пишу JavaScript!
adamjford

5
FWIW, посилання у відповіді Елі на запитання продавця з обговоренням проходження $vs nullяк першого параметра варто прочитати. У цьому конкретному випадку це не має значення.
Альнітак

4
@Alnitak: Так, але типізуєш $менше, ніж nullти, і ти безпечний, коли $.whenзміни змінюються (не те, що це, мабуть, у цьому випадку, але чому б не залишилося thisнезмінним за замовчуванням).
Томаш Зелінський

109

Вирішені способи вирішення проблем (спасибі!) Не належним чином вирішують проблему повернення об'єктів, наданих resolve()методу відкладеного, тому що jQuery викликає зворотні done()та fail()виклики з індивідуальними параметрами, а не масив. Це означає, що ми повинні використовувати argumentsпсевдомасив, щоб отримати всі вирішені / відхилені об’єкти, повернуті масивом відкладених, що некрасиво:

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

Оскільки ми пройшли масив відкладених, було б непогано повернути масив результатів. Було б також непогано повернути фактичний масив замість псевдомасиву, щоб ми могли використовувати такі методи Array.sort().

Ось рішення, натхнене методом When.js , when.all()який вирішує ці проблеми:

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

Тепер ви можете просто передати масив відкладених / обіцянок і отримати масив вирішених / відхилених об'єктів у зворотному дзвінку, наприклад:

$.when.all(deferreds).then(function(objects) {
    console.log("Resolved objects:", objects);
});

6
Можливо, ви хочете використовувати resolutionWith та rejectWith лише для того, щоб ви отримали ті самі оригінальні відкладені, що й "this" deferred.resolveWith (це, [Array.prototype.slice.call (аргументи)]) тощо "
Джеймі Пат

1
Існує лише невелика проблема з вашим кодом, коли в масиві є лише один елемент, масив результатів повертає саме такий результат, а не масив з одним елементом (який порушить код, який очікує масив). Щоб виправити це, використовуйте цю функцію var toArray = function (args) { return deferreds.length > 1 ? $.makeArray(args) : [args]; }замість Array.prototype.slice.call.
Луан Ніко

Гм, схоже, це не наздогнало жодного 404-го.
t.mikael.d

Знайшов причину .fail повинен бути .reject замість цього - щоб він міг наздогнати 404.
t.mikael.d

38

Ви можете застосувати whenметод до свого масиву:

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

$.when.apply($, arr);

Як ви працюєте з масивом jQuery Deferreds?


Я насправді бачив це питання, але, мабуть, всі додаткові подробиці в цьому питанні спричинили, що відповідь на мою проблему (яка була там справа) пролетіла прямо над моєю головою.
adamjford

1
@adamjford, якщо ви відчуваєте себе краще, я знайшов ваше запитання легшим для споживання (і спочатку в моєму конкретному пошуку Google для цієї точної проблеми).
патрон

@patridge: Радий почути, що це допомогло тобі!
adamjford

Це чудова відповідь, але мені було незрозуміло, як це стосується прикладу в оригінальному запитанні. Після консультацій із пов’язаним питанням з’ясувалося, що рядок "$ .when (відкладені). ". Так?
Папа Гарленд

7

Під час виклику декількох паралельних дзвінків AJAX у вас є два варіанти обробки відповідних відповідей.

  1. Використовувати синхронний дзвінок AJAX / один за одним / не рекомендується
  2. Використовуйте Promises'масив, $.whenякий приймає promises, і його зворотний виклик .doneотримує виклик, коли всі promises успішно повертаються з відповідними відповідями.

Приклад

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>


1
Ваша відповідь відповідала вашим запитам, і так само ви змінили назву питання. ОП вже знала, як здійснювати дзвінки AJAX та отримувати масив відкладених об'єктів. Єдиною точкою питання було те, як передати цей масив $.when.
Альнітак

5
Я думав, що детальніше пояснити на прикладі, було б краще, з наявними варіантами. І для цього я не вважаю, що потрібне скорочення.
vinayakj

2
downvote був для 1. навіть пропонував синхронізацію (хоч і з рекомендацією не робити) 2. код низької якості у прикладах (у тому числі for ... inна масиві ?!)
Alnitak

1
1. Погоджено, мав би мати (not recommended)2.Не погоджуватися - for ... inце нормально, оскільки масив містить лише ті властивості, які потрібні (відсутність додаткових властивостей). ніж в будь-якому випадку
vinayakj

1
re: 2 - проблема полягає в тому, що це може бути скопійовано іншими людьми, які не можуть зробити цю гарантію, або були досить німі, щоб додати Array.prototype. У будь-якому випадку для коду, що не є критичним для продуктивності, краще використовувати .mapзамість for/ pushциклу, наприклад var promises = capitalCities.map(ajaxRequest); $.when.apply($, promises).then(fillCountryCapitals)- виконану роботу.
Альнітак

6

Як просту альтернативу, яка не потребує $.when.applyабо array, ви можете використовувати наступний зразок для створення єдиної обіцянки для декількох паралельних обіцянок:

promise = $.when(promise, anotherPromise);

напр

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

Примітки:

  • Я зрозумів це, побачивши, як хтось ланцюжкові обіцянки послідовно використовує promise = promise.then(newpromise)
  • Мінус полягає в тому, що він створює додаткові перспективні об'єкти за кадром, і будь-які параметри, передані в кінці, не дуже корисні (оскільки вони вкладені всередині додаткових об'єктів). Для того, що ви хочете, хоча це коротко і просто.
  • Зворотній бік полягає в тому, що він не потребує управління масивом або управлінням масивом.

2
Виправте мене, якщо я помиляюся, але ваш підхід ефективно вкладає $ .when ($ .when ($ .when (...))), тож ви в кінцевому підсумку рекурсивно вкладаєтеся на 10 рівнів глибиною, якщо є 10 ітерацій. Це здається не дуже паралельним, оскільки вам доведеться чекати, коли кожен рівень поверне дитині вкладені обіцянки, перш ніж він може повернути його власну обіцянку - я думаю, що підхід масиву у прийнятій відповіді набагато чіткіший, оскільки він використовує гнучку поведінку параметрів, вбудовану в метод $ .when ().
Ентоні Маклін

@AnthonyMcLin: це покликане забезпечити більш просту альтернативу кодуванню, не кращу продуктивність (що є незначним при більшості кодування Async), як це робиться з ланцюжком then()викликів аналогічним чином. Поведінка з $.when- це діяти так, як вона паралельна (не прикута). Будь ласка, спробуйте, перш ніж викинути корисну альтернативу, бо це працює :)
Пройшло кодування

2
@Alnitak: Коні на курси. Ви, безумовно, маєте право на думку, але ви, очевидно, самі цього не використовували. Моя власна думка базується на практичному використанні цієї методики. Він працює і використовує, тому навіщо викидати інструмент із панелі інструментів на основі перебільшень, таких як "навантаження застережень" (один) і "нічого не вирішує" (неправда - це виключає обробку масиву та спрощує ланцюжок паралельних обіцянок, де повернення значення не потрібні, які, як ви повинні знати, так чи інакше рідко використовуються у випадках паралельної обробки). Передбачувані коментарі повинні бути за "ця відповідь не корисна" :)
Пройшло кодування

1
Привіт @GoneCoding. Чи можу я попросити, щоб ви не додавали коментарів для голосування у відповіді? Це підходить для коментарів, але в іншому випадку саме шум відволікає від інакше хорошого вмісту. Дякую.
півзахисник

1
@halfer: Я більше не публікую публікації, але мене дратує невігластво, що відображається до чогось оригінального. Зберігаючи всі нові ідеї для себе сьогодні :)
Пройшло кодування

4

Я хочу запропонувати інший із використанням $ .each:

  1. Ми можемо оголосити функцію ajax на зразок:

    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. Частина коду, де ми створюємо масив функцій з ajax для надсилання:

    var arrayOfFn = [];
    for (var i = 0; i < someDataArray.length; i++) {
        var ajaxFnForArray = new ajaxFn(someDataArray[i]);
        arrayOfFn.push(ajaxFnForArray);
    }
  3. І виклик функцій з надсиланням ajax:

    $.when(
        $.each(arrayOfFn, function(index, value) {
            value.call()
        })
    ).then(function() {
            alert("Cheer!");
        }
    )

1

Якщо ви перекладаєте та маєте доступ до ES6, ви можете використовувати синтаксис розширення, який спеціально застосовує кожен ітерабельний елемент об’єкта як дискретний аргумент, саме так, як $.when()це потрібно.

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

Посилання MDN - розповсюдження синтаксису


0

Якщо ви використовуєте angularJS або якийсь варіант бібліотеки обіцянок Q, тоді у вас є .all()метод, який вирішує цю точну проблему.

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

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

переглянути повний API:

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

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


4
Це абсолютно не має значення.
Бенджамін Грюенбаум

@BenjaminGruenbaum Як так? Усі бібліотеки з обіцянками JavaScript мають подібний API, і немає нічого поганого в показах різних реалізацій. Я зайшов на цю сторінку, шукаючи відповідь на кутовий, і я підозрюю, що багато інших користувачів дотягнуться до цієї сторінки, і не обов'язково опинятись лише в середовищі jquery.
mastaBlasta

2
А саме тому, що обіцянки jQuery не поділяють цей API, це абсолютно недоречно як відповідь на стек переповнення - є аналогічні відповіді для Angular, і ви можете запитати їх. (Не кажучи вже про те, ви повинні .mapтут, але добре).
Бенджамін Грюнбаум

0

У мене був дуже подібний випадок, коли я розміщував повідомлення в кожному циклі, а потім встановлював розмітку html в деяких полях з номерів, отриманих від ajax. Потім мені потрібно було зробити суму (тепер оновлених) значень цих полів і розмістити в загальному полі.

Таким чином, проблема полягала в тому, що я намагався отримати суму за всіма номерами, але жодних даних ще не надійшло за виклики асинхронного програмного забезпечення. Мені потрібно було виконати цю функціональність у кількох функціях, щоб мати можливість повторно використовувати код. Моя зовнішня функція очікує на дані, перш ніж я перейду і виконувати деякі речі з повністю оновленим 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;
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.