Контроль памяти в JavaScript
8 ноября 2013 JavaScript 12831 просмотр
Как и обычно, статья начинается с общих заезженных слов, что WEB уже не тот что был пару лет назад, и всё больше ответственности и логики переносится на Front-end. Например, в Tuffle.com Порядка 30 JS-файлов, а так как приложение асинхронное, то нужно как-то ими управлять. Об этом и пойдет речь.

HTML

Чтобы подключить скрипт на страницу, нужно в каком-то ее месте вставить тег с указанием пути к нужному скрипту.
<script src="my-script.js"></script>

Как поступит в этом случае браузер? Когда он дойдет то тега script, он остановится, пока не выкачает весь скрипт. Именно поэтому большинство разработчиков помещают этот тег в конец страницы, чтобы не блокировать отрисовку.

Async, Defer

Уже даже в HTML4 был добавлен атрибут defer. Он говорит браузеру о том, что скрипты нужно загружать в определенной последовательности, но только после того, как загрузится вся страница. Всё бы хорошо, да было много тёрок на эту тему, что скрипты загружаются не в той последовательности, а это критично, когда один скипт зависит от другого.
<script src="my-script-1.js" defer></script>
<script src="my-script-2.js" defer></script>

А вот уже в HTML5 появился атрибут async, который загружает скрипты асинхронно, параллельно с отрисовкой. Но опять же, порядка здесь не будет.

Обойдемся без script

Я имею в виду не то, что мы обойдемся без скриптов, но будем по-другому их загружать. По-настоящему асинхронно. Каждый скрипт за что-то отвечает и что-то выполняет, а это в свою очередь зависит от действий пользователя. Например, у нас есть страница с расширенным поиском, который открывается при нажатии на какую-то кнопку. А далеко-далеко внизу страницы есть нестандартный видеоплеер, который мы так долго писали. JS для поиска мы сохранили в search.js, для видеоплеера - в player.js. И добавили все эти скрипты на страницу, да еще и jQuery подключили. Но зачем? Если пользователь еще никуда не нажал, а видео у него еще не видно, так как он не дошел до низа страница. Поэтому начнем с того, что не будем делать ничего лишнего.

Точка входа

Итак, мы поняли, что загружать JS нужно только по требованию. Но кто будет обрабатывать эти требования? Создадим так называемую точу входа, или Front Controller на языке паттернов. Это будет единственный скрипт, который мы загрузим с помощью тега script. Хотя, можно обойтись и без тега. Назовем этот скрипт boot.js. Функционал, который нам нужен на данном этапе от boot.js: асинхронная загрузка скрипта с определением времени, когда этот скрипт загрузится.
/**
 *
 * @constructor
 */
function Boot() {
    this.cache = {}; // Если есть элемент с ключем src, значит файл уже загружен
}

/**
 * Этот метод асинхронно загружает скрипт и вызывает функцию callback после загрузки.
 * Если мы вызвали этот метод после того как срипт уже загружен, сразу выполнится callback.
 * @param {string} src
 * @param {function} callback
 */
Boot.prototype.loadScript = function Boot_loadScript(src, callback) {
    // ToDo : Exception, если скрипт не найден

    // Вариант, когда мы уже загрузили скрипт
    if (this.cache.hasOwnProperty(src)) {
        callback();
        return;
    }

    var s = this;

    var request = new XMLHttpRequest();
    request.open('GET', src, true) ;
    request.onreadystatechange = function() {
        if (this.readyState == 4) {
            var script = document.createElement('script');
            var text = document.createTextNode(this.responseText) ;
            script.appendChild(text) ;

            var head = document.getElementsByTagName('head')[0];
            head.insertBefore(script, head.firstChild) ;

            s.cache[src] = true;
            callback();
        }
    } ;
    request.send(null) ;
}

Сейчас у нас search.js не загружен. Как мы договорились, этот скрипт мы загрузим в том случае, если пользователь перейдет к расширенному поиску .
var b = new Boot();

document.getElementById('open-search').onclick = function() {
    b.loadScript('/js/search.js', function() {
        var searchObject = new Search();
        // Здесь инициализируется поиск
    });
};

Уничтожение

После завершения работы с чем-то нам опять же нужно побеспокоиться о том, чтобы ничего лишнего не осталось, т.е. подчистить концы. Что это может быть? События onclick и другие, интервалы, таймауты, да и вообще можно удадить сам объект. Так как всё это вертится в памяти. Я использую простой стандарт: создавая класс, я создаю первым делом метод destroy. Как может выглядеть этот destroy на примере search.js.
Search.prototype.destroy = function Search_destroy() {
    // Всего лишь примеры
    this.btn.onclick = null;
    clearInterval(this.interval);
    this.container.innerHtml = '';
}

И вызовем этот метод тогда, когда пользователь закроет поиск.
document.getElementById('close-search').onclick = function() {
    searchObject.destroy();
    delete searchObject;
};

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