Переосмысление работы с DOM

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

Одна из наиболее распространенных задач, с которыми может встречаться фронтенд-разработчик, работа с DOM (Объектная Модель Документа). Обычно она заключается в определении расположения элемента DOM и затем управление им и/или его содержимым. Многие из нас использовали jQuery для этой задачи в течение последних семи или восьми лет, поскольку это - одна из основных задач, с которыми jQuery отлично справляется.

Для интернета семь лет это очень много. Вернемся в 2006 год, когда появился jQuery, Flash был очень популярен и интернет-приложения, разрабатывались на Flex или Silverlight, или даже Laszlo казалось, что они могли бы быть будущим веба. Кому нужен DOM, когда плагины были будущим веб-разработки.

С того момента, много изменилось. Браузер теперь поддерживает работу с DOM способами, которые не были возможны, когда jQuery только появился. Плюс, появились новые библиотеки, которые предлагают абсолютно другой подход к работе с DOM, чем тот, к которому мы привыкли. Итак, настало время спросить, нужен ли нам все еще jQuery для работы с DOM?

Как мы обычно работаем с DOM

Прежде чем мы рассмотрим доступные нативные методы или альтернативные библиотеки. Полезно сначала вспомнить то, каковы общие методы для работы с DOM предоставляет jQuery, чтобы мы знали, что мы должны будем заменить.

Селекторы

Конечно, первая вещь, в которой мы нуждаемся, это селекторы. jQuery предлагает CSS подобный синтаксис для выбора элементов DOM. Вот некоторые простые селекторы:

$("#item") // выбирает элемент с id равным "item"
 
$(".fancybutton") // выбирает элементы с классом "fancybutton"
 
$("li:even") // выбирает четные элементы списка
 
$("ul li:nth-child(3)") // выбирает 3-ий элемент списка

Большинство читателей, вероятно, уже знакомы с основными селекторами в jQuery. После того как вы выбрали DOM элемент, вы можете начать поиск с этой точки - так что давайте посмотрим на типичные методы поиска в DOM.

Методы поиска в DOM

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

<html>
<head>
    <title>DOM Traversal Sample</title>
    <style>
        .cell {
            height: 50px;
            width: 50px;
            border: 1px solid #000000;
        }
   </style>
</head>
<body>
    <table class="board">
        <tr id="row1" class="row">
            <td id="cell11" class="cell"></td>
            <td id="cell12" class="cell"></td>
            <td id="cell13" class="cell"></td>
        </tr>
        <tr id="row2" class="row">
            <td id="cell21" class="cell"></td>
            <td id="cell22" class="cell"></td>
            <td id="cell23" class="cell"></td>
        </tr>
        <tr id="row3" class="row">
            <td id="cell31" class="cell"></td>
            <td id="cell32" class="cell"></td>
            <td id="cell33" class="cell"></td>
        </tr>
    </table>
</body>
</html>

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

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

siblings()

Метод siblings() получает братские элементы (т.е. DOM узлы с тем же родительским элементом) выбранного узла. В случае ячейки таблицы, он вернет другие смежные ячейки.

$("#cell11").siblings();

siblings_cell/siblings_cell

В случае выбора строки, метод вернет другие строки.

$("#row1").siblings();

siblings_row/siblings_row

parent() и parents()

Метод parent() возвращает непосредственный родительский узел выбранного DOM узла. Таким образом, для ячейки он вернет строку-родителя.

$("#cell23").parent();

parent/parent

Для строки, метод вернет таблицу-родителя.

$("#row3").parent();

parent_row/parent_row

Метод parents() возвращает все родителей для выбранного узла вверх по DOM дереву. Таким образом, для ячейки он вернет строку и таблицу (строго говоря, он также вернет body и html узлы, но для иллюстрации, я их не беру во внимание).

$("#cell33").parents();

parents/parents

next() и nextAll()

Метод next() возвращает братский узел, который следует непосредственно за выбранным. Например, если мы выбираем клетку, метод вернет только следующую ячейку в той же строке.

$("#cell21").next();

next/next

Метод nextAll() будет возвращать все последующие ячейки, за выбранной. Он отличается от siblings() тем, что он не возвращает братские узлы, которые предшествуют выбранному узлу.

$("#cell21").nextAll();

nextall/nextall

prev() and prevAll()

Методы prev() и prevAll() работают аналогично методам next() и nextAll(), но возвращают предшествующие узлы.

$("#cell23").prev();

prev/prev

$("#cell23"). prevAll();

prevall/prevall

closest() и find()

Некоторые методы обхода DOM требуют указать селектор. Например, метод closest() вернет DOM элемент, который соответствует указанному селектору и является ближайшим родителем выбранному узлу.

$("#cell32").closest(".row");

closest/closest

В отличие от closest(), который ищет вверх по DOM дереву, find() выполняет поиск вниз, среди потомков. Он вернет все дочерние элементы, которые соответствуют указанному селектору (в отличие от closest(), который возвращает только один).

$("#row2").find(".cell");

find/find

Нам еще нужен jQuery?

За годы, прошедшие с появления jQuery, в браузерах значительно улучшилась поддержка поиска по DOМ. Фактически, по мере возможности, jQuery просто использует многие из этих методов, значит, что в большинстве случаев мы заменяем одну строку JavaScript одной строкой jQuery в нашем коде, но при этом подключается вся библиотека jQuery.

Учитывая эти усовершенствования, справедливо задать вопрос, нужен ли нам еще JQuery для поиска в DOM? Давайте посмотрим, как мы можем заменить весь код, указанный выше, используя чистый JavaScript.

Селекторы

Используя document.querySelector() и document.querySelectorAll(), мы можем легко заменить селекторы, которые я перечислил выше. Вы даже можете описать эти функции в $ и вы не заметите отсутствия jQuery. Давайте посмотрим, как это сделать.

Вот замены для четырех селекторов, перечисленных в начале.

document.querySelector("#item")
 
document.querySelectorAll(".fancybutton")
 
//…pass!
 
document.querySelectorAll("ul")[0].children[2]

Как вы можете заметить, одна строка чистого JavaScript заменяет одну строку jQuery.

ОК. я вас обманул!

...но только совсем чуть-чуть.

Большинство селекторов jQuery - это CSS селекторы. document.querySelector() также использует CSS селекторы. Но некоторые селекторы (например :even) специфичны для jQuery. Следующая функция основана на реализации :even в JQuery и выполняет то же самое.

function selectEven(elems) {
    var i = 0, length = elems.length, matches = [];
 
    for ( ; i < length; i += 2 ) {
        matches.push(elems[i]);
    }
    return matches;
}
 
selectEven(document.querySelectorAll("li"));

Метод не сложный, но это не означает, что вы должны вызвать selectEven(), вместо фильтрации селектором :even.

Выше метод будет работать для любого элемента, но в случаях, когда все элементы являются дочерними определенного элемента (чаще всего), замените его строкой:

document.querySelectorAll("ul li:nth-child(even)");
siblings()

Как и для многих методов поиска в DOM, нет родной замены и для siblings(), так что мы должны будем заменить его с на свой метод.

function getSiblings(elem) {
    var i = 0, n = elem.parentNode.firstChild, matches = [];
 
    for ( ; n; n = n.nextSibling ) 
        if ( n.nodeType == 1 && n != elem)
            matches.push( n );
                return matches;
}

После того, как этот метод написан, мы можем легко заменить примеры с jQuery.

getSiblings(document.querySelector("#cell11"))
 
getSiblings(document.querySelector("#row1"))

Возвращаемые элементы будут теми же.

parent() и parents()

Существует простая замена одной строкой для JQuery метода parent(). Примеры выше могут быть заменены:

document.querySelector("#cell21").parentNode
 
document.querySelector("#row3").parentNode

Метод вернет те же DOM элементы. Однако, для метода parents() нет простой замены, так что нам придется ее написать.

function getParents(elem) {
    var n = elem.parentNode; matches = [];
 
    for ( ; n; n = n.parentNode ) {
        if ( n.nodeType == 9) {
            return matches;
        }
        else if (n.nodeType == 1)
            matches.push( n );
    }
}

Используя этот метод, мы можем заменить parents() из JQuery на наш метод getParents().

getParents(document.querySelector("#cell33"))
next() и nextAll()

Есть множество потенциальных замен для этих методов. У DOM узла есть свойство nextSibling. Это свойство используется внутри jQuery в методе next(). Тем не менее, в некоторых браузерах, это свойство содержит пробел, так что при замене нужно учесть эту особенность.

Для простоты, я решил заменить оба метода одним, он основан на реализации используемой внутри JQuery.

function getNext(elem,all) {
    var n = elem.nextSibling, matches = [];
 
    for ( ; n; n = n.nextSibling ) {
        if ( n.nodeType == 1) {
            matches.push( n );
            if (!all) return matches;
        }
    }
    return matches;
}

Для замены next() из jQuery, мы просто вызовем getNext() и укажем DOM узел:

getNext(document.querySelector("#cell21"))

Для замены nextAll(), мы просто передадим вторым параметром true:

getNext(document.querySelector("#cell21"),true)

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

document.querySelector("#cell21+*")

Также реализуем nextAll() с помощью менее строгого отбора.

document.querySelectorAll("#cell21~*")

Кроме того, childNode API имеет свойство nextElementSibling, что позволит избежать случаев, когда возвращается пробел. Итак метод next() может реализован следующим образом:

document.querySelector("#cell21").nextElementSibling

Я не делал кросс-браузерное тестирование этих методов, но, в соответствии с MDN, они отлично поддерживаются (хотя я не знаю, почему JQuery не использует их).

prev() и prevAll()

Как и следовало ожидать, замена этих методов похожа на замену next(). Мы будем использовать previousSibling (как сделано в JQuery), но нужно учитывать ситуацию, когда возвращается пробел.

function getPrev(elem,all) {
    var n = elem.previousSibling, matches = [];
 
    for ( ; n; n = n.previousSibling ) {
        if ( n.nodeType == 1) {
            matches.push( n );
            if (!all) return matches;
        }
    }
    return matches;
}

И снова, наша замена предыдущих примеров будет выглядеть очень просто. Для prev(), мы передаем только DOM узел:

getPrev(document.querySelector("#cell23"))

Для prevAll(), вторым аргументом передадим true:

getPrev(document.querySelector("#cell23"),true)

Как и в случае с next(), childNode API имеет свойство, называемое previousElementSibling, что позволит избежать ситуации, когда возвращается пробел. Так prev() может быть переписан как:

document.querySelector("#cell23").previousElementSibling

Я также не делал кросс-браузерное тестирование этих методов, но, в соответствии с MDN, они отлично поддерживаются (хотя я опять не знаю, почему JQuery не использует их).

closest() и find()

Как вы, надеюсь, помните closest() и find() получают дополнительные селекторы в качестве аргумента. Для closest() нет прямой замены, но мы можем легко ее написать, используя метод matchesSelector().

function getClosest(elem, selector) {
    var n = elem, el;
 
    for ( ; n; n = n.parentNode ) {
        el = n;
        if ( n.matchesSelector(selector))
            return el;
    }
}

Этот метод довольно прост и отлично работает. Просто передайте выбранный узел в качестве первого аргумента и селектор, в качестве второго. Теперь у нас есть замена для JQuery примера:

getClosest(document.querySelector("#cell32"),".row");

Проблема в том, что поддержка matchesSelector не является универсальной и часто нужно указывать префикс (на самом деле, по текущей спецификации, метод называется matches, а не matchesSelector но он не был реализован в таком виде в большинстве браузеров). Так что, если вам нужна поддержка устаревших браузеров и нужно использовать closest(), то это будет не просто.

К счастью заменить find() очень просто

document.querySelector("#row2").querySelectorAll(".cell")

Вы можете объединять в цепочку querySelector() и querySelectorAll(), благодаря чему, мы можем легко заменить find() из JQuery.

Оригинал на английском

Комментарии (2)

Mikhail Pavlov 04 Февраль 2015 14:27 #

ну вот, это перевод, оказывается. я уж подумал, что какие то мысли около этой темы бродили в твоей голове и ты попробовал "выйти за границы обычного".

можно продолжить статью дальше так: "нужен ли нам DOM ? " - в этой шутке только доля шутки, некоторые "сумашедшие", например предлагают виртуальный DOM. а полные психи, на мой взгляд, пошли еще дальше...

читаю тут значит вечерком и наткнулся: http://code.tutsplus.com/tutorials/intro-to-shadow-dom--net-34966

а оттуда ссылки на вообще жесть http://www.w3.org/TR/2013/WD-components-intro-20130606/#decorator-section стало не до смеха , и на https://dvcs.w3.org/hg/webcomponents/raw-file/default/spec/templates/index.html

тут уже видно , что работы у них "непочатый край"

и вот они чем они думают, хочу я спросить ? представляете чего удумали ? теневое DOM придумали ! которое невидимо для JS! понимаетели, и с CSS тоже самое, о с одним то DOM было полно проблем, так они еще теневые ветки придумали. а АПИ к ним, и все это начиналось с безобидной идеи виртуального DOM от React, кому то понравилось, идею стали развивать до неузнаваемости.

с декораторами в CSS вообще - теперь CSS может не только описывать отображение, но и плодить для себя еще работу!

где то на хабре была статья о назвалась как-то "игра на CSS и HTML" или "программирование на HTML", в общем там чела осмеяли с ухмылкой : "ты серьезно? программирование на html ?! " и еще статья типа "вы сломали мой javascript" - шедеврально по моему

а теперь уж не знаю чего и думать. видимо жить станет еще смешнее и интереснее. мой главный вывод: - теперь я отношусь к любой нестандартной идее с большой осторожностью , в смысле, пытаюсь предвидеть "куда это может привести? "

Mishkorez 18 Март 2015 13:04 #

Жизнь - боль

Авторизуйтесь, чтобы оставить комментарий

Авторизация