Ускоряем селекторы в jQuery

Когда я вчера начинал писать эту статью я хотел написать что-то типа «селекторы для продвинутых» небольшое руководство по сложным выборкам, но как то так получилось, что я отклонился от темы в сторону обьяснения внутрених механизмов jQuery и получилось что-то средние между «селекторами для продвинутых» и «перфомансом селекторов», что тоже не плохо. Объяснять как работают селекторы я буду на простейшем примере, который лучше смотреть в FF3.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).
Посчитаем общую сложность 10*3+10*2+3*1=53. Попробуем поменять местами поставив самые простые на перед — 10*1+3*2+1*3=19, получается в 2,5 раза меньше.

Отсюда вывод, что в примере селекторы лучше всего поменять местами в противоположном порядке — :disabled.myClass:visible.

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

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

Все комментарии