Pull to refresh

Как MooTools jQuery заборол, или детектив в стиле Коломбо

Reading time5 min
Views8.7K
Стек вызовов jQuery/MooTools По долгу работы в Айри я иногда разбираю ошибки функционирования сайта на сетевом уровне / уровне браузерного взаимодействия. Обычно это сводится к простому анализу заголовков запроса-ответа и воспроизведению тривиальных условий. Но иногда бывают интересные случаи.

Все начиналось холодным февральским вечером. Клиент написал о странной проблеме при ускорении сайта: слайд-шоу множилось и блокировало поведение сайта, страницы были недоступны. Через два дня после выяснения всех подробностей я узнал, почему Mootools и jQuery категорически нельзя использовать совместно. И подтвердился в мысли, что и «алкоголь — зло», и «eval — зло».

Но обо всем по порядку.

Выясняем корень бед


На момент сейчас в браузерах есть достаточное количество инструментов профилирования (даже, можно сказать, это количество несколько избыточное), позволяющих зафиксировать проблемное место, воспроизвести ошибку и ее устранить. Это, в первую очередь:
  • Сетевая панель в инструментах разработчика: она отображает все запросы, легитимно переданные браузером, со всеми заголовками и ответами. Если речь не идет о каком-то шифровании трафика и нужна стандартная информация о запросах (заголовки, состояние, размер), то ее более чем хватает.
  • Консоль (ошибок) в инструментах разработчика. В случае возникновения проблемы можно посмотреть как причину ошибки, так и стек обратных вызовов. А также повторить поведение браузера при выполнении кода.
  • DOM-дерево и исходный код сайта. Не всегда бывает понятно, какие свойства были заданы, а какие — динамически применились к HTML-элементам. Но всегда можно посмотреть информацию «из первых рук» — от браузера.

Если бы речь шла о простой трассировке ошибке — то на этом статью можно было бы заканчивать. Но ошибка была непростая, а рекурсивная. И вкладка браузера «падала» через несколько секунд после загрузки страницы, оставляя в консоли ошибок примерно такой след:
Стек вызовов
c.Request.Class.send		@	mootools-core.js:182
i.extend.$owner			@	mootools-core.js:50
Element.implement.load		@	mootools-core.js:187
st.event.trigger		@	jquery.js:2989
(anonymous function)		@	jquery.js:3639
st.extend.each			@	jquery.js:642
st.fn.st.each			@	jquery.js:263
st.fn.extend.trigger		@	jquery.js:3638
st.fn.(anonymous function)	@	jquery.js:3662
st.fn.load			@	jquery.js:7498
(anonymous function)		@	jquery.bxslider.js:11
st.extend.each			@	jquery.js:642
st.fn.st.each			@	jquery.js:263
(anonymous function)		@	jquery.bxslider.js:11
st.extend.each			@	jquery.js:642
st.fn.st.each			@	jquery.js:263
loadElements			@	jquery.bxslider.js:11
setup				@	jquery.bxslider.js:11
init				@	jquery.bxslider.js:6
$.fn.bxSlider			@	jquery.bxslider.js:52
(anonymous function)		@	VM375:2
f				@	jquery.js:1026
p.fireWith			@	jquery.js:1138
st.extend.ready			@	jquery.js:427
xt				@	jquery.js:97

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

Ошибка в слайдере bxslider

Лирическое отступление


Стоит сказать, что и MooTools, и jQuery приходили на сайт в сжатом виде. Однако, к jQuery шла карта исходников (source map), что существенно облегчило поиск крайнего (=виноватого).

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

Копаем вглубь


В любой проблеме важно выделить минимальные условия, все еще приводящие к возникновению проблемы (но без дополнительных данных), и эти условия последовательно проанализировать. Как оказалось, минимальным условием для стабильного воспроизведения проблемы было наличие встроенного (inline) JavaScript-кода в HTML-странице. Механизмы кэширования в браузере и у интернет-провайдеров могут помешать сделать простые выводы, поэтому при работе с публичными незашифрованными HTML-страницами необходимо четко формулировать и несколько раз перепроверять условия, чтобы быть в них уверенным наверняка.

Но почему включение JavaScript-кода в HTML приводило к рекурсивному его выполнению в браузере? Яндекс и Google ничего о таких ситуациях пока не знает. Нужно ему помочь.

Гипотезы: мало не бывает


Гипотезы С одной стороны все понятно: JavaScript inline-код выполняется рекурсивно, нельзя его использовать на данном сайте. С другой стороны: что конкретно приводит к рекурсивному выполнению inline-кода?

Разобраться помогает стек вызовов (листинг выше). Bxslider (Written while drinking Belgian ales and listening to jazz — бельгийский эль точно помешал автору спрогнозировать некоторые нестандартные варианты развития событий) на каждом объекте (в нашем случае — картинке) вызывал свойство load, которое обрабатывалось через jQuery примерно следующим образом:

Обработка события jQuery
jQuery.event.triggered = type;
try {
	elem[ type ]();
} catch ( e ) {
	// IE<9 dies on focus/blur to hidden element (#1486,#12518)
	// only reproducible on winXP IE8 native, not IE9 in IE8 mode
}

Вроде все четко: jQuery вызывает нативный метод у элемента, как только со всей остальной оберткой закончили. В данном случае, это img["load"](). Что должно приводить к завершению события load у изображения, оно должно положиться в кэш браузера, и все должны быть счастливы. Но с такой ситуацией не согласна библиотека MooTools:

Обработка load MooTools
Element.Properties.load = {
	set: function ( a ) {
		var b = this.get("load").cancel();
		b.setOptions(a);
		return this;
	},
	get: function() {
		var a = this.retrieve("load");
		if ( !a ) {
			a = new Request.HTML( {data: this, link: "cancel", update:this, method:"get"} );
			this.store("load", a);
		}
		return a
	}
};

MooTools метод load понимает по-своему. И в случае отсутствия информации об объекте, объект загружается через new Request.HTML. Вроде тоже нормально: давайте еще раз загрузим картинку, если о ней нет информации у MooTools (ведь картинка уже загрузилась в браузерный кэш, это просто операции в памяти компьютера локального пользователя). Но jQuery, когда вызывает этот метод у изображения, почему-то забывает передать параметры, в частности, URL. Наверное, jQuery не знает, что после него еще будет работать MooTools, которому эти параметры ой как нужны будут. И MooTools без параметров загружает «пустой» URL (текущую страницу).

Тоже вроде допустимый расклад: при загрузке страницы браузер загрузит ее еще 5 раз с сервера (просто HTML-документ), если в слайдере загружается 5 картинок (так и происходило на сайте). Если страница в серверном кэше, то на производительности это (почти) никак не отражается (и в ресурсах на сетевой панели инструментов разработчика найти эти «лишние» вызовы, перемешанные с картинками и счетчиками, тоже непросто).

Но проблема в том, что по умолчанию MooTools выполняет eval всех скриптов в загруженном HTML-документе. И это уже хуже: мы можем пережить выполнение кода счетчиков по нескольку раз на сайте. А если начинает выполняться обработчик DOMReady, который загружает слайдер, который вызывает загрузку изображений, которые вызывают загрузку HTML-страницы, на которой исполняется весь inline-код, который запускает обработчик DOMReady… Ну, вы поняли.

Резюме


Не использовать совместно несколько JavaScript-фреймворков. Никогда (для Joomla! есть даже плагин, вырезающий MooTools из системы). Если вдруг возникнет желание — перечитать данную статью. Описанная проблема оказалась «на поверхности» и была быстро выявлена. Но возможно ситуации, когда совместное поведение фреймворков будет зависеть от сети (задержек и порядка прохождения запросов) и используемого браузера. И тогда найти причину проблемы и ее исправить возможным совсем не кажется.

Использовать eval только в тех случаях, когда вы контролируете исполняемый код. Если нет контроля, нет eval.

Алкоголь — зло, эль — зло. Трезвый образ жизни — наше все.

Инструменты разработчика в браузерах реально могут дать вам всю необходимую информацию. Нужно уметь ими пользоваться.
Tags:
Hubs:
+2
Comments8

Articles