ilyachalov (ilyachalov) wrote,
ilyachalov
ilyachalov

Category:

JavaScript: предзагрузка изображений, упрощаю код

Начало: JavaScript: предзагрузка изображений.

В прошлом посте я решал задачу из учебника по JavaScript и написал такую функцию:
function preloadImages(sources, callback) {
    let imgs = [],   // массив HTML-элементов img для предзагрузки картинок
        loaded = []; // массив HTML-элементов img с загруженной картинкой
    
    // цикл выполнения предзагрузки заданных картинок
    for (let i = 0; i < sources.length; i++) {

        // запуск предзагрузки очередной картинки
        let img = document.createElement("img");
        img.src = sources[i];

        // после окончания предзагрузки картинки поместить ее в массив загруженных,
        // и проверить, если это была последняя из заданных картинок, то запустить
        // функцию callback
        img.addEventListener("load", onLoad);
        img.addEventListener("error", onLoad);
        function onLoad() {
            loaded.push(this);
            if (loaded.length == sources.length) callback();
        }

        imgs.push(img);
    }
}

Для чего я создал массив imgs? Я сохраняю в нем ссылки на HTML-элементы img, которые нужны лишь для предзагрузки заданных изображений. Эти HTML-элементы не добавляются на HTML-страницу.

Я полагал, что эти HTML-элементы img нужно временно где-то хранить, пока изображения не предзагрузятся в кэш браузера полностью и пока не исполнится функция-обработчик onLoad, повешенная на события load и error каждого временно созданного HTML-элемента img. Я думал, что, если ссылки на эти HTML-элементы нигде не сохранить, то они будут стерты сборщиком мусора и это нарушит работу моей функции.

https://learn.javascript.ru/garbage-collection

Однако, как оказалось, такое сохранение временных объектов img в данном случае не требуется.

1) Предзагрузка изображения браузером запускается при записи адреса изображения в свойство src временного объекта img. После этой записи с самим объектом img можно, как я понимаю, делать всё, что угодно, хоть попытаться его затереть. Предзагрузка изображения в кэш браузера всё равно будет выполнена. Чтобы это проверить, я написал такой код:
let img = document.createElement("img");
img.src = "https://en.js.cx/images-load/1.jpg" + "?" + Math.random();
img = "";

Здесь в первой строке создан временный объект, ссылка на который записана в переменную img. Во второй строке записываем адрес изображения в свойство src объекта. Это заставляет браузер запустить загрузку изображения в кэш. В третьей строке затираем ссылку на объект, созданный в первой строке, значением "". Как я понимаю, после этого сборщик мусора должен стереть объект, созданный в первой строке, из памяти, так как на этот объект больше нет ссылки из нашего скрипта. При этом загрузка изображения в кэш браузера заканчивается благополучно, я проверил с помощью инструмента разработчика «Network» в моём браузере (у меня — «Microsoft Edge» на движке «Chromium»).

2) А как тогда будет срабатывать функция-обработчик onLoad? Ведь, по идее, на момент, когда она будет запущена, локальная переменная img уже, скорее всего, не будет существовать (каждая ее версия существует только в лексическом окружении своей итерации цикла for).

Тут, как я понимаю, сработает то, что функция-обработчик onLoad является замыканием, то есть помнит значения переменных в окружающем ее коде. Благодаря этой своей способности каждая функция onLoad помнит ссылку на свой (своей итерации) объект img, поэтому (по идее) сборщик мусора не будет удалять этот объект из памяти.

https://learn.javascript.ru/closure
https://ru.wikipedia.org/wiki/Замыкание_(программирование)
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

Ссылки на сами функции onLoad, полагаю, хранятся в неких списках (массивах), создаваемых браузером, куда они попадают при регистрации посредством метода addEventListener.

Таким образом, массив imgs из кода можно убрать и функцию упростить:
function preloadImages(sources, callback) {
    let loaded = []; // массив HTML-элементов img с загруженной картинкой
    
    // цикл выполнения предзагрузки заданных картинок
    for (let i = 0; i < sources.length; i++) {

        // запуск предзагрузки очередной картинки
        let img = document.createElement("img");
        img.src = sources[i];

        // после окончания предзагрузки картинки поместить ее в массив загруженных,
        // и проверить, если это была последняя из заданных картинок, то запустить
        // функцию callback
        img.addEventListener("load", onLoad);
        img.addEventListener("error", onLoad);
        function onLoad() {
            loaded.push(this);
            if (loaded.length == sources.length) callback();
        }
    }
}
Я проверил, эта версия кода работает так же, как и предыдущая версия.

Массив loaded тоже можно убрать, заменив на переменную-счетчик. Но от массива loaded может быть польза: HTML-элементы img в него записываются в том порядке, в котором заканчивается предзагрузка соответствующих изображений.

Если такая информация не нужна, то заменяем массив loaded на переменную-счетчик loaded. В результате код функции становится еще проще:
function preloadImages(sources, callback) {
    let loaded = 0; // счетчик предзагруженных картинок
    
    // цикл выполнения предзагрузки заданных картинок
    for (let i = 0; i < sources.length; i++) {

        // запуск предзагрузки очередной картинки
        let img = document.createElement("img");
        img.src = sources[i];

        // после окончания предзагрузки картинки увеличить значение счетчика,
        // и проверить, если это была последняя из заданных картинок, то запустить
        // функцию callback
        img.addEventListener("load", onLoad);
        img.addEventListener("error", onLoad);
        function onLoad() {
            loaded++;
            if (loaded == sources.length) callback();
        }
    }
}

Этот вариант кода очень близок к варианту решения от авторов задачи.

* * *

Дополнение от 8 октября 2021 года. Логично будет вынести функцию onLoad за пределы цикла. Это не уменьшит длину скрипта, зато, по идее, уменьшит нагрузку на память: мы объявим функцию один раз за пределами цикла, а не будем объявлять ее в каждой итерации цикла. Меняем код:
function preloadImages(sources, callback) {
    let loaded = 0; // счетчик предзагруженных картинок

    // после окончания предзагрузки картинки увеличить значение счетчика,
    // и проверить, если это была последняя из заданных картинок, то запустить
    // функцию callback
    function onLoad() {
        loaded++;
        if (loaded == sources.length) callback();
    }
    
    // цикл выполнения предзагрузки заданных картинок
    for (let i = 0; i < sources.length; i++) {
        // запуск предзагрузки очередной картинки
        let img = document.createElement("img");
        img.src = sources[i];
        // запланируем запуск функции onLoad после предзагрузки картинки
        img.addEventListener("load", onLoad);
        img.addEventListener("error", onLoad);
    }
}
Этот вариант кода выдает в тесте такой же результат, как и предыдущий вариант кода.

Однако, работает он несколько по-другому. В предыдущем варианте кода в замыкание функции onLoad попадала переменная img, создаваемая в соответствующей итерации цикла. Из-за этого функция onLoad могла видеть эту переменную, несмотря на то, что к моменту запуска функции onLoad соответствующая итерация цикла была закончена (я это проверил) и переменная img должна была быть уничтожена сборщиком мусора.

В текущем на данный момент поста варианте кода функция onLoad находится вне цикла и переменная img уже не попадает в замыкание функции onLoad. Поэтому функция onLoad не видит переменную img, но всё-таки может получить ссылку на объект, ссылка на который хранилась в этой переменной. Я проверил это, внеся следующие изменения в функцию onLoad:
    function onLoad() {
        console.log(img);  // ошибка
        console.log(this); // нет ошибки
        loaded++;
        if (loaded == sources.length) callback();
    }
В рабочем коде, конечно, следует оставить только один из этих двух вызовов функции console.log, а другой закомментировать. И наоборот.

Как так получилось, что функция onLoad всё же видит объект, созданный в закончившейся на момент запуска функции onLoad итерации цикла? (Она видит его посредством ключевого слова this.) Я полагаю, что в момент регистрации функции onLoad в качестве функции-обработчика некоего события с помощью метода addEventListener в список (массив) регистрации записывается не только ссылка на саму функцию, но и ссылка на объект, с которым связано целевое событие. Поэтому эту ссылку на объект позже можно получить с помощью ключевого слова this.
Tags: Образование, Программирование
Subscribe

  • JavaScript: Blob

    Прочел подраздел 2.3 « Blob» третьей части учебника по JavaScript. Для меня эта статья оказалась настолько объемной в плане нового, что ее разбор…

  • Сказка про двоичные данные, кодировку Windows-1251 и Юникод

    Вопрос из комментариев к подразделу 2.3 « Blob» третьей части учебника по JavaScript, цитата: Не могу найти способ выдавать файл с кодировкой…

  • Как работает кодирование Base64, окончание

    Начало: « Как работает кодирование Base64». Пример второй. « Картинка-смайлик» В предыдущем примере кусок двоичных данных, содержащий текст,…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments