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);