6.3. Демонстрация - Обход DOM-дерева
Шаг 1
В этой демонстрации давайте посмотрим на несколько различных способов обходов деревьев. Они пригодятся, если вы будете, например, писать браузерные расширения, которые будут работать с динамически изменяемыми страницами. Напомним, что document.querySelector() очень долгий, и постоянно вызывать его — дорого. Поэтому есть смысл один раз получить какую-то неизменяемую ноду и доставать её детей, просто обходя дерево. Итак, у нас есть пример табличной разметки. Давайте попробуем полностью обойти её из тега body. В момент обхода будем запоминать порядок обхода нод в отдельное поле и потом выведем его на страницу, чтобы увидеть разницу.
const root = document.body;
const resultElement = document.getElementById('result');
function traverse(node) {
const result = [];
resultElement.innerHTML = result.join(' -> ');
}
traverse(root);
Шаг 2
Одним из двух самых простых обходов списка является поиск в ширину (breadth-first search, BFS). Не бойтесь слова поиск: так исторически сложилось, что алгоритм, который раньше искал элементы в дереве, может просто обходить и модифицировать дерево, но ничего там не искать. Его суть довольно проста: мы сначала просматриваем все элементы на одном уровне вложенности, а затем переходим на следующий и следующий, пока не обойдём всё. Этот алгоритм хорошо подходит для поиска, если вы знаете, что искомый элемент, скорее всего, лежит «сверху» и есть основания полагать, что дерево довольно широкое.
const root = document.body;
const resultElement = document.getElementById('result');
function traverse(node) {
const result = [];
// очередь для просмотра нод
const queue = [];
// куда мы сразу же кладем ноду, с которой будем начинать обход
queue.push(node);
// обходим, пока в очереди что-то есть
while(queue.length) {
// получаем ноду из списка
const currentNode = queue.shift();
// делаем всё, что нужно с нашей нодой (сейчас это просто перекладывание ноды в список просмотренных)
result.push(currentNode.localName);
// и кладем в очередь всех потомков нашей ноды.
// из-за того, что мы будем класть потомков в конец и доставать из начала, мы гарантированно просмотрим все ноды текущего уровня перед спуском на следующий
queue.push(...currentNode.children);
}
resultElement.innerHTML = result.join(' -> ');
}
traverse(root);
Шаг 3
Второй способ обойти дерево — поиск в глубину (DFS, Depth-first search). В отличие от обхода в ширину, этот метод подойдет, если вы подозреваете, что нужный элемент находится внизу дерева и оно «узкое». Но если ваша цель — полностью обойти дерево, то этот способ по производительности ровно такой же, как и предыдущий. Этот способ является рекурсивным. В одном из вариантов мы сначала обработаем ноду, на которой находимся (этот способ и есть в демо), а затем рекурсивно вызовем операцию обхода на всех потомках. В другом — наоборот, сначала вызовем обход на детях, а затем обработаем саму ноду. Если наше дерево разделяет поддеревья на левые и правые, то можно применить третью вариацию: сначала пройти левых детей, потом обработать ноду и затем пройти правых детей. Все эти методы позволяют оптимизировать поиск в зависимости от гипотез о глубине нахождения элемента, но в рамках полного прохода выигрыша не дадут.
const root = document.body;
const resultElement = document.getElementById('result');
function traverse(node) {
const result = [];
// в рекурсивной функции...
function recursive(node) {
// ... сначала обрабатываем ноду, в которой находимся ...
result.push(node.localName);
// ...а потом вызываем её же на всех детях
for (const child of node.children) {
recursive(child);
}
}
recursive(node);
resultElement.innerHTML = result.join(' -> ');
}
traverse(root);