Ускоряем селекторы в 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).
Посчитаем общую сложность 10*3+10*2+3*1=53. Попробуем поменять местами поставив самые простые на перед — 10*1+3*2+1*3=19, получается в 2,5 раза меньше.

Отсюда вывод, что в примере селекторы лучше всего поменять местами в противоположном порядке — :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

А это значит про предыдущие расчеты нужно немного подкорректировать и теперь самым ‘тяжелым’ является селектор класса.

23 Комментарии “Ускоряем селекторы в jQuery

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

    Да, полезное замечание, касаемо не только jQuery или JavaScript, но и других языков и технологий!

  2. сори букаф так много что глаза болят … я проснулся тока что (

  3. Глаз болит от такого монолитного текста, вот смотри разберем:
    >>>

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

    Итак, считаем:
    * первый цикл обойдет все 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 натянут тут они все видимы но в реальной ситуации когда выбираются все абзацы текста, все может быть по другомую.

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

  4. Ты хочешь попасть в индекс по албанским запросам?)
    У тебя ошибок туча в тексте :)

  5. Ну собственно Америки вы не открыли.. в любом руководстве по SQL сказано что в запросах где в предложении Where много OR лучше всего ставить первым предикатом то что чаще всего встречается (у вас в статье как раз наоборот — но сути дела это не меняет), т.к. все остальные условия просто не будут проверяться и запись попадёт в выборку.
    И ещё я не очень понял — «дадим ему модификатор 3» — почему? почему не 5 или 7? — если за этим кроются какие-то изыскания — ссылочку пожалуйста — лично мне будет очень интересно почитать.
    Вобщем нового немного — но за напоминание спасибо.

  6. @Steward
    Конечно я ничего не придумал (а жаль), но многие люди и этого не понимают, даже не новички, а все потому что нет нормальной документации на русском (да и на англиском). В статье я пытался объяснить механизмы почему так происходит, и думаю у меня получилось.

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

  7. 
    var cls = tmp1[i].className.split('/\s+/')
    	for (var c in cls)
    		if (cls[c] == "myClass")
    
    
    if( -1 !== (" " + tmp1[i].className + " ").indexOf( " " + "myClass" + " ") )
    
  8. дело в том что я этот код не придумал а взял из jquery поэтому намек проигнорирован

  9. CTAPbIu_MABP :
    @Steward
    Конечно я ничего не придумал (а жаль), но многие люди и этого не понимают, даже не новички, а все потому что нет нормальной документации на русском (да и на англиском). В статье я пытался объяснить механизмы почему так происходит, и думаю у меня получилось.

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

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

  10. да, я тоже думал что все делается в один проход, но к сожалению… чтобы подтвердить это проведем эксперимент. Выберем все четные и нечетные option из примера, по идеи их должно быть 10

    
    alert($("option:even:odd").length); // 2
    

    но на самом деле их только два потому что сначала были выбраны нечетные (5 штук) а потом из них были выбраны четные (2 штуки)

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