Опубликован: 16.02.2009 | Уровень: специалист | Доступ: платный
Лекция 7:

Оптимизация JavaScript

Циклические ссылки

Циклические ссылки являются источником практически любой утечки. Обычно скриптовые движки нормально отрабатывают с циклическими ссылками при помощи собственных сборщиков мусора, однако из-за некоторых неопределенностей их механизм эвристических правил может дать сбой. Одной из таких неопределенностей будет состояние DOM-объекта, к которому имеет доступ текущая порция скрипта. Основной принцип в данном случае можно описать так:

 Основной шаблон циклической ссылки

Рис. 7.3. Основной шаблон циклической ссылки

Утечка в таком шаблоне происходит из-за особенностей учета DOM-ссылок. Объекты скриптового движка удерживают ссылку на DOM-элемент и ожидают, пока будут освобождены все внешние ссылки, чтобы освободить, в свою очередь, этот указатель на DOM-элемент. В нашем случае у нас две ссылки на объект скрипта: внутри области видимости скриптового движка и от расширенного свойства DOM-элемента. По окончанию своей работы скрипт освободит первую ссылку, но ссылка из DOM-элемента никогда не будет освобождена, потому что ждет, что это сделает объект скрипта!

Наверное, первой же мыслью будет, что такой сценарий развития событий легко обнаружить и устранить, однако на практике представленный базовый случай является только вершиной айсберга. Может оказаться так, что циклическая ссылка находится в конце цепочки из 30 объектов, а обнаружить ее при этом крайне тяжело.

Стоит посмотреть, как данный шаблон будет выглядеть в HTML. Это может вызвать утечку, используя глобальную переменную и DOM-объект, как показано ниже.

<script type="text/javascript">

	var myGlobalObject;

	function SetupLeak()
	{
	// Для начала создадим ссылку из скрипта на DOM-элемент
		myGlobalObject = document.getElementById("LeakedDiv");

	// Потом установим ссылку из DOM на глобальную переменную
		document.getElementById("LeakedDiv").expandoProperty =
			myGlobalObject;
	}

	function BreakLeak()
	{
		document.getElementById("LeakedDiv").expandoProperty = null;
	}

	window.onload = SetupLeak;
	window.onunload = BreakLeak;

</script>

Чтобы разрушить этот шаблон, можно использовать явное присвоение null тому свойству, которое "течет". Таким образом, при закрытии документа мы сообщаем скриптовому движку, что между DOM-элементом и глобальной переменной нет больше никакой связи. В результате все ссылки будут очищены, и сам DOM-элемент будет освобожден. В таком случае веб-разработчик знает больше о внутренних отношениях между объектами, чем сам скрипт, и может поделиться этой информацией со скриптом.

Более сложный случай

Хотя это только базовый шаблон, для более сложных ситуаций может оказаться нелегко выяснить первопричину утечки. Распространенной практикой по написанию объектно-ориентированного JScript является расширение DOM-элементов путем инкапсуляции их внутри JScript-объекта. В процессе создания такого объекта в большинстве случаев получается ссылка на желаемый DOM-элемент, а затем она сохраняется в только что созданном объекте, при этом экземпляр этого объекта оказывается прикрепленным к DOM-элементу. Таким способом модель приложения всегда получает доступ ко всему что нужно. Проблема заключается в том, что это явная циклическая ссылка, но из-за использования других аспектов языка она может остаться незамеченной. Устранение шаблонов такого рода может быть весьма затруднительным, но вы вполне можете использовать простые методы, обсужденные ранее.

<script type="text/javascript">

	function Encapsulator(element)
	{
	// Создаем элемент
		this.elementReference = element;

	// Создаем циклическую ссылку
		element.expandoProperty = this;
	}

	function SetupLeak()
	{
	// Утечка: все в одном
		new Encapsulator(document.getElementById("LeakedDiv"));
	}

	function BreakLeak()
	{
		document.getElementById("LeakedDiv").expandoProperty = null;
	}

	window.onload = SetupLeak;
	window.onunload = BreakLeak;

</script>

<div id="LeakedDiv"></div>

Более комплексным решением заявленной проблемы может стать создание процесса регистрации ссылок, чтобы соответствующим образом уведомлять приложение, какие из них могут быть удалены. В этом случае по закрытию документа нужно будет пройтись по всем таким элементам и убрать с них ссылки. Однако при неверном подходе можно не только не уменьшить, а даже увеличить количество циклических ссылок, не избавляясь от заявленной проблемы.

Замыкания

Замыкания очень часто являются причиной утечек из-за того, что они создают циклические ссылки практически без ведома программиста. Ведь не так очевидно, что параметры родительской функции могут быть "заморожены" во времени, на них может быть создана ссылка, которая будет удерживаться до полного освобождения замыкания. Это стало уже широко распространенной практикой программирования, и пользователи достаточно часто сталкиваются с нехваткой доступных ресурсов.

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

 Циклические ссылки с самозамыканием

Рис. 7.4. Циклические ссылки с самозамыканием

В случае обычной циклической ссылки у нас есть 2 постоянных объекта, которые содержат ссылки друг на друга, - но замыкания отличаются от этой модели. Вместо создания прямых ссылок они наследуют информацию из пространства порождающих их объектов. В обычном случае локальные переменные в функции и используемые параметры существуют только в течение времени жизни самой функции. В случае замыканий эти переменные и параметры имеют внешнюю ссылку на весь период жизни замыкания, который может быть значительно дольше, чем у породившей ее функции.

В качестве примера можно рассмотреть Объект 2, который был бы освобожден по окончанию вызова функции, в общем случае. Однако после добавления замыкания была создана вторая ссылка на этот параметр, которая не может быть освобождена, пока не будет закрыто замыкание. Если вы прикрепили замыкание к событию, то вам придется его у события в конце концов убрать. Если замыкание прикреплено к расширенному параметру, нужно будет его занулить (приравнять этот параметр к null ).

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

<script type="text/javascript">

	function AttachEvents(element)
	{
	// Эта структура создает у элемента ссылку на ClickEventHandler
		element.attachEvent("onclick",ClickEventHandler);

		function ClickEventHandler()
		{
		// Это замыкание ссылается на элемент
		}
	}

	function SetupLeak()
	{
	// Происходит утечка
		AttachEvents(document.getElementById("LeakedDiv"));
	}

	function BreakLeak()
	{
	}

	window.onload = SetupLeak;
	window.onunload = BreakLeak;

</script>

<div id="LeakedDiv"></div>

Устранить эту утечку не так просто, как в случае с обычной циклической ссылкой. "Замыкание" можно рассматривать как временный объект, который существует в области видимости функции. После завершения функции ссылка на само замыкание теряется, поэтому встает вопрос: как же вызвать завершающий detachEvent?

Один из возможных выходов заключается в использовании второго замыкания, которое цепляется на событие onUnload всего окна браузера. Поскольку оно имеет объекты из той же области видимости, то становится возможным снять обработку события, высвободить замыкание и завершить процесс очистки. Чтобы окончательно прояснить ситуацию, мы можем добавить в наш пример дополнительное свойство, в котором сохраним ссылку на замыкание, затем по ссылке освободим замыкание и обнулим само свойство.

<script type="text/javascript">

	function AttachEvents(element)
	{
	// чтобы иметь возможность освободить замыкание,
	// мы должны где-то сохранить ссылку на него
		element.expandoClick = ClickEventHandler;

	// Эта структура создает у элемента ссылку
	// на ClickEventHandler
		element.attachEvent("onclick", element.expandoClick);

		function ClickEventHandler()
		{
		// Это замыкание ссылается на элемент
		}
	}

	function SetupLeak()
	{
	// Происходит утечка
		AttachEvents(document.getElementById("LeakedDiv"));
	}

	function BreakLeak()
	{
		document.getElementById("LeakedDiv").detachEvent("onclick",
	  		document.getElementById("LeakedDiv").expandoClick);
		document.getElementById("LeakedDiv").expandoClick = null;
	}
	window.onload = SetupLeak;
	window.onunload = BreakLeak;

</script>

<div id="LeakedDiv"></div>

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

В данном примере можно не использовать замыкание как обработчик события, а переместить его в глобальный контекст. Как только замыкание становится функцией, оно больше не наследует параметры или локальные переменные от родительской функции, поэтому нам не нужно вообще волноваться по поводу циклических ссылок, вызываемых данным замыканием. Так что бoльшая часть проблем может быть решена просто путем устранения всех замыканий, которые реально не нужны.

Дарья Билялова
Дарья Билялова
Россия
Елена Петрушевская
Елена Петрушевская
Россия, г. Нижневартовск