Тема этой статьи на самом деле довольно специфична. Недавно я столкнулся с ситуацией, когда мне нужно было предварительно загружать много изображений параллельно. С учетом этих ограничений все оказалось более сложным, чем ожидалось, и я, безусловно, многому научился за это время. Но сначала позвольте мне описать ситуацию незадолго до начала.
Допустим, у нас есть несколько «колод» на странице. Вообще говоря, колода — это коллекция изображений. Мы хотим предварительно загрузить изображения каждой колоды и узнать, когда колода закончила загрузку всех ее изображений. На этом этапе мы можем свободно запускать любой фрагмент кода, который нам нужен, например, добавление класса в колоду, запуск последовательности изображений, запись чего-либо, что угодно…
Сначала это звучит довольно просто. Это даже звучит очень легко. Хотя, возможно, вы, как и я, упустили из виду одну деталь: мы хотим, чтобы все колоды загружались параллельно, а не последовательно. Другими словами, мы не хотим загружать все изображения из колоды 1, затем все изображения из колоды 2, затем все изображения из колоды 3 и так далее.
На самом деле, это не идеально, потому что у нас заканчиваются колоды, ожидающие окончания предыдущих. Таким образом, в сценарии, где первая колода имеет десятки изображений, а вторая имеет только одно или два, нам нужно будет дождаться полной загрузки первой колоды, прежде чем она будет готова ко второй колоде. Тьфу, не здорово. Конечно, мы можем сделать лучше!
Поэтому идея состоит в том, чтобы загружать все колоды параллельно, чтобы при полной загрузке колоды нам не приходилось ждать остальных. Для этого, грубо говоря, нужно загрузить первое изображение всех колод, затем второе из всех колод и так далее, пока все изображения не будут предварительно загружены.
Хорошо, давайте начнем с создания некоторой разметки, чтобы мы все согласились с тем, что происходит.
Кстати, в этой статье я буду предполагать, что вы знакомы с идеей обещаний. Если это не так, я рекомендую это маленькое чтение .
Разметка
С точки зрения разметки, колода — это не что иное, как элемент, такой как div
, с классом deck
мы можем ориентироваться, и атрибутом data-images
содержащим массив URL-адресов изображений (как JSON).
<div class="deck" data-images='["...", "...", "..."]'>...</div> <div class="deck" data-images='["...", "..."]'>...</div> <div class="deck" data-images='["...", "...", "...", "..."]'>...</div>
Подготовка основания
Что касается JavaScript, то, что неудивительно, это немного сложнее. Мы создадим две разные вещи: класс колоды (пожалуйста, поместите это между очень большими кавычками и не придирайтесь к термину) и инструмент предварительной загрузки.
Поскольку предварительный загрузчик должен знать обо всех изображениях из всех колод, чтобы загружать их в определенном порядке, он должен быть общим для всех колод. У колоды не может быть собственного предзагрузчика, иначе мы столкнемся с первоначальной проблемой: код выполняется последовательно, а это не то, что нам нужно.
Поэтому нам нужен предварительный загрузчик, который передается в каждую колоду. Последний добавляет свои изображения в очередь предварительного загрузчика, и как только все колоды добавили свои элементы в очередь, предварительный загрузчик может начать предварительную загрузку.
Фрагмент кода выполнения будет:
// Instantiate a preloader var ip = new ImagePreloader(); // Grab all decks from the DOM var decks = document.querySelectorAll('.deck'); // Iterate over them and instantiate a new deck for each of them, passing the // preloader to each of them so that the deck can add its images to the queue Array.prototype.slice.call(decks).forEach(function (deck) { new Deck(deck, ip); }); // Once all decks have added their items to the queue, preload everything ip.preload();
Я надеюсь, что это имеет смысл, пока!
Сборка колоды
В зависимости от того, что вы хотите сделать с колодой, «класс» может быть довольно длинным. Для нашего сценария единственное, что мы делаем, это добавляем loaded
класс в узел, когда его изображения уже загружены.
Функция Deck
не имеет ничего общего:
- Загрузка данных (из атрибута
data-images
) - Добавление данных в конец очереди прелоадера
- Сообщаем прелоадеру, что делать, когда данные предварительно загружены
var Deck = function (node, preloader) { // We get and parse the data from the `data-images` attribute var data = JSON.parse(node.getAttribute('data-images')); // We call the `queue` method from the preloader, passing it the data and a // callback function preloader.queue(data, function () { node.classList.add('loaded'); }); };
Пока все идет хорошо, не так ли? Остается только предзагрузчик, хотя это также самый сложный фрагмент кода из этой статьи.
Сборка Preloader
Мы уже знаем, что нашему предварительному загрузчику нужен метод queue
чтобы добавить коллекцию изображений в очередь, и метод preload
чтобы запустить предварительную загрузку. Ему также понадобится вспомогательная функция для предварительной загрузки изображения, называемая preloadImage
. Давайте начнем с этого:
var ImagePreloader = function () { ... }; ImagePreloader.prototype.queue = function () { ... } ImagePreloader.prototype.preloadImage = function () { ... } ImagePreloader.prototype.preload = function () { ... }
Предварительному загрузчику необходимо свойство внутренней очереди для хранения колод, которые он должен предварительно загрузить, а также их соответствующий обратный вызов.
var ImagePreloader = function () { this.items = []; }
items
— это массив объектов, каждый из которых имеет два ключа:
-
collection
содержащая массив изображений URL для предварительной загрузки, -
callback
содержащий функцию для выполнения, когда колода полностью загружена.
Зная это, мы можем написать метод queue
.
// Empty function in case no callback is being specified function noop() {} ImagePreloader.prototype.queue = function (array, callback) { this.items.push({ collection: array, // If no callback, we push a no-op (empty) function callback: callback || noop }); };
Хорошо. На этом этапе каждая колода может добавлять свои изображения в очередь. Теперь нам нужно создать метод preload
, который позаботится о фактической предварительной загрузке изображений. Но прежде чем переходить к коду, давайте сделаем шаг назад, чтобы понять, что нам нужно делать.
Идея состоит не в том, чтобы предварительно загружать все изображения из каждой колоды, одно за другим. Идея состоит в том, чтобы предварительно загрузить первое изображение каждой колоды, затем второе, затем третье и так далее.
Предварительная загрузка изображения означает создание нового изображения из JavaScript (с использованием new Image()
) и применение к нему src
. Это приведет к тому, что браузер загрузит источник асинхронно. Из-за этого асинхронного процесса нам нужно зарегистрировать обещание, которое разрешится, когда ресурс будет загружен браузером.
По сути, мы заменим каждый URL изображения из наших массивов обещанием, которое будет выполнено, когда данное изображение будет загружено браузером. На этом этапе мы сможем использовать Promise.all(..)
чтобы получить окончательное обещание, которое разрешается, когда все обещания из массива разрешены. И это для каждой колоды.
Давайте начнем с метода preloadImage
:
ImagePreloader.prototype.preloadImage = function (path) { return new Promise(function (resolve, reject) { // Create a new image from JavaScript var image = new Image(); // Bind an event listener on the load to call the `resolve` function image.onload = resolve; // If the image fails to be downloaded, we don't want the whole system // to collapse so we `resolve` instead of `reject`, even on error image.onerror = resolve; // Apply the path as `src` to the image so that the browser fetches it image.src = path; }); };
А теперь метод preload
. Он делает две вещи (и, следовательно, может быть разделен на две разные функции, но это выходит за рамки данной статьи):
- Он заменяет все URL-адреса изображений обещаниями в определенном порядке (сначала изображение из каждой колоды, затем второе, затем третье …)
- Для каждой колоды регистрируется обещание, которое вызывает колбэк из колоды, когда все обещания колоды разрешены (!)
ImagePreloader.prototype.preload = function () { // Promises are not supported, let's leave if (!('Promise' in window)) { return; } // Get the length of the biggest deck var max = Math.max.apply(Math, this.items.map(function (el) { return el.collection.length; })); // Loop from 0 to the length of the largest deck for (var i = 0; i < max; i++) { // Iterate over the decks this.items.forEach(function (item) { // If the deck is over yet, do nothing, else replace the image at // current index (i) with a promise that will resolve when the image // gets downloaded by the browser. if (typeof item.collection[i] !== 'undefined') { item.collection[i] = this.preloadImage(item.collection[i]) } }, this); } // Iterate over the decks this.items.forEach(function (item, index) { // When all images from the deck have been fetched by the browser Promise.all(item.collection) // Execute the callback .then(function () { item.callback() }) .catch(function (err) { console.log(err) }); }); };
Это оно! В конце концов, не так уж сложно, ты согласен?
Толкая вещи дальше
Код работает отлично, хотя использование обратного вызова, чтобы сказать прелоадеру, что делать, когда загружена колода, не очень элегантно. Возможно, вы захотите использовать Обещание, а не обратный вызов, тем более что мы все время использовали Обещания!
Я не был уверен, как справиться с этим, поэтому должен согласиться, что попросил моего друга Валериана Галлиата помочь мне в этом.
То, что мы используем здесь, — это отложенное обещание . Отложенные обещания не являются частью собственного API Promise, поэтому нам нужно его заполнить; К счастью, это всего лишь пара строк. По сути, отложенное обещание — это обещание, которое вы можете выполнить позже.
Применяя это к нашему коду, это изменило бы очень маленькие вещи. .queue(..)
метод .queue(..)
:
ImagePreloader.prototype.queue = function (array) { var d = defer(); this.items.push({ collection: array, deferred: d }); return d.promise; };
Разрешение в .preload(..)
:
this.items.forEach(function (item) { Promise.all(item.collection) .then(function () { item.deferred.resolve() }) .catch(console.log.bind(console)); });
И, наконец, способ, которым мы добавляем наши данные в очередь, конечно!
preloader.queue(data) .then(function () { node.classList.add('loaded'); }) .catch(console.error.bind(console));
И мы сделали!
Если вы хотите увидеть код в действии, посмотрите демонстрацию ниже:
Выводы
Там вы идете, ребята. Приблизительно в 70 строках JavaScript нам удалось параллельно асинхронно загружать изображения из разных коллекций и выполнять некоторый код после завершения загрузки коллекции.
Оттуда мы можем многое сделать. В моем случае целью было запустить эти изображения в виде быстрой последовательности петель (в стиле gif) при нажатии кнопки. Поэтому я отключил кнопку во время загрузки и включил ее снова, как только колода закончила загружать все свои изображения. Благодаря этому первый цикл запуска проходит без проблем, поскольку браузер уже кэшировал все изображения.
Я надеюсь тебе это понравится! Вы можете взглянуть на код на GitHub или поиграть с ним прямо на CodePen .