Передайте массив Deferreds в $ .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()методу deferred, потому что jQuery вызывает обратные вызовы done()и fail()с отдельными параметрами, а не массивом. Это означает, что мы должны использовать argumentsпсевдомассив, чтобы получить все разрешенные / отклоненные объекты, возвращаемые массивом deferreds, что ужасно:

$.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
Возможно, вы захотите использовать resolWith и rejectWith только для того, чтобы получить те же исходные отложенные значения, что и для «this» deferred.resolveWith (this, [Array.prototype.slice.call (arguments)]) и т. Д.
Jamie Pate,

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?


Я действительно видел этот вопрос, но я предполагаю, что все дополнительные детали в этом вопросе заставили ответ на мою проблему (которая была прямо там) пролететь прямо над моей головой.
Адамджфорд

1
@adamjford, если тебе от этого станет легче, я понял, что твой вопрос легче усвоить (и сначала в моем конкретном поиске в Google по этому вопросу).
Патрон

@patridge: Рад слышать, что это помогло вам!
Адамджфорд

Это отличный ответ, но мне было непонятно, как это применимо к примеру из исходного вопроса. После изучения связанного вопроса выяснилось, что строку «$ .when (deferreds) .done (function () {» следует просто заменить на «$ .when.apply ($, deferreds) .done (function () {»). ". Верно?
Гирлянда Папа Римский

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
понижение было для 1. даже предлагая синхронизацию (хотя и с рекомендацией не делать) 2. код низкого качества в примерах (в том числе for ... inдля массива ?!)
Alnitak

1
1. Согласен, должен был (not recommended)2. Не согласен - for ... inвсе в порядке, потому что массив содержит только те свойства, которые нужны (без дополнительных свойств). спасибо в любом случае
vinayakj

1
re: 2 - проблема в том, что это может быть скопировано другими людьми, которые не могут дать такую ​​гарантию, или были настолько глупы, чтобы добавить к ним Array.prototype. В любом случае, для не критичного к производительности кода было бы лучше использовать .mapвместо for/ pushloop, например 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: это предназначено для того, чтобы предоставить более простую альтернативу кодированию, а не лучшую производительность (что пренебрежимо мало для большинства асинхронных кодировок), как это делается then()аналогичным образом для цепочки вызовов. Поведение с $.whenдолжно действовать как оно параллельно (не сковано). Пожалуйста, попробуйте, прежде чем выбросить полезную альтернативу, так как она работает :)
Gone Coding

2
@Alnitak: Лошади для курсов. Вы, безусловно, имеете право на мнение, но вы, очевидно, не использовали это сами. Мое собственное мнение основано на практическом использовании этой техники. Он работает и использует, поэтому зачем выбрасывать инструмент из набора инструментов, основываясь на преувеличениях, таких как «грузы предостережений» (один) и «ничего не решает» (не соответствует действительности - это устраняет обработку массива и упрощает создание цепочки параллельных обещаний, когда возвращаются значения не нужны, которые, как вы должны знать, редко используются в случаях параллельной обработки в любом случае). Предполагается, что понижение голосов за «этот ответ бесполезен» :)
Gone Coding

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 Link - Синтаксис распространения


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, и нет ничего плохого в том, чтобы показать разные реализации. Я зашел на эту страницу в поисках ответа для angular, и я подозреваю, что многие другие пользователи дойдут до этой страницы и не обязательно будут в среде только jquery.
mastaBlasta

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

0

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

Таким образом, проблема заключалась в том, что я пытался подсчитать сумму по всем числам, но данные от асинхронных вызовов 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.