Передайте массив отложенных в $.when()

Вот надуманный пример того, что происходит: 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) но неизвестно, сколько отложенных объектов будет при выполнении в реальной проблеме, которую я пытаюсь решить.

9 ответов

Решение

Чтобы передать массив значений любой функции, которая обычно ожидает, что они будут отдельными параметрами, используйте Function.prototype.applyтак что в этом случае вам нужно:

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

Смотрите http://jsfiddle.net/YNGcm/21/

В ES6 вы можете использовать ... вместо этого оператор распространения:

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

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

Обходные пути выше (спасибо!) Не решают проблему возврата объектов, предоставленных отложенному resolve() метод, потому что JQuery вызывает done() а также fail() обратные вызовы с отдельными параметрами, а не массив. Это означает, что мы должны использовать arguments псевдомассив для получения всех разрешенных / отклоненных объектов, возвращаемых массивом deferreds, что ужасно:

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

Поскольку мы передали массив отсрочек, было бы неплохо вернуть массив результатов. Также было бы неплохо получить реальный массив вместо псевдомассива, чтобы мы могли использовать такие методы, как Array.sort(),

Вот решение, вдохновленное когда .jswhen.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);
});

Вы можете применить when метод для вашего массива:

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

$.when.apply($, arr);

Как вы работаете с массивом jQuery Deferreds?

При вызове нескольких параллельных вызовов 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>

Как простая альтернатива, которая не требует $.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)
  • Недостатком является то, что он создает дополнительные объекты обещания за кулисами, и любые параметры, передаваемые в конце, не очень полезны (так как они вложены в дополнительные объекты). За то, что вы хотите, хотя это коротко и просто.
  • Плюс в том, что он не требует массива или управления массивами.

Я хочу предложить другой с использованием $.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!");
        }
    )
    

Если у вас есть доступ к ES6, вы можете использовать распространенный синтаксис, который конкретно применяет каждый повторяемый элемент объекта в качестве отдельного аргумента. $.when() нужно это

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

MDN Link - Синтаксис распространения

У меня был очень похожий случай, когда я публиковал в каждом цикле, а затем устанавливал 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;
    }

Если вы используете 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

https://docs.angularjs.org/api/ng/service/%24q

Другие вопросы по тегам