Как Вы знаете — в разработке объёмного JS-приложения ��де используется популярнейшая библиотека jQuery наступает момент когда остро встаёт проблема производительности. Все силы кидаются на амбразуру профайлера, каждый вызов скрупулёзно исследован, каждый функционально нагруженный кусок реализации обнюхан со всех сторон и выправлен. Но беда поступает не с той стороны, откуда её ждут 90% разработчиков. Селекторы — Как много в этом слове.
Давайте разберёмся — как работает эта магия и почему поиск DOM-элементов может стать причиной падения производительности приложения.
В самой библиотеке для поиска элементов используется движок Sizzle у которого есть ряд особенностей. Их мы и рассмотрим.
В новых браузерах появилась отменная функция querySelectorAll() и querySelector(), которая умеет производить поиск элементов используя возможности браузера (в частности — используемые при просмотре CSS и назначении свойств элементам). Работает данная функция не во всех браузерах, а только в FF 3.1+, IE8+ (только в стандартном режиме IE8), Opera 9.5+(?) и Safari 3.1+. Так вот Sizzle всегда определяет наличие данной функции и пытается любой поиск выполнить через неё. Однако, тут не без сюрпризов — для успешного использования querySelectorAll() наш селектор должен быть валидным.
Приведу простой пример:
Два приведенных селектора практически ничем не отличаются и вернут одинаковый набор элементов. Однако первый селектор вернёт результат работы querySelectorAll(), а второй — результат обычной фильтрации по элементам.
Если не удалось использовать querySelectorAll(), Sizzle будет пытаться использовать обычные функции браузера getElementById(), getElementsByName(), getElementsByTagName() и getElementByClass(). В большинстве случаев их хватает, но (sic!) в IE < 9 getElementByClassName() поломана и использование селектора по классу приведёт к полному перебору элементов в этом браузере.
В общем случае Sizzle разбирает селектор справа налево. Для иллюстрации данной особенности приведу несколько примеров:
Сначала будут найдены элементы .my_class, а потом отфильтрованы только те, которые имеют .divs в родителях. Как мы видим — это довольно затратная операция, причём использование контекста не решает проблемы (о контексте мы поговорим ниже).
Как я уже сказал — в большинстве случаев Sizzle будет разбирать селектор справа налево, но не в случае использования элемента с ID:
В таком случае селектор поведёт себя как ожидалось и сразу будет взят элемент #divs для использования в виде контекста.
Второй параметр, передаваемый вместе с селектором в функцию $() называется контекстом. Он призван сузить круг поиска элементов. Однако — при разборе контекст пристыкуется к началу селектора, что выигрыша совершенно не даёт. Выигрышная комбинация использования контекста — валидный селектор для querySelectorAll(), так как данная функция может быть применена не только от имени document, но и от элемента. Тогда селектор с контекстом образно трансформируется следующую конструкцию:
В данном примере, если невозможно использовать querySelectorAll(), Sizzle будет фильтровать элементы простым перебором.
Ещё одно замечание о контексте (речь не о селекторах) — если вторым параметром в селектор для функции .live() передать объект jQuery — событие будет ловиться на document(!), а если DOM-объект — то всплывать событие будет только до этого элемента. Такие тонкости я стараюсь не запоминать, а использую .delegate().
Для поиска вложенных элементов можно воспользоваться следующим селектором:
Как мы знаем — разбор селектора и поиск начнётся со всех .child элементов на странице (при условии, что querySelectorAll() недоступно). Такая операция достаточно затратна и мы можем трансформировать селектор так:
Однако, если есть необходимость использовать фильтры по каким-либо атрибутам (:visible, :eq и т.д.) и селектор выглядит так:
то фильтр будет применён в последнюю очередь — т.е. мы опять отступаем от правила «справа налево».
Быстрых селекторов Вам в новом году! Всех в наступившим!
По мотивам мастер-класса Ильи Кантора и собственных наблюдений.
Давайте разберёмся — как работает эта магия и почему поиск DOM-элементов может стать причиной падения производительности приложения.
Как jQuery разбирает селектор
В самой библиотеке для поиска элементов используется движок Sizzle у которого есть ряд особенностей. Их мы и рассмотрим.
querySelectorAll()
В новых браузерах появилась отменная функция querySelectorAll() и querySelector(), которая умеет производить поиск элементов используя возможности браузера (в частности — используемые при просмотре CSS и назначении свойств элементам). Работает данная функция не во всех браузерах, а только в FF 3.1+, IE8+ (только в стандартном режиме IE8), Opera 9.5+(?) и Safari 3.1+. Так вот Sizzle всегда определяет наличие данной функции и пытается любой поиск выполнить через неё. Однако, тут не без сюрпризов — для успешного использования querySelectorAll() наш селектор должен быть валидным.
Приведу простой пример:
Два приведенных селектора практически ничем не отличаются и вернут одинаковый набор элементов. Однако первый селектор вернёт результат работы querySelectorAll(), а второй — результат обычной фильтрации по элементам.
$('#my_form input[type="hidden"]')$('#my_form input[type=hidden]')Разбор селектора и поиск
Если не удалось использовать querySelectorAll(), Sizzle будет пытаться использовать обычные функции браузера getElementById(), getElementsByName(), getElementsByTagName() и getElementByClass(). В большинстве случаев их хватает, но (sic!) в IE < 9 getElementByClassName() поломана и использование селектора по классу приведёт к полному перебору элементов в этом браузере.
В общем случае Sizzle разбирает селектор справа налево. Для иллюстрации данной особенности приведу несколько примеров:
$('.divs .my_class')Сначала будут найдены элементы .my_class, а потом отфильтрованы только те, которые имеют .divs в родителях. Как мы видим — это довольно затратная операция, причём использование контекста не решает проблемы (о контексте мы поговорим ниже).
Как я уже сказал — в большинстве случаев Sizzle будет разбирать селектор справа налево, но не в случае использования элемента с ID:
$('#divs .my_class')В таком случае селектор поведёт себя как ожидалось и сразу будет взят элемент #divs для использования в виде контекста.
Контекст
Второй параметр, передаваемый вместе с селектором в функцию $() называется контекстом. Он призван сузить круг поиска элементов. Однако — при разборе контекст пристыкуется к началу селектора, что выигрыша совершенно не даёт. Выигрышная комбинация использования контекста — валидный селектор для querySelectorAll(), так как данная функция может быть применена не только от имени document, но и от элемента. Тогда селектор с контекстом образно трансформируется следующую конструкцию:
$('.divs', document.getElementById('wrapper'));
document.getElementById('wrapper').querySelectorAll('.divs'); // при наличии возможности использовать querySelectorAll()
В данном примере, если невозможно использовать querySelectorAll(), Sizzle будет фильтровать элементы простым перебором.
Ещё одно замечание о контексте (речь не о селекторах) — если вторым параметром в селектор для функции .live() передать объект jQuery — событие будет ловиться на document(!), а если DOM-объект — то всплывать событие будет только до этого элемента. Такие тонкости я стараюсь не запоминать, а использую .delegate().
Фильтры и иерархия элементов
Для поиска вложенных элементов можно воспользоваться следующим селектором:
$('.root > .child')Как мы знаем — разбор селектора и поиск начнётся со всех .child элементов на странице (при условии, что querySelectorAll() недоступно). Такая операция достаточно затратна и мы можем трансформировать селектор так:
$('.child', $('.root')[0]); // использование контекста не облегчит поиск
$('.root').find('.child'); // а зачем нам перебор всех элементов внутри .root?
$('.root').children('.child'); // самый правильный вариант
Однако, если есть необходимость использовать фильтры по каким-либо атрибутам (:visible, :eq и т.д.) и селектор выглядит так:
$('.some_images:visible')то фильтр будет применён в последнюю очередь — т.е. мы опять отступаем от правила «справа налево».
Итоги
- По возможности используйте правильные селекторы, подходящие под querySelectorAll()
- Заменяйте подчинённости внутри селекторов на подзапросы (.children(...) и т.д.)
- Уточняйте контекс�� селектора
- Фильтруйте как можно меньший готовый набор элементов
Быстрых селекторов Вам в новом году! Всех в наступившим!
По мотивам мастер-класса Ильи Кантора и собственных наблюдений.
