Delayed DOM Manipulations

31 Август 2010
Чебурашка решил начать разговор издалека. - Солнышко светит, травка зеленеет! - сказал он. - А нам вот так нужны гвозди! Не дадите немножко? - Это не травка зеленеет, - ответил кладовщик.- Это краску пролили. А гвоздей нет. Каждый ящик на учете.

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

Самая простая вещь которая может быть это вынос за пределы цикла и отложенная работа с DOM деревом.


var lis = [1,2,3,4,5,6];
$(lis).each(function(i, e){
	$("ul#my").append("<li>"+e+"</li>")
});

Превращаеться в


var lis = [1,2,3,4,5,6], html = "";
$(lis).each(function(i, e){
	html = "<li>"+e+"</li>";
});
$("ul#my").html(html);

Кароче самая тривиальная и банальная вещь из существующих. Но писать append внутри цикла как-то логичнее и приятнее ну, по крайней мере, для меня. И решил я захачить жуквери так что бы можно было писать как хочеться а работало как надо. Вот тут собственно и уместно предисловие, пишем append внутри each и каждая миллисекунда на счету.

После дня ковыряния у меня родился вот такой код:


(function($){

/*
 * @author CTAPbIu_MABP
 * @email CTAPbIuMABP@gmail.com
 * @link http://mabp.kiev.ua/2010/08/31/delayed-dom-manipulations/
 * @license GPL
 */

	var data = {},	// тут список всех элементов и отложенных операций над ними
		isOuterLoop = true, // определяет самый внешний вложенный цикл
		supportedFunctions = [ // список переопределяемых методов
			"append",
			"prepend",
			"before",
			"after"
		];
	
	/**
	 * Складывает аргументы оригинальных методов в data
	 * @param funcName {String} имя оригинального метода 
	 * @param args {array-like object} аргументы оригинального метода
	 * @return {jQuery} возвращяет jQuery для продолжения цепочки
	 */
	function delay (funcName, args){
		var i = $.inArray(this, data[funcName].elt),
			args = $(args).toArray(); // приводим аргументы к массиву
		if (i > -1){
			data[funcName].tmp[i] = data[funcName].tmp[i].concat(args);
		}else{
			data[funcName].elt.push(this);
			data[funcName].tmp.push(args)
		}
		return this;
	}
	
	// создаем место для хранения данных
	// и обертки ко всем поддерживаемым методам 
	$(supportedFunctions).each(function(i, funcName){
		data[funcName] = {tmp : [], elt : []};
		$.fn[funcName+"Delayed"] = function(){
			return delay.call(this, funcName, arguments)
		}
	});
	
	// создаем обертку для each
	$.fn.eachDelayed = function(){
		var ret, isOuter = isOuterLoop;
		isOuterLoop = false;
		ret = $.fn.each.apply(this, arguments); // выполняем оригинальный each
		if (isOuter){ // после самого вернего вложенного цикла
			$.each(data, function(funcName, funcParam){ // обходим все данные
				$(funcParam.elt).each(function(i){ // и выполняем все оригинальные методы
					$.fn[funcName].apply(funcParam.elt[i], funcParam.tmp[i]);
				});
				data[funcName] = {tmp : [], elt : []}; // обнуляем данные 
			})
			isOuterLoop = true;
		}
		return ret;
	}
	
})(jQuery);

Ну что ж погнали, посмотрим что он делает. А делает он обертку для методов из массива supportedFunctions, грубо говоря AOP. На самом деле переопределенные методы не выполняют вставку в DOM дерево, а только вызывают функцию delayed которая складывает аргументы оригинальных методов в массив data. После конца цикла все накопленные данные проверяются и выполняются все отложенные методы.

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

Теперь я думаю надо объяснить на чем основан хак. Так сложилось что все четыре метода из массива supportedFunctions используют метод domManip (line 4263) -> buildFragment (line 4355) -> clean (line 4417) передавая все свои аргументы, для построения DocumentFragment одним махом. Вот этим-то я и воспользовался, в самом внешнем цикле я один раз вызываю оригинальный метод с кучей параметров.

Еще два слова о том как пользоваться этим чудом


$(document).ready(function(){
	var lis = [1,2,3,4,5,6],
            ul1 = $("ul#my1"),
            ul2 = $("ul#my2");
	
	$(lis).eachDelayed(function(i, e){
		ul1.beforeDelayed("<div>"+e+"</div>");
		ul1.appendDelayed("<li>"+e+"</li>");
		ul2.prependDelayed("<li>1+"+e+"</li>", "<li>e+"+e+"</li>");
		$(lis).eachDelayed(function(i, e){
			ul2.appendDelayed("<li>2+"+e+"</li>");
		});
	});
	
});

Результат выполнения кода приводить не буду, там ничего красивого, зато работает. Все методы before, append и prepend выполняются по одному разу при окончании внешнего цикла.

Хотел еще сделать поддержку для всяких wrap, но что-то там как-то грустно все, они не используют domManip.

Скачать можно тут DOWNLOAD LINK

  1. Костег
    31 Август 2010 в 16:03 | #1
    и риальне прирост производительности есть или это байтдроч очередной?
  2. 31 Август 2010 в 16:30 | #2
    не байтодроча нет. это только для личного удобства, хочешь можешь писать так как во втором примере кода написано, хочешь можешь через delayed
Комментирование отключено.