Реализация интерфейса ElementTraversal

    Достаточно много браузеров (Opera 9.6, Google Chrome 2, Safari 4, Firefox 3.5) обзавелись поддержкой весьма удобного интерфейса ElementTraversal, который позволяет перемещаться по DOM-дереву, игнорируя текстовые узлы. В этих браузерах для каждого элемента стал доступен следующий набор новых getter'ов:
    • firstElementChild — первый дочерний элемент;
    • lastElementChild — последний дочерний элемент;
    • nextElementSibling — следующий соседний элемент;
    • previousElementSibling — предыдущий соседний элемент;
    • childElementCount — количество дочерних элементов.
    Не вдаваясь в подробности реализации этих getter'ов и опираясь на слова из спецификации:
    The ElementTraversal interface is a set of read-only attributes
    будем называть их далее атрибутами.

    Уточним: элементами называются узлы DOM-дерева, имеющие свойство nodeType равное 1; элементам соответствуют теги.

    Использование этих атрибутов должно повысить производительность JavaScript-приложений при перемещении по DOM-дереву, т. к. отпадает необходимость проверять в цикле nodeType узлов и использовать дополнительные функции-обертки. А атрибут childElementCount позволяет узнать, есть ли вообще дочерние элементы у текущего узла, в отличие от практически бесполезного метода hasChildNodes. Конечно, проверить наличие дочерних элементов можно следующим образом:
    node.getElementsByTagName("*").length
    Но такой способ слишком расточителен, встроенный атрибут childElementCount теоретически должен работать гораздо быстрее.

    Все это конечно хорошо и интересно, но что же делать со старыми браузерами, которые не поддерживают интерфейс ElementTraversal? Очевидно, придется написать дополнительные функции для каждого атрибута. Их реализацию вы можете посмотреть в статье «Быстрый поиск DOM-элементов», хотя, думаю, для многих не составит труда написать такие функции самому, ничего сложного в этом нет.

    А теперь самое интересное, в Internet Explorer 8 появилась возможность создавать эти самые getter'ы и setter'ы. Странно только, почему ни в одном обзоре о нововведениях IE8 не прозвучало об этом мощном и полезном механизме. В JScript создать getter или setter теперь можно с помощью метода defineProperty встроенного объекта Object. Вспоминаем, что подобная возможность, посредством методов __defineGetter__ и __defineSetter__, изначально была в браузерах на основе Gecko, как оказалось уже и в Safari 3, и в Opera 9.6 внедрили этот механизм.

    Итак, имея возможность создавать getter'ы, мы можем внедрить поддержку интерфейса ElementTraversal в Internet Explorer 8, Mozilla Firefox 2+ и Safari 3+, ну а Opera 9.6 и так его поддерживает.

    Осталось написать код:
    // Создаем новый элемент для дальнейших проверок<br>var element = document.createElement("div");<br><br>// Проверяем, что браузер не поддерживает ElementTraversal<br>if(typeof element.firstElementChild == "undefined") {<br><br>  // Создаем объект с набором методов<br>  var ElementTravrsal = {<br><br>    // Поиск первого дочернего элемента<br>    firstElementChild: function() {<br>      // Получаем первый дочерний узел<br>      var node = this.firstChild;<br>      // Находим следующий соседний узел пока не встретили элемент<br>      // или не получили значение null<br>      while(node && node.nodeType != 1) node = node.nextSibling;<br>      // Возвращаем найденный элемент или null<br>      return node;<br>    },<br><br>    // Поиск последнего дочернего элемента<br>    lastElementChild: function() {<br>      // Получаем последний дочерний узел<br>      var node = this.lastChild;<br>      // Находим предыдущий соседний узел пока не встретили элемент<br>      // или не получили значение null<br>      while(node && node.nodeType != 1) node = node.previousSibling;<br>      // Возвращаем найденный элемент или null<br>      return node;<br>    },<br><br>    // Поиск следующего соседнего элемента<br>    nextElementSibling: function() {<br>      // Объявляем переменную и инициализируем<br>      // ее ссылкой на текущий элемент<br>      var node = this;<br>      // Находим следующий соседний узел пока не встретили элемент<br>      // или не получили значение null<br>      do node = node.nextSibling<br>      while(node && node.nodeType != 1);<br>      // Возвращаем найденный элемент или null<br>      return node;<br>    },<br><br>    // Поиск предыдущего соседнего элемента<br>    previousElementSibling: function() {<br>      // Объявляем переменную и инициализируем<br>      // ее ссылкой на текущий элемент<br>      var node = this;<br>      // Находим предыдущий соседний узел пока не встретили элемент<br>      // или не получили значение null<br>      do node = node.previousSibling;<br>      while(node && node.nodeType != 1);<br>      // Возвращаем найденный элемент или null<br>      return node;<br>    },<br><br>    // Определение количества дочерних элементов<br>    // Проверяем, что браузер не поддерживает геттер children<br>    childElementCount: typeof element.children == "undefined" ? function() {<br>      // Браузер не поддерживает children,<br>      // поэтому получаем список всех дочерних узлов<br>      var list = this.childNodes,<br>      // определяем их количество<br>      i = list.length,<br>      // заводим счетчик элементов<br>      j = 0;<br>      // Проходя в цикле по всем дочерним узлам,<br>      while(i--)<br>          // если встретился элемент,<br>        if(list[i].nodeType == 1)<br>          // увеличиваем счетчик<br>          j++;<br>      // Возвращаем количество дочерних узлов или 0<br>      return j;<br>    } : function() {<br>      // Браузер поддерживает children,<br>      // поэтому получаем список всех дочерних элементов<br>      // и возвращаем их количество<br>      return this.children.length;<br>    }<br>  };<br><br>  // Создаем геттеры для IE8<br>  if(Object.defineProperty)<br>    for(var property in ElementTravrsal)<br>      if(ElementTravrsal.hasOwnProperty(property))<br>        Object.defineProperty(Element.prototype, property, {<br>          get: ElementTravrsal[property]<br>        });<br><br>  // для Firefox 2+ и Safari 3+<br>  if(Object.__defineGetter__)<br>    for(var property in ElementTravrsal)<br>      if(ElementTravrsal.hasOwnProperty(property))<br>        HTMLElement.prototype.__defineGetter__(property, ElementTravrsal[property]);<br><br>}
    Включив этот код в проект, уже сейчас можно пользоваться ElementTraversal в большинстве браузеров:
    • Internet Explorer 8;
    • Mozilla Firefox 2+ и другие браузеры на основе Gecko 1.8+;
    • Opera 9.6+ (может быть и 9.5+);
    • Safari 3+ (может даже и 2? К сожалению, нет возможности проверить);
    • Google Chrome 2+.
    Для разработки некоторых сервисов или административных интерфейсов этого списка вполне может быть достаточно. Ну а если нужна поддержка всех браузеров, без дополнительных функций, как в приведеной статье по ссылке выше, не обойтись. Или все же можно расширить список поддерживаемых браузеров? Если есть идеи, как это сделать в других браузерах, пишите в комментариях, пусть даже это будут нерациональные решения, интересна сама возможность реализации.

    Используются атрибуты нового интерфейса точно так же, как и старые firstChild, lastChild, nextSibling и т. д. Возьмем, к примеру, такой XHTML-код:
    <div id="test"><br>  TextNode<br>  <div>TextNode</div><br>  TextNode<br>  <p>TextNode</p><br>  TextNode<br></div><br>TextNode<br><p>TextNode</p>
    И выполним несколько перемещений по DOM-дереву в JavaScript:
    // Получим ссылку на элемент<br>// с идентифкатором "test"<br>var node = document.getElementById("test");<br><br>// Узнаем tagName следующего элемента<br>alert(node.nextElementSibling.tagName); // → "P"<br><br>// Найдем последний дочерний элемент<br>node = node.lastElementChild;<br><br>// И узнаем, есть у него дочерние элементы?<br>alert(node.childElementCount); // → "0"<br><br>// А вообще дочерние узлы?<br>alert(node.hasChildNodes()); // → "true"
    Если было интересно, могу написать подробнее о применении getter'ов и setter'ов в JScript и JavaScript в следующей статье.

    Архив с кодом из статьи: ElementTraversal.zip

    Update: перенес в тематический блог, спасибо за карму :-)

    Похожие публикации

    Средняя зарплата в IT

    111 111 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 6 788 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 17

      0
      Странно, что в интерфейс включили childElementCount. По-моему, было бы логичнее включить childElements, аналог childNodes, а у него уже можно и количество узнать.
        –1
        Наверное, решили не дублировать уже имеющийся во всех браузерах, кроме Firefox, геттер children, пришедший из IE4, а для Firefox его можно сделать тем же __defineGetter__. Кстати, children появится в новой версии Firefox 3.5, об этом написано в MDC на странице «Firefox 3.5 for developers» в разделе «Other improvements».
        +1
        а чем это отличается от обычной обертки? Т.е. node у нас будет обладать некоторыми свойствами, которые иногда будут использоваться (именно иногда, поэтому дополнительная нагрузка на дерево может превысить пользу от этого подхода).
          0
          Стандартные методы, геттеры и сеттеры тоже в прототипе находятся, не думаю, что добавление еще нескольких пользовательских геттеров в прототип объекта Element/HTMLElement для браузеров, не поддерживающих ElementTraversal, существенно понизит производительность, но все же попробую протестировать.
            0
            Тестировал на следующем коде:
            var t1 = new Date().getTime(), i = 100000, doc = document;
            while(i--) doc.createElement('div');
            t1 = new Date().getTime() - t1;

            if(window.HTMLElement) {
              HTMLElement.prototype.__defineGetter__("getter1", function(){});
              HTMLElement.prototype.__defineGetter__("getter2", function(){});
              HTMLElement.prototype.__defineGetter__("getter3", function(){});
              HTMLElement.prototype.__defineGetter__("getter4", function(){});
              HTMLElement.prototype.__defineGetter__("getter5", function(){});
            }
            else {
              Object.defineProperty(Element.prototype, "getter1", {get: function(){}});
              Object.defineProperty(Element.prototype, "getter2", {get: function(){}});
              Object.defineProperty(Element.prototype, "getter3", {get: function(){}});
              Object.defineProperty(Element.prototype, "getter4", {get: function(){}});
              Object.defineProperty(Element.prototype, "getter5", {get: function(){}});
            }

            i = 100000;

            var t2 = new Date().getTime();
            while(i--) doc.createElement('div');
            t2 = new Date().getTime() - t2;

            document.title = t1 + ' / ' + t2;
            Разницы в скорости не заметил :-)
            0
            Ну вообще-то навигацию по DOM дереву лично мне больше нравиться делать через XPath выражения. Вполне наглядно и добно.

            Игнорирование текстовых узлов созданых непечатными символами давно уже используется в IE. Я бы даже сказал, только так и используется )) В принципе хорошо, что и в других предусмотрели возможность работы согласно подобной схеме.
              +1
              — если нода изначально «пуста», то полетят ошибки;
              'children' лучше выкинуть (не-стандарт, есть баги);
              — упростить бы 'do-if-break-while', заодно из консоли сгинут уорнинги;
              — строго (===) сравнивать 'typeof' не обязательно…

              >могу написать подробнее
              Конечно, пиши.

              +1
                0
                если нода изначально «пуста», то полетят ошибки
                Спасибо, поправил.

                'children' лучше выкинуть (не-стандарт, есть баги)
                Не сталкивался с проблемами при использовании children, можно подробнее?

                упростить бы 'do-if-break-while', заодно из консоли сгинут уорнинги
                Вообще я за то, чтобы пользоваться всей гибкостью языка, но тоже поправил :-)

                строго (===) сравнивать 'typeof' не обязательно
                Так быстрее
                0
                >можно подробнее
                IE видит комментарии, остальные гипотетически могут реализовать с ошибками, ибо нестандарт.

                >Вообще я за то
                Кто ж против, только break-ать надо ли? Если правильная нода сразу же нашлась, то просто не стоит заходить ни внутрь 'do-while', ни в 'while'. Алгоритм для всех случаев по идее один и тот же — в двух случаях идём налево, в двух — направо, но логика одна, не две, что и получилось сейчас.

                >Так быстрее
                Тест есть готовый? ;)
                  0
                  Так варнинги были из-за присваивания внутри while, вроде break здесь не причем.
                  А о том, что так быстрее, в этой ситуации на 2-х проверках конечно бессмысленно о каком то выигрыше в скорости говорить, я просто взял себе за правило, всегда писать сравнение со строкой без привидения типов, т.к. это гораздо быстрее при сравнении разных типов, но тут typeof выдает строку, поэтому наверное, никакой разницы не будет в скорости, поспешил я написать :-)
                    0
                    >из-за присваивания
                    Конечно из-за присваивания, мне там виделось что-то проще… вроде 'while (node && node.nodeType != 1)...', дабы не заходить внутрь, не брэкать, не делать безусловный 'do', а заодно убежать от ворнингов, убрав присвоение из 'while'…

                    >без привидения типов
                    Строгое стравнение выкидывает 'false' для значений разных типов. Нестрогое сравнение также может не конвертировать значения разных типов (undefined, null), в остальных случаях конвертирует, что само собой затратно по сравнению со строгим сравнением, чем и объясняется скорость в этом случае. В отношения значений одного и того же типа алгоритм обоих сравнений одинаков (буква в букву), в частности для 'typeof' при сравнении строки со строкой, теоретически они должны работать одинаково. Практически же энная реализация может учудить, сделав что-то быстрее или медленнее, но это скорее исключение из правил, на которое можно было бы ориентироваться, если бы была всем вокруг известна ужасающая разница на популярном движке (вроде «сверхбыстрой» конкатенации у IE)…
                    0
                    О, написал про IE, но на 8-ке не проверял, сказал по памяти, под рукой нет, может уже исправили. ???
                      0
                      Что исправили? children? Нет, IE8RC1 возвращает узел с комментарием при использовании children :-(
                        +1
                        Жаль, значит укоренился баг…
                    0
                    node.getElementsByTagName("*").length — это не количество дочерних узлов. У дочерних узлов могут быть дочерние узлы, которые считать не надо.
                      0
                      Имелась ввиду проверка наличия дочерних элементов по аналогии с hasChildNodes, который проверяет наличие дочерних узлов, т.к. текстовые узлы не могут содержать дочерние узлы, то проверка:
                      node.getElementsByTagName("*").length
                      всегда верно будет показывать факт наличия дочерних элементов, но не всегда правильно их количество.
                        0
                        Перефразировал предложение в статье.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое