Перейти к содержимому

Комплексное руководство по обходу DOM в JavaScript

Обход DOM (Document Object Model) — это фундаментальный навык для веб-разработчиков, использующих JavaScript. Освоение обхода DOM позволит вам динамически манипулировать веб-страницами, создавая интерактивные и отзывчивые пользовательские интерфейсы. Это руководство предоставит вам подробные объяснения и множество примеров кода, чтобы помочь вам стать профессионалом в обходе DOM.

Введение в обход DOM

DOM представляет структуру веб-страницы в виде дерева узлов. Каждый узел соответствует элементу или фрагменту контента на странице. Обход DOM включает перемещение между этими узлами для доступа к элементам или манипуляции с ними.

Понимание структуры дерева DOM

Прежде чем переходить к методам обхода, важно понять структуру дерева DOM. Вот простой HTML-документ для наглядности:


html
<!DOCTYPE html>
<html>
<head>
    <title>DOM Traversal Example</title>
</head>
<body>
    <div id="container">
        <p class="text">Hello, World!</p>
        <ul>
            <li>Item 1</li>
            <li>Item 2</li>
            <li>Item 3</li>
        </ul>
    </div>
</body>
</html>

В этом документе элемент <code><body></code> содержит <code><div></code> с id "container", который, в свою очередь, содержит элемент <code><p></code> и <code><ul></code> с дочерними элементами <code><li></code>.

Базовые методы обхода

Доступ к дочерним узлам

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


html
<!DOCTYPE html>
<html>
<head>
    <title>Accessing Child Nodes</title>
</head>
<body>
    <div id="blog-post">
        <h2>Blog Post Title</h2>
        <p>Some interesting content...</p>
        <div class="comments">
            <p>Comment 1</p>
            <p>Comment 2</p>
            <p>Comment 3</p>
        </div>
    </div>

    <script>
        const commentsContainer = document.querySelector('.comments');
        const comments = commentsContainer.children; // Only includes element nodes

        // Display the number of comments
        console.log(`Number of comments: ${comments.length}`);
    </script>
</body>
</html>

Этот код выбирает <code><div></code> с классом "comments" и выводит количество элементов комментариев внутри него. Примечание: children возвращает только узлы элементов, тогда как childNodes включает текстовые узлы и узлы комментариев. Для обхода только элементов предпочтительнее использовать children.

Переход к родительским узлам

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


html
<!DOCTYPE html>
<html>
<head>
    <title>Navigating to Parent Nodes</title>
</head>
<body>
    <div id="shopping-cart">
        <ul>
            <li>Item 1</li>
            <li>Item 2</li>
            <li>Item 3</li>
        </ul>
    </div>

    <script>
        const cartItem = document.querySelector('li');
        const parent = cartItem.parentNode;

        // Display the parent node
        console.log(`The parent of the first cart item is a: ${parent.tagName}`);
    </script>
</body>
</html>

Этот код выбирает первый элемент <code><li></code> и выводит имя тега его родительского узла. Для навигации только по элементам parentElement часто предпочтительнее parentNode, так как он пропускает текстовые узлы и возвращает null, если родитель не является элементом.

Узлы-братья (сиблинги)

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


html
<!DOCTYPE html>
<html>
<head>
    <title>Task List Navigation</title>
    <style>
        .task {
            margin: 10px;
            padding: 10px;
            border: 1px solid #ccc;
        }
        .completed {
            text-decoration: line-through;
            color: gray;
        }
    </style>
</head>
<body>
    <div class="task-list">
        <div class="task">
            <p>Task 1: Do the laundry</p>
            <button class="complete-task">Complete Task</button>
        </div>
        <div class="task">
            <p>Task 2: Buy groceries</p>
            <button class="complete-task">Complete Task</button>
        </div>
        <div class="task">
            <p>Task 3: Clean the house</p>
            <button class="complete-task">Complete Task</button>
        </div>
    </div>

    <script>
        document.querySelectorAll('.complete-task').forEach(button => {
            button.addEventListener('click', () => {
                const task = button.parentElement;
                task.classList.add('completed');
                button.disabled = true;

                const nextTask = task.nextElementSibling;
                if (nextTask) {
                    console.log(`Next task: ${nextTask.querySelector('p').textContent}`);
                } else {
                    console.log('No more tasks available');
                }
            });
        });
    </script>
</body>
</html>

Этот код предоставляет список задач, где у каждой задачи есть кнопка "Выполнить задачу". Когда задача отмечена как выполненная, текст зачеркивается, а кнопка отключается. Также отображается описание следующей задачи. Если задач больше нет, выводится сообщение об этом. Аналогично, previousElementSibling и nextElementSibling пропускают текстовые узлы, что делает их безопаснее для обхода только элементов по сравнению с previousSibling и nextSibling.

Продвинутые техники обхода

Поиск элементов по классу или тегу

Представьте, что вы создаете панель управления, в которой перечислены все пользователи, и вы хотите найти и подсчитать все элементы пользователей.


html
<!DOCTYPE html>
<html>
<head>
    <title>Finding Elements by Class or Tag</title>
</head>
<body>
    <div class="user">User 1</div>
    <div class="user">User 2</div>
    <div class="user">User 3</div>

    <script>
        const users = document.getElementsByClassName('user');

        // Display the number of users
        console.log(`Number of users: ${users.length}`);
    </script>
</body>
</html>

Этот код подсчитывает и выводит количество элементов с классом user.

Методы Query Selector

Представьте, что у вас есть новостной сайт, и вы хотите выделить все заголовки.


html
<!DOCTYPE html>
<html>
<head>
    <title>Query Selector Methods</title>
</head>
<body>
    <div id="news">
        <h1 class="headline">Headline 1</h1>
        <h1 class="headline">Headline 2</h1>
        <h1 class="headline">Headline 3</h1>
    </div>

    <script>
        const headlines = document.querySelectorAll('.headline');

        // Highlight all headlines
        headlines.forEach(headline => {
            headline.style.color = 'red';
        });

        // Display the number of headlines
        console.log(`Number of headlines: ${headlines.length}`);
    </script>
</body>
</html>

Этот код выбирает все элементы с классом headline, меняет их цвет на красный и выводит количество этих элементов.

Обход с помощью рекурсивных функций

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


html
<!DOCTYPE html>
<html>
<head>
    <title>Recursive Traversal</title>
    <style>
        .comment {
            margin: 10px;
            padding: 10px;
            border: 1px solid #ccc;
        }
        .reply {
            margin-left: 20px;
            border-left: 2px solid #aaa;
        }
    </style>
</head>
<body>
    <div class="comments">
        <div class="comment">
            <p>Comment 1</p>
            <div class="reply">
                <p>Reply 1-1</p>
                <div class="reply">
                    <p>Reply 1-1-1</p>
                </div>
            </div>
            <div class="reply">
                <p>Reply 1-2</p>
            </div>
        </div>
        <div class="comment">
            <p>Comment 2</p>
            <div class="reply">
                <p>Reply 2-1</p>
            </div>
        </div>
    </div>

    <script>
        function traverseComments(node) {
            if (!node) return; // Guard against null/undefined

            if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('comment')) {
                console.log(`Comment: ${node.querySelector('p').textContent}`);
            }

            if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('reply')) {
                console.log(`Reply: ${node.querySelector('p').textContent}`);
            }

            for (let i = 0; i < node.childNodes.length; i++) {
                traverseComments(node.childNodes[i]);
            }
        }

        traverseComments(document.querySelector('.comments'));
    </script>
</body>
</html>

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

Практические примеры

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

Создание динамического списка задач

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


html
<!DOCTYPE html>
<html>
<head>
    <title>Dynamic To-Do List</title>
    <style>
        .info { color: darkgreen; }
    </style>
</head>
<body>
    <div id="todo-list">
        <h2>To-Do List</h2>
        <ul id="tasks">
            <li>Task 1</li>
            <li>Task 2</li>
        </ul>
        <input type="text" id="task-input" placeholder="Add a new task" />
        <button id="add-button">Add Task</button>
    </div>

    <script>
        const tasks = document.getElementById('tasks');
        const input = document.getElementById('task-input');
        const button = document.getElementById('add-button');

        button.addEventListener('click', () => {
            const newTask = input.value.trim();
            if (newTask) {
                const li = document.createElement('li');
                li.textContent = newTask;
                tasks.appendChild(li);
                input.value = '';
                console.log('Added new task to the to-do list');
            }
        });
    </script>
</body>
</html>

Этот код позволяет пользователям добавлять новые задачи в список задач, вводя текст в поле ввода и нажимая кнопку.

Обновление атрибутов элементов

Представьте, что у вас есть список товаров, и вы хотите отмечать товары как "избранные" при их нажатии.


html
<!DOCTYPE html>
<html>
<head>
    <title>Updating Element Attributes</title>
    <style>
        .favorite { font-weight: bold; color: gold; }
        .info { color: darkblue; }
    </style>
</head>
<body>
<h4>Click on the list item below to see the result!</h4>
    <ul id="product-list">
        <li>Product 1</li>
        <li>Product 2</li>
        <li>Product 3</li>
    </ul>

    <script>
        const productList = document.getElementById('product-list');

        productList.addEventListener('click', (event) => {
            if (event.target.tagName === 'LI') {
                event.target.classList.toggle('favorite');
                console.log(`Toggled favorite status for: ${event.target.textContent}`);
            }
        });
    </script>
</body>
</html>

Этот код позволяет пользователям отмечать товары как "избранные" путем нажатия на них, изменяя их внешний вид с помощью класса favorite.

INFO

Минимизируйте доступ к DOM для повышения производительности. Группируйте манипуляции с DOM, чтобы уменьшить перерасчеты макета и перерисовки.

Заключение

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

Практика

Какой из следующих методов можно использовать для обхода DOM в JavaScript?

Считаете ли это полезным?

Предпросмотр dual-run — сравните с маршрутами Symfony на продакшене.