Доклад с coffee’n’code (дополненная)

Вот это я должен был рассказывать на код и кофе, выкладываю дабы реабилитироваться после того как забыл слайды.

1. Предисловие

Я бы хотел начать эту статью с цитаты Джона Ресига (John Resig), основного идеолога jQuery и корпорации Mozilla.

Я думаю, вы переоцениваете полезность jQuery. Используя его пользователи теперь ограничены селекторами которые могут использовать (они могут использовать, только то, что предоставляет браузер и полагаться на милость кросс-браузерных багов) и это большая проблема. Не говоря уже о том что jQuery призывает к смешиванию html разметки, css и javascript.

В этой статье я хочу показать что в умелых руках и хуй — балалайка.

Итак начнем настраивать наш… инструмент с самого начала.

2. Selectors

Начну, пожалуй, с самого начала с функции $, она принимает два параметра: первый — селектор, второй — контекст. Хотя контекст обычно опускают я в последствии покажу как им грамотна пользоваться.

2.1. Простой селект

Самый простой вариант это выбор по id, имени тега и имени класса.


$("#id")
$("tag")
$(".class")

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


document.getElementById("id");

Поскольку предполагается что id уникальный то поиск проходит очень быстро, и если на странице есть два элемента с таким id то найден будет только первый. Хот я в IE и тут накосячили и до 7 версии включительно в случаи отсутствия элемента с таким id он вернет элемент у которого совпадает атрибут name.

Во втором случаи тоже всё относительно просто:


document.getElementsByTagName("tag");

Получили все ноды с таким именем из документа и все готово. И на удивление никаких косяков, если не учитывать что при запросе getElementsByTagName(«*») IE вернет и комментарии тоже.

В третьем случаи если есть возможность работу перехватывает:


document.getElementsByClassName("class");

(Табличку поддерживаемости этой функции смотрите на quirksmode.org)

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


var nodes = document.getElementsByTagName("*"), result = [];
for (var i=0; i<nodes.length; i++){
	if(" " + (nodes[i].className || nodes[i].getAttribute("class")) + " ").indexOf("class") > -1)
		result.push(nodes[i]);
}

Какой метод использовать определяется в самом начале при подключении библиотеки.

2.2. Селекст через querySelectorAll

Но это все подходит только для элементарных селекторов. А на практике приходится обычно использовать намного более сложные конструкции. И для них в современных браузерах FireFox 3.0, Safari 3.2, Opera 9.5 в том числе и в IE8 появились функция querySelector и querySelectorAll. Они соответственно предназначены для поиска одной или нескольких нод по CSS3 селекторам. Если браузер клиента поддерживает эту функцию то все о чем мы говорили в прошлом пункте — отпадает и поиск происходит через querySelectorAll.


$("#id .class tag")

В лучшем случаи селектор будет обработан именно querySelectorAll потому что он написан по правилам CSS3. Но так можно не со всеми селекторами, jQuery поддерживает ряд селекторов которые не входят в CSS3 такие, например, как :visible.


$("#id .class tag:visible")

Такой селектор выдаст ошибку в функции querySelectorAll и селектор будет перенаправлен в поисковый движок Sizzle где строка будет разбита на простые селекторы и превратится по сути в несколько разных поисков в котором каждым следующим контекстом является предыдущий селектор.


$(document).find("#id").find(".class").find("tag").filter(":visible")

Скорость этого метода поиска напрямую зависит от величины DOM дерева, чем оно больше — тем медленнее, но её можно значительно увеличить написав селектор раздельно.


$("#id .class tag").filter(":visible")

При этом querySelectorAll выберет все ноды, а Sizzle разберется с «:visible»

По поводу псевдо-селекторов тоже кстати очень интересный вопрос: CSS3 поддерживает несколько видов псевдо-классов такие как :nth-of-type/:nth-child/:parent/:not/:checked , jQuery имеет свою реализацию этих селекторов для браузеров не поддерживающих querySelectorAll или для браузеров в которых querySelectorAll не поддерживает данный селектор (табличку поддерживаемости селекторов смотрите на quirksmode.org), но эта реализация иногда отличается. Для примера возьмем псевдо-класс :nth-of-type и выберем все четные дивы а из них все нечетные.


document.querySelectorAll("div:nth-of-type(even):nth-of-type(odd)") // Safari/FireFox:0  IE/Opera:N/A
$("div:nth-of-type(even):nth-of-type(odd)"); // Safari/FireFox:0  IE/Opera:All
$("div:even:odd"); // All: вернут 1,5,9 дивы 

Первых два примера работают одинаково и вернут либо 0, если отработала функция querySelectorAll (это касается первого примера), либо все элементы, потому что их обработал Sizzle (это особенность реализации выражения «:»), а третий вернет 1, 5, 9 и тд. элементы, а значит селекторы отрабатывали в три прохода, сначала из всего дом дерева были выбраны все дивы, потом из них были выбраны все нечётные, а потом из оставшихся были выбраны все чётные.

jQuery так же имеет набор псевдо-селекторов который не входят в CSS3 и обслуживаются только Sizzle’ом :visible/:animated/:input/:header. Их лучше выделять отдельно так как они могут сильно замедлить выборку. Так например было с селекторами :visible/:hidden в версии 1.2.6, для того чтобы узнать видимый элемент или нет надо было подняться до самого верха по DOM-дереву проверяя атрибуты display и visible каждого родителя. [пруфлинк]


$("div").filter(":visible")

Псевдо-классы используемые для поиска элементов формы такие как :radio тоже имеют некоторое преимущество если не используется querySelectorAll в противном случаи CSS3 селектор input[type=radio] работает быстрее

2.3. Сложенный селект

Сложенный селект это когда нам надо выбрать группу из двух или более разных селекторов, например все дивы у которых класс равен A, B и C

Это можно сделать двумя способами


$(".a,.b,.c")

выбрать все сразу


$(".a").add(".b").add(".c")

или по одному.

При этом если задействована функция querySelectorAll то первый быстрее второго в четыре раза, а если нет то второй в два раза быстрее первого.

Если уже заговорили про классы их можно искать как любые другие атрибуты например если надо найти все классы имена которых начинаются на «my» можно сделать так


$("[class^=my]")

а не городить логику с использованием add, тем более что такой способ поддерживается querySelectorAll. [пруфлинк]

2.4. Неправильный селект в контексте

На сайте tvidesign.co.uk в одной очень популярной статье «Improve your jQuery — 25 excellent tips» которую перевели на русский и перепечатывают где только не лень, начиная с Хабра, написано что селект лучше делать в контексте и приведен вот такой пример:


$('#listItem' + i, $('.myList'))

Я не спорю что Jon Hobbs-Smith иммет неплохое портфолио но тем не менее он ничего не знает о jQuery. Да и вообще статьи начинающиеся на «100-500 советов» попахивают несостоятельностью автора излагать свою мысль последовательно, не говоря уже о том что все эти советы Капитан Очевидность уже давно нам дал в мануалах, факах и руководствах.

Рассмотрим пример подробнее: контекст это то, где ищут селектор, значит пример можно переписать в более наглядную но менее читаемую форму


$($(".myList")).find("#listItem")

При этом контекст от первого поиска будет являться document.


$($(".myList",document)).find("#listItem")

Еще раз перепишу согласно формуле.


$($(document).find(".myList")).find("#listItem")

И наконец раскроем скобки


$(document).find(".myList").find("#listItem")

Что же получается мы выполняем дорогостоящую операцию поиска по имени класса (по всему DOM-дереву в худшем случаи) для того чтобы упростить и без того самую простую операцию поиска по id. БРЕД!

2.5. Правильный селект в контексте

Правильно делать с точностью да наоборот. В контекст надо указывать id элемента.


$(".class",$("#id"))

Только вот я не понимаю зачем вообще в контекст передавать jQuery объект вполне достаточно.


$(".class","#id")

Это можно переписать как.


$("#id").find(".class")

Можно еще больше ускорить работу, если искать вот таким способом:


$(document.getElementById("id")).find(".class")

Но это ИМХО будет уже плохим тоном. Хотя поэкспериментировать интересно, что если вместо getElementById взять querySelectorAll


$("div",document.querySelectorAll("#id"))

Это примерно тоже самое что и


$("div",[document.getElementById("id")])

Не прироста производительности ни красоты кода из этого не получить, поэтому советую в контекст передавать что-то простое вроде id или при использовании псевдо-селекторов обрабатываемых Sizzle’ом передавать их в селектор а все остальное в контекст


$(":visible","input[type=checkbox]")

Ну раз уже заговорили о псевдо-селекторах то


$(":checkbox")

быстрее чем


$("input[type=checkbox]")

без использования querySelectorAll и наоборот.

2.6. Cложный селест

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


$("#id > div")

Но если выборка уже есть то будем использовать ее как контекст. Как мы уже выяснили поиск в контексте происходит при помощи функции find


$("#id").find("> div")

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


$("#id").children("div")

Есть еще ряд функций поиска и манипуляций которых стоит избегать без крайней необходимости это: find, closest, wrap, wrapInner, replaceWith, clone. Заметте wrapAll сюда не входит. [пруфлинк]

3. Cache

3.1. Внутреннее кеширование

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

Ситуация такая, вы работаете со списком у вас если один из элементов li для того чтобы получить все элементы включая текущий надо выбрать всех братьев (все у кого родитель это родитель текущего) этого элемента и добавляем его самого


$("#id").siblings().add("#id")

Так как он — прошлый элемент, с которым работали в цепочке вызовов, мы можем взять его из кеша.


$("#id").siblings().andSelf()

Конечно в данном конкретном случаи быстрее было бы сделать


$("#id").parent().children()

Потому что siblings это и есть выбор всех детей родителя. Но я думаю что принцип использования этот пример иллюстрирует нормально.

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


var elt = $("#id");
elt.children().css({/**/})
elt.click();

Можно после работы с детьми вернуться обратно к родителю и работать с ним дальше


$("#id").children().css({/**/}).end().click()

3.2. Кеширование селекторов

Поскольку кеш так слабо развит, селекторы нужно кешировать вручную. Возьмем например вот такой код


for(var i=0;i<1000;i++)
	$("ul").append("<li>"+i+"</li>")

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


var elts = $("ul");
for(var i=0;i<1000;i++)
	elts.append("<li>"+i+"</li>")

3.3. Буферизация

Но и это еще не все, этот код можно заставить работать еще быстрее. Каждый раз делая append мы заставляем обновиться DOM-дерево и заставляем браузер перерисовать страницу. Этого можно избежать придерживая вставку в DOM-дерево.


var str = "";
for(var i=0;i<1000;i++)
	str += "<li>"+i+"</li>"
$("ul").html(str);

Дело в том что функции для работы с DOM-деревом у jQuery самые "тяжелые" [пруфлинк]. Это просто объясняется. Все html ноды на которые повешены события через jQuery имею в себе атрибут с объектом jQuery. При удалении этих нод нужно следить чтобы не было утечек памяти и удаляет эти атрибуты перед удалением ноды. В результате функции html и text вызывают функции полной очистки и только потом вставки нового содержимого.


jQuery(DOMElement).empty().append(text)

Функция empty выбирает все ноды и по очереди удаяет


jQuery(DOMElement).children().remove()

А функция remove уже заботится чтобы из элементов были удалены все дополнительные данные и события

Джон Ресиг (John Resig) утверждал что знает способ быстро удалить все это и что улучшит эти методы, но что-то воз и ныне там. Поэтому будем ждать улучшенных функций уже в jQuery 1.4

3.4. Создание "на лету"

Прошлый пример на самом деле был нужен, для того что бы я подобрался поближе к интересненькому. Часто приходится создавать какие-то вспомогательные дивы, естественно меня заинтересовал самый эргономичный способ это сделать. Казалось бы в чем проблема кинул кусок html кода в и jQuery сама все сделала. Возьмем самый простой и банальный пример надо создать пустой див.


$("<div></div>")

или


$("<div/>")

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


$("<div>text</div>")

А не создавать а потом добавлять текст


$("<div/>").text("text")

Но это не каcается создания атрибутов, для них используются намного более "легкие" функции attr/css/addClass [пруфлинк], вот тут то и имеет смысл вместо


$("<div style='background:red;'/>")

писать


$("<div/>").css({background:'red'});

это даст небольшой, но выигрыш.

4. Events

4.1. Множественные события

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


$(window).bind("resize load",null,function(){
	$("#id").css({width:document.clientWidth})
});

Только при этом не забываем что IE8 ведет себя не корректно и при загрузке страницы сначала происходит событие resize а только потом load.

Тоже самое корректно и в обратную сторону.


$(window).unbind("resize load");

Но это не работает в версии 1.2.6, точнее это работает только с именованными функциями а с анонимными не работает, их надо удалять по одной.

.

4.2. Одно событие на много элементов

Если случается повесить события на длинный список.


var ul = $("<ul/>");
for(var i=0,j=1000;i<j;i++)
	$("<li>"+i+"</li>").click(function(e){
		alert(this.innerHTML);
	}).appendTo(ul);

ul.appendTo("body");

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


var str = "";
for(var i=0,j=1000;i<j;i++)
	str += "<li>"+i+"</li>";
$("<ul/>")
	.append(str)
	.click(function(e){
		alert(e.target.innerHTML);
	})
	.appendTo("body");

37 Комментарии “Доклад с coffee’n’code (дополненная)

  1. оффтоп: диз так себе, заголовки портят все… отступов не хватает и тп

  2. и вот здесь

    
    .post .content h4 {
    color:#008800;
    margin-bottom:10px;
    }
    

    сотри упоминание про маржин

  3. отец, ты супер!
    перевести на инглиш т порвать тмошних гуру на Digg-ах и смэшныхмагазинах!

  4. >>В этой статье я хочу показать что в умелых руках и хуй — балалайка.
    Было бы забавно посмотреть на реакцию зала, если б ты так и начал доклад с этих слов)))
    Поздравляю, статья отличная! Валяй, переводи на инглиш.

  5. Может и сказал, да только говорил он тихо в начале, так что я в первом ряду не разобрал.

  6. Статейка объемная получилась, есть что почитать.
    Добавить бы live events ;)

  7. @Andrey
    Да надо дописать, но я сегодня весь день над другой статейкой работал. завтра займусь.

  8. Хорошая статья ) хоть я и не занимался особо оптимизацией селекторов ) некоторые моменты меня порадовали ) теперь все будет работать быстрее ) спасибо )

  9. Спасибо за статью, очень нужная инфа. Вопрос по поводу очистки памяти: если я вешаю на d2 какое-то событие, а потом делаю $(‘#d1’).empty() — событие будет удалено из памяти?
    Вообще, по поводу очистки памяти в jQuery вопросов много, есть что почитать на эту тему на русском?

  10. Теги порезались.. там был div с id=d1 и в него вложен div с id=d2

  11. Не знаю кто такой Jon Hobbs-Smith и какое у него портфолио, но в «Неправильный селект в контексте» Вы в корне не правы. Насколько я понимаю Вы вырвали пример из контекста, там должен был быть цыкл, и выбирались елементы с id у родителя по class. Не уверен что 100 выборок по id будет быстрее выборки контекста по класу и потом выбора из нее по id. У вас же все написаное после примера не имеет к ниму никакого отношения, хотя и логически верно.
    Прежде чем поливать когото грязью, разберитесь в сути вопроса ;) А за статью спасибо, много раз видел собраные вмести твики для jQuery, но с хорошим пояснением принципа почему именно так не встречал.
    Спасибо

  12. Из вышесказанного днлаю вывод что ты не читал исходную статью, не проводил тестов, а просто говоришь первое что пришло в голову.
    Да и я никого грязью не поливаю, мне просто не нравится что кто-то написал херню, а ее потом теражируют не вдумываясь. Круто конечно что люди выучили инглыш и хотят поделиться с незнающими, но свою голову тоже надо иметь.

  13. @CTAPbIu_MABP
    Притензия была конкретно в следующем:

    
    $('#listItem' + i, $('.myList'))
    

    как из кода могло получиться

    
    $(".class",$("#id"))
    

    ?
    Хотя, возможно я не прав, и это просто следующий топик… Тогда я не прав, и приношу извинения. Но тогда нужно подумать о более ощутимом разделении топиков

  14. Ну все правильно, первый кусок кода относится к статье Джона, а второй к моему объяснению почему первый неправильный. А между ними стоит большой зеленый заголовок, который видно даже под IE6 ;)

  15. @CTAPbIu_MABP
    Ну, это ваш блог, и Вам виднее, как и что должно тут оформляться. Но тем не менее, это привело к конфузу в моем случае.
    «ослика» я не использую и не могу… у меня Safari. Если «большой зеленый» — это в полтора раза выше обычного текста и жирный, — то все впорядке.

  16. блог конечно мой, но он для людей, и если людям не удобно я это, конечно, исправлю. но я только что открыл 4й сафари под винду и вижу свои зеленые заголовки. можно мне плз скрин где их не видно?

  17. Заголовок видно, если знать что это заголовок… Возможно им нужна нумерации и/или чтото еще…
    Ответил на мыло информера из блога.

  18. Теперь не ошибусь з заголовком.
    А в моем коментарии #25 подсветка кода вставлена вручную или блог сам распознал код?

  19. к сожалению в ручную
    я пытался хачить вордпресс чтоб он сам пускал теги [pre][code class=»javascript»] но потом обновился и снова хачить стало лениво

  20. все равно приятно, на моей памяти, это первый блог в котором в коментах код выделяется, по крайней мере среди тех за которыми я слежу.

  21. ну а че глаза ломать? а мне всего пару движений сделать и всем приятно :)

Комментарии закрыты