Как использовать объекты Deferred в цикле for

Я не знаю, как спросить об этом.

Я хочу знать размер трех изображений, из которых у меня есть только URL (само собой разумеется, это просто пример).

Что-то подсказывает мне, что Deferred - это способ сделать это... Я использовал его в других ситуациях, но я не знаю, как связать вместе различные объекты deferred, по одному для каждой итерации цикла for, и иметь один done функция для всех из них (имеет ли это смысл? Я не знаю, ясно ли я выражаюсь).

Это в основном то, что я пытаюсь сделать с помощью этого кода, но, естественно, здесь deferred будет вызывать done только один раз, а не для каждого изображения. Я хотел бы, чтобы Deferred разрешился только после того, как у меня будут размеры всех изображений.

обновить

Теперь я понимаю, благодаря jgreenberg и Берги, что мне нужен объект Deferred для каждого изображения, а также массив Promises. Новый код:

var imagesURLArray = [
  'http://placekitten.com/200/300',
  'http://placekitten.com/100/100',
  'http://placekitten.com/400/200'];

var promises = [];

function getImageDimensions(url){
  var deferred = new $.Deferred();
  var img = $('<img src="'+ imagesURLArray[i] +'"/>').load(function(){
    deferred.resolve( {width: this.width, height: this.height} );
  });
  return deferred.promise();
}

for (var i = 0; i < imagesURLArray.length; i++) {
  promises.push(getImageDimensions(imagesURLArray[i]));
}

$.when.apply($, promises).then(function(dimensions){
   console.log('dimensions: ' + dimensions);
});

Образец здесь

Однако я до сих пор не могу понять, как получить данные из всех объектов Deferred в then(). Аргумент dimensions возвращает только данные из первого объекта Deferred.


person zok    schedule 15.07.2014    source источник
comment
Посмотрите на jQuery .when().   -  person pmandell    schedule 15.07.2014
comment
jQuery load ненадежен для кросс-браузерных изображений   -  person charlietfl    schedule 15.07.2014
comment
Вы можете resolve использовать deferred только один раз, поэтому для каждого изображения вам потребуется отдельный deferred. Вы можете объединить их с $.when, если хотите.   -  person Bergi    schedule 16.07.2014
comment
@update: см. эту ветку о том, как получить доступ к результатам объединенных отсрочек. (Или используйте подходящую библиотеку Promise)   -  person Bergi    schedule 16.07.2014


Ответы (2)


Вот небольшой фрагмент кода, который расширяет встроенные обещания jQuery для работы с событиями .load() для изображений. Это позволяет вам выполнить очень простое кодирование, чтобы дождаться загрузки вашей группы изображений. Вот дополнительный код для jQuery для поддержки промисов для загрузки изображений:

(function() {
    // hook up a dummy animation that completes when the image finishes loading
    // If you call this before doing an animation, then animations will
    // wait for the image to load before starting
    // This does nothing for elements in the collection that are not images
    // It can be called multiple times with no additional side effects
    var waitingKey = "_waitingForLoad";
    var doneEvents = "load._waitForLoad error._waitForLoad abort._waitForLoad";
        jQuery.fn.waitForLoad = function() {
        return this.each(function() {
            var self = $(this);
            if (this.tagName && this.tagName.toUpperCase() === "IMG" && 
              !this.complete && !self.data(waitingKey)) {
                self.queue(function(next) {
                    // nothing to do here as this is just a sentinel that 
                    // triggers the start of the fx queue
                    // it will get called immediately and will put the queue "inprogress"
                }).on(doneEvents, function() {
                    // remove flag that we're waiting,
                    // remove event handlers
                    // and finish the pseudo animation
                    self.removeData(waitingKey).off(doneEvents).dequeue();
                }).data(waitingKey, true);
            }
        });
    };

    // replace existing promise function with one that
    // starts a pseudo-animation for any image that is in the process of loading
    // so the .promise() method will work for loading images
    var oldPromise = jQuery.fn.promise;
    jQuery.fn.promise = function() {
        this.waitForLoad();
        return oldPromise.apply(this, arguments);
    };
})();

Это работает путем переопределения текущего метода .promise(), и когда метод .promise() вызывается для каждого изображения в коллекции, загрузка которого еще не завершена, он запускает фиктивную анимацию в очереди jQuery «fx», и эта фиктивная анимация завершается, когда изображение «загружается». " событие срабатывает. Поскольку jQuery уже поддерживает обещания для анимации, после запуска фиктивной анимации для каждого изображения, которое еще не загружено, он может затем просто вызвать исходный метод .promise(), а jQuery выполнит всю работу по созданию обещания и отслеживанию завершения анимации. и разрешение обещания, когда все анимации будут сделаны. Я на самом деле удивлен, что jQuery не делает этого сама, потому что это такой небольшой дополнительный код, который использует многие вещи, которые они уже делают.

Вот тестовый jsFiddle для этого расширения: http://jsfiddle.net/jfriend00/bAD56/

Одна очень приятная особенность встроенного метода .promise() в jQuery заключается в том, что если вы вызовете его для набора объектов, он вернет вам главный промис, который разрешается только тогда, когда все отдельные промисы были разрешены, поэтому у вас нет делать всю эту уборку - она ​​сделает эту грязную работу за вас.


Это вопрос мнения, является ли переопределение .promise() хорошим способом или нет. Мне показалось хорошим просто добавить некоторые дополнительные функции к существующему методу .promise(), чтобы в дополнение к анимации он также позволял управлять событиями изображения load. Но, если этот выбор дизайна не является вашей чашкой чая, и вы предпочитаете оставить существующий метод .promise() как есть, тогда поведение обещания загрузки изображения может быть вместо этого помещено в новый метод .loadPromise(). Чтобы использовать его таким образом, вместо блока кода, который начинается с присвоения oldPromise = ..., вы должны заменить это:

jQuery.fn.loadPromise = function() {
    return this.waitForLoad().promise();
};

А чтобы получить событие обещания, включающее события изображения load, нужно просто вызвать obj.loadPromise() вместо obj.promise(). Остальной текст и примеры в этом посте предполагают, что вы используете переопределение .promise(). Если вы переключитесь на .loadPromise(), вам придется заменить его на оставшийся текст/демо.


Концепция в этом расширении также может быть использована для изображений в DOM, поскольку вы можете сделать что-то подобное, чтобы вы могли что-то сделать, как только набор изображений в DOM был загружен (без необходимости ждать, пока все изображения будут загружены ):

$(document).ready(function() {
    $(".thumbnail").promise().done(function() {
        // all .thumbnail images are now loaded
    });
});

Или, не связанный с исходным вопросом, но также что-то полезное, что вы можете сделать с этим расширением, вы можете дождаться загрузки каждого отдельного изображения, а затем запустить анимацию для каждого из них, как только оно загрузится:

$(document).ready(function() {
    $(".thumbnail").waitForLoad().fadeIn(2000);
});

Каждое изображение будет исчезать, начиная с момента его загрузки (или сразу, если оно уже загружено).


И вот как ваш код будет выглядеть с использованием вышеуказанного расширения:

var imagesURLArray = [
  'http://placekitten.com/200/300',
  'http://placekitten.com/100/100',
  'http://placekitten.com/400/200'];

// build a jQuery collection that has all your images in it
var images = $();
$.each(imagesURLArray, function(index, value) {
    images = images.add($("<img>").attr("src", value));
});
images.promise().done(function() {
    // all images are done loading now
    images.each(function() {
        console.log(this.height + ", " + this.width);
    });
});

Рабочая демонстрация jsFiddle: http://jsfiddle.net/jfriend00/9Pd32/


Или в современных браузерах (которые поддерживают .reduce() для массивов) вы можете использовать это:

imagesURLArray.reduce(function(collection, item) {
    return collection.add($("<img>").attr("src", item));
}, $()).promise().done(function() {
    // all images are done loading now
    images.each(function() {
        console.log(this.height + ", " + this.width);
    });
});;
person jfriend00    schedule 16.07.2014
comment
+1 за усилия, даже если я против перезаписи $.fn.promise - person Bergi; 16.07.2014
comment
@Bergi. Как я написал, можно было удалить замену .promise() и потребовать ручного вызова .waitForLoad() перед вызовом .promise(), как в .waitForLoad().promise(), но я стремился к более встроенному поведению, просто встроив его в .promise() и это недопустимо для любых объектов в коллекции, которые не являются изображениями в процессе загрузки. Просто любопытно, а почему вы против замены $.fn.promise? Он просто вызывает один метод, а затем вызывает и возвращает исходный метод дословно. - person jfriend00; 16.07.2014
comment
Имхо он просто не подходит. Я бы предпочел, чтобы .waitForLoad() возвращал обещание, добавляя .promise() к вызову .each(…) внутри него :-) - person Bergi; 16.07.2014
comment
@Bergi - .promise() работает с анимацией в jQuery и любыми другими действиями в очереди. Почему он не может также работать для загрузки изображений? Это описание jQuery таково: Return a Promise object to observe when all actions of a certain type bound to the collection, queued or not, have finished. Мне кажется, это вписывается в это. У него также есть очень хорошая функция: когда вы вызываете его для коллекции, он возвращает обещание, которое разрешается, когда все отдельные элементы коллекции разрешаются, что экономит $.when() ручное кодирование, и я тоже хотел воспользоваться этим. - person jfriend00; 16.07.2014
comment
@Bergi - я не хотел возвращать обещание от .waitForLoad(), потому что само по себе полезно запускать анимацию, как только изображение загружается, связывая его вот так $(imageSelector).waitForLoad().animate(...), даже не используя обещания. - person jfriend00; 16.07.2014
comment
Думаю, мне не нравится, что это не только наблюдение за действием, но и фактическая постановка задачи (даже если это уже происходит на самом деле). Если бы вы хотели перезаписать метод jQuery для регистрации собственных событий загрузки в очереди анимации (или лучше в собственной очереди?), я бы выбрал методы prop/attr - person Bergi; 16.07.2014
comment
@Bergi - это постановка задачи в очередь анимации, которая экономит массу кода. Без этого потребовалось бы дублировать множество функций, которые jQuery уже имеет для создания, хранения и выполнения промисов. Это пустая очередь анимации, которая автоматически запускает любое обещание, которое этот объект имеет в реальном времени. Я воспользовался этой функциональностью, а не дублировал ее. И я рассудил, что нет необходимости анимировать изображение, которое еще не загружено, поэтому, если эта анимация должна ждать (за моей задачей в очереди), это на самом деле полезная функция, а не проблема. - person jfriend00; 16.07.2014
comment
Я не против использования очереди (это полезный инструмент, когда вы понимаете, как он работает), мне просто не нравится, что вы используете .promise() для неявной постановки загрузки изображения в очередь. Было бы разумнее поместить это в метод, который присваивает .src, если вы действительно хотите сделать это неявно. Но я бы не стал вмешиваться в нативные методы jQuery, чтобы делать больше, чем вы ожидаете… - person Bergi; 16.07.2014
comment
@Bergi - я выбрал метод, который можно было бы использовать для любого изображения, независимо от того, как оно было создано (это может быть даже изображение, указанное в источнике страницы), потому что это увеличило бы его полезность. И я хотел перехватывать события загрузки только тогда, когда они действительно будут использоваться (кажется более эффективным). Перехват настройки .src на .attr() не сделает ни того, ни другого. Возможно, вам будет удобнее использовать .loadPromise(), который просто сделает .waitForLoad(), а затем вернет то, что возвращает .promise(), а обычный .promise() останется без изменений. - person jfriend00; 16.07.2014
comment
Добавлена ​​опция .loadPromise(). - person jfriend00; 17.07.2014
comment
Это довольно многословно, так как я ожидал чего-то более простого, но это заставило меня узнать кое-что новое - мне пришлось, чтобы понять код и обсуждение. Спасибо! - person zok; 18.07.2014

Вам нужно использовать http://api.jquery.com/jquery.when/

Я пытался изменить ваш код, но не запускал его. Я верю, что вы поймете это отсюда.

function getImageDimensions(url){
  var deferred = new $.Deferred();
  var img = $('<img src="'+ imagesURLArray[i] +'"/>').load(function(){
    var dimensions = [this.width, this.height];
    deferred.resolve(dimensions);
  });
  return deferred.promise()
}

var promises = []
for (var i = 0; i < imagesURLArray.length; i++) {
  promises.push(getImageDimensions(imagesURLArray[i]));
}
$.when.apply($, promises).then(function(dimensions){
   console.log('dimensions: ' + dimensions);
});
person jgreenberg    schedule 15.07.2014
comment
Спасибо, это кажется очень близким, хотя я все еще получаю только размеры первого файла. Мне удалось добиться не столь элегантного решения здесь - person zok; 16.07.2014
comment
Я бы преобразовал массив изображений в карту изображений {url:width} вместо того, чтобы передавать значение обратно в deferred . Я никогда не передавал данные обратно в отложенном виде. - person jgreenberg; 16.07.2014
comment
@jgreenebrg: На самом деле вы должны всегда передавать данные обратно в файл deferred.resolve. Даже если jQuery не сможет передать полезные результаты из $.when - person Bergi; 16.07.2014
comment
@Bergi Имеет смысл, у меня никогда не было причин это делать. Для меня просто имеет смысл, что у вас не будет доступа в результате, когда. - person jgreenberg; 16.07.2014