Рекурсия и как она работает

Рекурсия должна быть такой
Рекурсия должна быть такой

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

Часто ли вы встречались с ситуацией, когда нужно пройтись по DOM дереву, например, что-то найти или посчитать на странице? Думаю, такая ситуация иногда возникает. Первое, что приходит на ум — использовать рекурсивную функцию, которая выполнит требуемую операцию. Давайте подробнее разберем, что для этого требуется и как избежать неприятностей при данном способе.

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

<html lang="en" class="no-js"> <head> <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Simple Page</title> </head> <body> <div class="wrapper"> <div> <header> <a href="/" id="logo">HTML Example</a> <div class="description"> <h1>Simple Page</h1> </div> </header> <main> <div id="articals"> <div> <h2> This is the list of the articles. </h2> </div> <div> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque viverra hendrerit ex, in faucibus mauris gravida eget. Nam sodales consequat justo id gravida. Duis volutpat enim eget est eleifend rhoncus. Nullam convallis elit sit amet mattis auctor. Proin placerat ligula sed nunc tincidunt placerat. Aenean id neque luctus, mattis orci et, eleifend urna. Donec tincidunt tristique mollis. Sed ullamcorper ipsum risus, et semper felis mollis placerat. Nam porttitor ante vel nunc mollis ullamcorper. Donec sit amet sapien sed ex feugiat laoreet vitae sollicitudin est. Integer lectus ligula, tempor non ex non, lacinia cursus tellus. </p> </div> <div> <p> Sed auctor ultricies mattis. Etiam tristique at tortor at blandit. Aenean convallis id metus nec auctor. Morbi sapien ipsum, ullamcorper et dignissim eget, eleifend et lorem. Nulla et lacus velit. Nam tortor velit, dapibus aliquet leo sed, molestie malesuada velit. Curabitur vulputate est odio, sit amet rhoncus tellus rutrum et. Donec semper at mauris vitae fringilla. Nulla facilisi. Fusce laoreet quis ligula ac malesuada. Aliquam bibendum nisi vel ante condimentum, nec viverra nisi sollicitudin. Duis accumsan pharetra felis, in convallis mauris porta in. Etiam et ligula fringilla, elementum ligula vitae, ornare velit. Maecenas sit amet dui ac purus cursus interdum. Suspendisse potenti. </p> </div> <div> <p> Aliquam posuere sagittis sapien id pharetra. Vivamus ac gravida tellus, quis finibus sem. Integer elementum convallis metus eget maximus. Sed pretium quam at sapien vulputate, sit amet bibendum urna auctor. Duis viverra lectus eu dictum imperdiet. Nullam luctus convallis felis ut egestas. Sed sit amet ullamcorper dui. Quisque id ex egestas, ultricies ex at, tempor arcu. Mauris sit amet pretium ipsum. Morbi egestas tincidunt neque. Maecenas mattis facilisis urna, at auctor nulla pulvinar efficitur. Etiam vestibulum, diam vel porttitor feugiat, nunc nisi iaculis dui, a facilisis risus metus nec lorem. Phasellus quis lobortis leo, sit amet vehicula nunc. In nibh enim, dapibus in ullamcorper id, scelerisque eu nulla. Etiam ultrices felis sit amet turpis semper consectetur. Maecenas velit elit, imperdiet sit amet ipsum quis, congue luctus nunc. </p> </div> </div> <a class="download" title="Zip package" href="#">Download</a> <a class="closeheader" title="Hide header">➤</a> </main> <footer> <div id="footer"> <span>© The HTML template</span> </div> </footer> </div> </div> </body> </html>

Далее нам нужна функция подсчета элементов:

const countElements = () => { let count = {}; return (tagName) => { count[tagName] = count[tagName] ? count[tagName] += 1 : count[tagName] = 1; return count; } }

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

Инициализируем счетчик:

const count = countElements();

Далее нам нужна функция обхода дочерних элементов конкретного HTML элемента:

const getChildren = (element) => { const childElements = [...element.children]; if (childElements.length > 0) { childElements.map(elem => { count(elem.tagName); if (elem.children) { getChildren(elem); } }) } }

Данная функция собирает массив из дочерних элементов, передает на каждой итерации название тега в нашу функцию count() и затем, если тег имеет вложенные элементы запускает рекурсию для этого элемента.

Давайте посмотрим как обрабатывает JS интерпретатор данный код. Посмотрим в каком порядке будет подсчитываться каждый элемент.

Всё выглядит хорошо. Например, наша функция обходит HTML документ из примера выше в среднем за 0.7 ms, но как обычно бывает, есть маленький нюанс.

А он заключается в Call stack— стеке вызовов, таком механизме, который позволяет интерпретатору JavaScript отслеживать в каком месте кода сейчас происходит выполнение скрипта, и в каком порядке будут выполняться те или иные функции. Так вот Call stack имеет ограничение — это лимит количества вызовов.

Посмотрим как он работает, Call stack использует принцип LIFO (Last in, First out). Возьмем вот такой пример:

const second = () => { console.log('И это запись'); } const first = () => { console.log('Это запись'); second(); console.log('А это конец записи'); } first();

А теперь визуализируем, как интерпретатор будет складывать в стек вызовы каждой функции:

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

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

Есть несколько способов обойти ограничение, все они сводятся к использованию асинхронных операций, более подробно можно посмотреть здесь.
Понравилась статья? Больше полезного материала у меня на канале:

Итак

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

Ссылки:

Подпишитесь на мой канал в Telegram:

22
Начать дискуссию