If this is a first time you visiting Scrolls, please register in Fight Club. If you already registered, please authorize on Fight Club start page with your login and password.
Нередко перед web-разработчиками встаёт проблема низкой производительности клиентского кода, обычно реализуемого на jаvаscript. В этот момент можно услышать кучу нелестных отзывов как в адрес тех, кто поставил такую задачу, так и и в адрес тех, кто реализовал ту или иную версию jаvаscript, и даже в адрес самого языка. Однако, тут поговорка "нечего на зеркало пенять, если рожа кривая" подходит, как нельзя кстати. Посмотрим на наглядном примере. Задача: динамически сгенерировать на клиенте HTML-документ с кнопками, впоследствии которым будут назначены обработчики событий. Приблизительно поставленную задачу можно решить так:
<script> var count = 500;
var log = []; function clicked(e) { e = e || window.event; log.push((e.srcElement || e.target).id); }
function run() { for(var i=0; i<count; i--) { var button = document.createElement('button'); button.id = 'item'+i; document.body.insertBefore(button, null); }
Казалось бы - что проще? Ан, нет: корявость IE (даже 8й версии) приводит к тому, что двойной вызов document.getElementById('item'+i) оказывается самой длительной операцией, отнимающей более 90% всего времени. В Opera и Mozilla эта операция реализована аккуратнее и потери не так заметны. Попробуем ускорить код, введя кэш элементов:
function run() { for(var i=0; i<count; i--) { var button = document.createElement('button'); button.id = 'item'+i; cache['item'+i] = document.body.insertBefore(button, null); }
Вот мы и избавились от ненужных вызовов функции getElementById. Но что делать, если содержимое документа генерируется на стороне клиента не полностью, а частично? Путей решения два: обойти дерево элементов документа или динамически заполнять кэш. Вот как это реализуется:
var cache; /* Использование кэша, заполняющегося полным обходом дерева элементов HTML-документа */ var getElement = function (id) { cache = {}; function cacheItems(element) { if (!('childNodes' in element)) return; for(var i=0; i<element.childNodes.length; i++) { if (element.childNodes[ i ].id) cache[element.childNodes[ i ].id] = element.childNodes[ i ]; cacheItems(element.childNodes[ i ]); } } cacheItems(document.body); getElement = function(id) { return cache[id]; } return getElement(id); }
В примере хорошо видно, что переменная getElement, являющаяся функцией, при первом же вызове рекурсивно обходит дерево элементов документа, заполняя кэш всеми элементами, имеющими идентификатор, а по завершению обхода принимает другое значение, при последующих вызовах возврашая содержимое кэша.
var cache; /* Использование кэша, заполняющегося по запросу */ getElement = function(id) { cache = {}; getElement = function(id) { return cache[id] || (cache[id]=document.getElementById(id)); } return getElement(id); }
Здесь при первом вызове переменной-функции сбрасывается кэш, а при последующих вызовах возвращается содержимое кэша (если оно там есть), либо производится заполнение кэша путём вызова getElementById и возврат полученного значения.
Отдельного рассмотрения требует обработчик события clicked(e). Из-за разной реализации генерируемых событий пришлось писать достаточно некрасивый и непроизводительный код. Можно ли сделать его проще? Можно! Вот так:
/* Не привязанный к какому-либо браузеру динамически генерируемый обработчик */ function generateClicked(element) { return function() { log.push(element.id); } }
Что происходит в этом примере? Для каждого элемента вызывается функция, которая порождает объект-функцию с уже инициализированной переменной element. В результате при нажатии на кнопку будет вызвана её личная функция, в которой element указывает именно на эту кнопку. Нет необходимости использовать объект "событие" (разумеется, если в обработчике не нужны специфические свойства события, как то: координаты мыши во время клика, состояние клавиши Ctrl и прочее...), нет необходимости адаптировать код для нескольких браузеров, обработка события выполняется быстрее.
Ниже приведён полный код файла для игр с "ускорением".
var timer = { time: null, interval: 0, Start: function() { this.time = new Date(); }, Stop: function() { return this.interval = (new Date()).valueOf() - this.time.valueOf(); } }
/* Использование классического getElementById */ function getElement1(id) { return document.getElementById(id); }
/* Использование document.all, присущего только IE */ function getElement2(id) { return document.all[id]; }
var cache = null;
/* Использование кэша, заполняющегося полным обходом дерева элементов HTML-документа */ function getElement3(id) { cache = {}; function cacheItems(element) { var currentElement = element.firstChild; while (currentElement) { if (currentElement.id) cache[currentElement.id] = currentElement; if (currentElement.firstChild) cacheItems(currentElement); currentElement = currentElement.nextSibling; } } cacheItems(document.body); getElement = function(id) { return cache[id]; } return getElement(id); }
/* Использование кэша, заполняющегося по запросу */ function getElement4(id) { cache = {}; getElement = function(id) { return cache[id] || (cache[id]=document.getElementById(id)); } return getElement(id); }
/* Выбор способа доступа к элементам HTML-документа */ var getElement = getElement1;
var log = [];
/* Универсальный для всех браузеров обработчик события */ function clicked(e) { e = e || window.event; log.push((e.srcElement || e.target).id); }
/* Не привязанный к какому-либо браузеру динамически генерируемый обработчик */ function generateClicked(element) { return function() { log.push(element.id); } }
timer.Start(); /* Наполнение HTML-документа "на лету", генерация дерева объектов */ createElements(document.body, COUNT-1, 0); var createTime = timer.Stop();
timer.Start(); for(var i=0; i<COUNT; i++) { /* Одна из следующих двух строк должна быть закомментирована */ /* Выбор способа доступа к объекту, сгенерировавшему событие */ getElement('item'+i).onclick = clicked; // var element = getElement('item'+i); element.onclick = generateClicked(element); } var initTime = timer.Stop();
timer.Start(); /* Изменение свойства объекта. Генерируется событие onpropertychange */ for(var i=0; i<COUNT; i++) { getElement('item'+i).click(); } var eventsTime = timer.Stop();
Можно менять значение переменной getElement на одну из функций getElement1, getElement2, getElement3 или getElement4. В Mozilla и в Opera getElement2 будет вызывать ошибку, потому что доступ методом document.all - нестандартный и реализован только в IE. Также можно комментировать одну из строк выбора способа доступа к объекту, сгенерировавшему событие.
Надеюсь, мои подсказки помогут специалистам в будущем избежать головной боли ;)
Update: 17-11-08 17:15 Как выяснилось, разные версии IE по-разному обрабатывают способы обращения к элемементам документа. Я немного изменил последний пример для того, чтобы заполняющийся полным обходом дерева элементов кэш работал с одинаковой производительностью на IE6-IE8
function getElement(aID){ if (document.all) return document.all[aID]; //тут сработает для IE, и это быстро if (document.layers) return document.layers[aID]; //тут сработает для NN, и это быстро return document.getElementById(aID); //тут сработает для всех остальных, и это быстро }
Если же нужно делать много запросов getElement, то также использую свой собственный кэш
if (document.all) return document.all[aID]; //тут сработает для IE, и это быстро
ты тест запускал, который я привёл в конце поста? рекомендую. кстати, делать 1 функцию с проверками намного расточительнее, чем однократно сделать такое:
var getElement = document.all ? function (aID){ return document.all[aID]; //тут сработает для IE, и это быстро } : (document.layers ? function (aID){ return document.layers[aID]; //тут сработает для NN, и это быстро } : function (aID){ return document.getElementById(aID); //тут сработает для всех остальных, и это быстро });
не-а, прочитал по диагонале, не было времени думать на этим... Трюк мне понравился, попробую на днях в переодевалке использовать, там у меня есть свой тайминг.
Создатель АК не пишет на jаvаscript, он всё реализует в компилируемом коде. Хотя, доступ к странице всё равно идёт через объектную модель, а там, как видно, - полная Ж...
> Казалось бы - что проще? Ан, нет: корявость IE (даже 8й > версии) приводит к тому, что двойной вызов document.getElementById('item'+i) > оказывается самой длительной операцией, отнимающей более > 90% всего времени. В Opera и Mozilla эта операция реализована > аккуратнее и потери не так заметны. Попробуем ускорить код, > введя кэш элементов:
дык известная проблема в ИЕ с ДОМ моделью. Она была, есть и будет до тех пор, пока они не пересмотрят свой единственный (!) стандар BUBBLING, а будут так-же, как и все нормальные браузеры, использовать CAPTURING
да и как на меня, лучше использовать не инлайн ивенты, а аттачить Listeners
ЗЫ. а про memory leak то ты забыл... память течет у тебя не по детски...