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

    Стек вызовов 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.

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

    Инструменты разработчика в браузерах реально могут дать вам всю необходимую информацию. Нужно уметь ими пользоваться.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 8

      0
      Разве api.jquery.com/jquery.noconflict/ не исправит проблему?
      быстрое гугление нашло еще и это extensions.joomla.org/extension/jquery-easy
        0
        Нет. Если MooTools потом еще где-то используется. Т.е. простое отключение MooTools в данном конкретном случае было невозможно: ломалась логика сайта. Единственное верный выход — переводить всю логику на один фреймворк, а потом отключать, например, MooTools.
        0
        Довольно смело делать столь категорические заявления. Проблема вовсе не в совместном использовании нескольких фреймворков.
          0
          Приведите опровергающие доводы, если делаете такое заявление. Проблема как раз из-за наложения функционала одного фреймворка на функционал другого.
          +1
          Если у вас яваскрипт код вставлен в слайд, то он и так как минимум 2 раза выполнится, а при переключении слайдов, вероятно, будет повторно выполняться.

          Дело в том, что Bxslider делает копии слайдов. А при вставке кода на страницу (в данном случае копии слайда) джквери ищет в нем скрипты и выполняет их.

          Очень не приятная ситуация, после которой bxslider мне разонравился.

          Как дела с этим в других слайдерах — не знаю, но в следующий раз попробую подружиться со slick (http://kenwheeler.github.io/slick/).

          Если у кого есть на примете хорошие кроссбраузерные слайдеры с поддержкой старых браузеров, но использующие новые возможности css3 — буду очень благодарен за ссылку.
            +1
            Хорошее замечание, но неверное относительно ситуации в статье. Описанная ситуация имела место конкретно с MooTools: при удалении MooTools (или переопределении соответствующего метода) проблема исчезала.
            Стек вызовов, собственно, говорит сам за себя. Равно как и бесчисленные запросы со стороны MooTools на загрузку страницы.

            Bxslider, в принципе, может быть не очень надежным, и обладать другими проблемами. Но их в статье я не исследовал.
              0
              Ага, просто ситуация достаточно схожа, а еще и bxslider фигурирует :)
            0
            Еще кто-то пользуется MooTools?

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое