Ускоряем селекторы в jQuery
Когда я вчера начинал писать эту статью я хотел написать что-то типа "селекторы для продвинутых" небольшое руководство по сложным выборкам, но как то так получилось, что я отклонился от темы в сторону объяснения внутренних механизмов jQuery и получилось что-то средние между "селекторами для продвинутых" и "перфомансом селекторов", что тоже не плохо. Объяснять как работают селекторы я буду на простейшем примере, который лучше смотреть в FireFox 3.1 или IE8:
<style>
.myClass{
color:red;
}
</style>
<select name="myName">
<option value="101">101</option>
<option value="102">102</option>
<option value="103">103</option>
<option value="104">104</option>
<option value="105">105</option>
<option value="106" class="myClass">106</option>
<option value="107" class="myClass">107</option>
<option value="108" disabled="disabled">108</option>
<option value="109" disabled="disabled">109</option>
<option value="110" disabled="disabled" class="myClass">110</option>
</select>
Теперь напишем селектор который выберет все элементы которые нам видны, которые имеют класс myClass и которые неактивны.
jQuery().ready(function($){
// выбираем все option
var o = $("select[name=myName] option");
// фильтруем
o.filter(":visible.myClass:disabled");
});
Работает, но это не оптимальный селектор, даже если он занимает минимум символов при написании, он работает медленно. Надо понимать как это все работает и какая операция быстрее. Итак попробуем разобрать: если бы все было прекрасно и браузер поддерживал функцию document.querySelectorAll, jQuery бы передал ей селектор, но у нас не та ситуация, я специально создал такие условия чтобы querySelectorAll ничего не ускорил, ну или мы используем какой-то старый браузер который не поддерживает этот метод. Итак у нас три части селектора :visible + .myClass + :disabled , (и хотя querySelectorAll понимает второй селектор, jQuery все равно придется обрабатывать первый и третий) примерный код выглядел бы так, только намного сложнее.
// Этот код работает идентично тому как работает jQuery
// выбираем все option без jQuery
var o = document.querySelectorAll("select[name=myName] option"),
tmp1 = [], tmp2 = [], result = [];
function isVisible(obj){
if (obj == document)
return true;
if(!obj)
return false;
if(obj.style.display !== "none" && obj.style.visibility !== "hidden")
return isVisible(obj.parentNode);
}
// фильтруем
for (var i=0; i<o.length; i++){
if(isVisible(o[i]))
tmp1.push(o[i]);
} // 10
for (var i=0; i<tmp1.length; i++){
var cls = tmp1[i].className.split('/\s+/')
for (var c in cls)
if (cls[c] == "myClass")
tmp2.push(tmp1[i]);
} // 3
for (var i=0; i<tmp2.length; i++){
if(tmp2[i].getAttribute("disabled"))
result.push(tmp2[i]);
} // 1
alert(result.length); // 1
Конечно есть механизмы оптимизации, но есть и много другого кода поэтому от него отстранимся и будем решать нашу задачу. Задача очень простая надо посчитать общее количество циклов и трудозатраты на каждом цикле.
Итак, считаем:
- первый цикл обойдет все 10 элементов и трудазатраты у него возрастают пропорционально величине DOM дерева (дадим ему модификатор 3)
- второй менее трудозатратен и его величина пропорциональна количеству классов у элемента (модификатор 2)
- и наконец третий цикл совсем простой с минимальным количеством трудозатрат (модификатор 1).
Отсюда вывод, что в примере селекторы лучше всего поменять местами в противоположном порядке - :disabled.myClass:visible.
Конечно пример про :visible натянут тут они все видимы но в реальной ситуации когда выбираются все абзацы текста, все может быть по другому.
В общем это я все клоню к тому, что для того чтобы селекторы быстро работали надо вначале писать самые простые, отсекающие как можно больше элементов, а потом сложные чтобы они фильтровали как можно меньше элементов.
21.02 UPD: Несколько часов назад вышла jquery-1.3.2.js . В release note сказано что отремонтированы селекторы :visible/:hidden и действительно сейчас они полностью переписаны и не зависят от величины дом дерева.
vae element = document.getElementById("id")
if(!element.offsetWidth && !elem.offsetHeight)
// hidden
else
// visible
А это значит про предыдущие расчеты нужно немного подкорректировать и теперь самым 'тяжелым' является селектор класса.
По поводу модификаторов могу сказать что они были взяты просто от количества составляющих. Потому что для того чтобы там стояло более менее значащее число надо было оговорить изначальный размер DOM дерева и количество классов у элементов, и все равно бы это ничего не дало потому что на практике все совсем иначе.