DAY_13JavaScript DOM 操作完全指南:实战案例、性能优化与业务价值(下)

本篇聚焦 DOM 的进阶实战与工程化使用,围绕图片懒加载、无缝滚动、随机点名器、选项卡、性能优化、常见坑点和业务价值展开,帮助你把 DOM API 用到真实项目中。


前言

掌握 DOM 基础 API 之后,更重要的是知道它们在项目里如何组合使用。下篇从图片懒加载、滚动交互、动态插入、状态联动等经典场景出发,进一步总结性能优化方法、常见踩坑点、面试考点和业务落地价值。

建议先具备元素获取、内容读写、尺寸位置、滚动控制和节点操作的基础,再阅读本篇的案例与优化部分。

目录

  • [8. 实战案例(续)](#8. 实战案例(续))
    • [8.4 案例四:通用倒计时器](#8.4 案例四:通用倒计时器)
    • [8.5 案例五:图片懒加载(基础版)](#8.5 案例五:图片懒加载(基础版))
    • [8.6 案例六:图片懒加载(进阶版)](#8.6 案例六:图片懒加载(进阶版))
    • [8.7 案例七:无缝滚动(scrollLeft 驱动)](#8.7 案例七:无缝滚动(scrollLeft 驱动))
    • [8.8 案例八:无缝滚动(CSS 动画驱动)](#8.8 案例八:无缝滚动(CSS 动画驱动))
    • [8.9 延伸练习:随机点名器与选项卡](#8.9 延伸练习:随机点名器与选项卡)
    • [8.10 预习衔接:HTML DOM 与事件](#8.10 预习衔接:HTML DOM 与事件)
  • [9. 最佳实践与性能优化](#9. 最佳实践与性能优化)
    • [9.1 性能优化建议](#9.1 性能优化建议)
    • [9.2 DocumentFragment 批量操作](#9.2 DocumentFragment 批量操作)
    • [9.3 避免布局抖动](#9.3 避免布局抖动)
    • [9.4 事件委托](#9.4 事件委托)
    • [9.5 选择器性能](#9.5 选择器性能)
    • [9.6 常见坑点与避坑指南](#9.6 常见坑点与避坑指南)
  • [10. 参考资源](#10. 参考资源)
    • [10.1 官方文档](#10.1 官方文档)
    • [10.2 延伸阅读与实践文章](#10.2 延伸阅读与实践文章)
    • [10.3 相关知识图谱](#10.3 相关知识图谱)
    • [10.4 常用网站应用](#10.4 常用网站应用)
  • [11. 经典使用场景与业务价值](#11. 经典使用场景与业务价值)
    • [11.1 电商商品列表:懒加载、筛选、批量选择](#11.1 电商商品列表:懒加载、筛选、批量选择)
    • [11.2 内容社区与资讯流:无限滚动、动态插入、状态提示](#11.2 内容社区与资讯流:无限滚动、动态插入、状态提示)
    • [11.3 管理后台:表格操作、批量处理、状态联动](#11.3 管理后台:表格操作、批量处理、状态联动)
    • [11.4 在线表单与活动页:校验、倒计时、引导提交](#11.4 在线表单与活动页:校验、倒计时、引导提交)
    • [11.5 在线文档与教程页面:目录导航、代码折叠、阅读进度](#11.5 在线文档与教程页面:目录导航、代码折叠、阅读进度)
    • [11.6 图片与媒体展示:轮播、预览、无缝滚动](#11.6 图片与媒体展示:轮播、预览、无缝滚动)
    • [11.7 业务价值总览](#11.7 业务价值总览)
  • [12. 知识点归纳总结](#12. 知识点归纳总结)
    • [12.1 API 速查手册](#12.1 API 速查手册)
    • [12.2 面试高频考点](#12.2 面试高频考点)
    • [12.3 学习路径建议](#12.3 学习路径建议)
    • [12.4 知识体系回顾](#12.4 知识体系回顾)

8. 实战案例(续)

8.4 案例四:通用倒计时器

应用场景:秒杀活动、限时优惠、活动开始倒计时等。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>倒计时器示例</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Arial', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .countdown-container {
            text-align: center;
            background: rgba(255, 255, 255, 0.95);
            padding: 40px;
            border-radius: 20px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
        }

        .countdown-container h1 {
            color: #333;
            margin-bottom: 30px;
            font-size: 24px;
        }

        .countdown-display {
            display: flex;
            gap: 20px;
            justify-content: center;
            margin: 30px 0;
        }

        .time-unit {
            display: flex;
            flex-direction: column;
            align-items: center;
        }

        .time-value {
            width: 80px;
            height: 80px;
            background: linear-gradient(145deg, #667eea, #764ba2);
            color: white;
            font-size: 36px;
            font-weight: bold;
            display: flex;
            justify-content: center;
            align-items: center;
            border-radius: 10px;
            box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
        }

        .time-label {
            margin-top: 10px;
            color: #666;
            font-size: 14px;
        }

        .time-separator {
            font-size: 36px;
            font-weight: bold;
            color: #667eea;
            padding-top: 20px;
        }

        .message {
            color: #667eea;
            font-size: 18px;
            margin-top: 20px;
            min-height: 27px;
        }

        .input-group {
            margin-top: 30px;
            display: flex;
            gap: 10px;
            justify-content: center;
        }

        .input-group input {
            padding: 12px 20px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 14px;
            width: 200px;
        }

        .input-group button {
            padding: 12px 24px;
            background: linear-gradient(145deg, #667eea, #764ba2);
            color: white;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
            transition: transform 0.2s;
        }

        .input-group button:hover {
            transform: translateY(-2px);
        }

        .finished .time-value {
            background: linear-gradient(145deg, #11998e, #38ef7d);
        }
    </style>
</head>
<body>
    <div class="countdown-container">
        <h1 id="title">距离目标时间还有</h1>

        <div class="countdown-display" id="countdown">
            <div class="time-unit">
                <div class="time-value" id="days">00</div>
                <div class="time-label">天</div>
            </div>
            <div class="time-separator">:</div>
            <div class="time-unit">
                <div class="time-value" id="hours">00</div>
                <div class="time-label">时</div>
            </div>
            <div class="time-separator">:</div>
            <div class="time-unit">
                <div class="time-value" id="minutes">00</div>
                <div class="time-label">分</div>
            </div>
            <div class="time-separator">:</div>
            <div class="time-unit">
                <div class="time-value" id="seconds">00</div>
                <div class="time-label">秒</div>
            </div>
        </div>

        <div class="message" id="message"></div>

        <div class="input-group">
            <input type="number" id="minutesInput" placeholder="输入分钟数" min="1" max="60" value="5">
            <button id="startBtn">开始倒计时</button>
        </div>
    </div>

    <script>
        (function() {
            let countdownInterval = null;
            let targetDate = null;

            const daysEl = document.querySelector('#days');
            const hoursEl = document.querySelector('#hours');
            const minutesEl = document.querySelector('#minutes');
            const secondsEl = document.querySelector('#seconds');
            const messageEl = document.querySelector('#message');
            const startBtn = document.querySelector('#startBtn');
            const minutesInput = document.querySelector('#minutesInput');
            const container = document.querySelector('.countdown-container');

            // 设置目标时间
            function setTarget(minutes) {
                const now = new Date();
                targetDate = new Date(now.getTime() + minutes * 60 * 1000);
            }

            // 格式化数字(补零)
            function formatNumber(num) {
                return num < 10 ? '0' + num : num;
            }

            // 更新倒计时显示
            function updateCountdown() {
                if (!targetDate) return;

                const now = new Date();
                const diff = targetDate.getTime() - now.getTime();

                if (diff <= 0) {
                    clearInterval(countdownInterval);
                    daysEl.textContent = '00';
                    hoursEl.textContent = '00';
                    minutesEl.textContent = '00';
                    secondsEl.textContent = '00';
                    messageEl.textContent = '倒计时结束!';
                    container.classList.add('finished');
                    startBtn.disabled = false;
                    return;
                }

                const days = Math.floor(diff / (24 * 60 * 60 * 1000));
                const hours = Math.floor((diff % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
                const minutes = Math.floor((diff % (60 * 60 * 1000)) / (60 * 1000));
                const seconds = Math.floor((diff % (60 * 1000)) / 1000);

                daysEl.textContent = formatNumber(days);
                hoursEl.textContent = formatNumber(hours);
                minutesEl.textContent = formatNumber(minutes);
                secondsEl.textContent = formatNumber(seconds);
            }

            // 开始倒计时
            startBtn.addEventListener('click', function() {
                const minutes = parseInt(minutesInput.value);
                if (isNaN(minutes) || minutes <= 0) {
                    messageEl.textContent = '请输入有效的分钟数';
                    return;
                }

                clearInterval(countdownInterval);
                setTarget(minutes);
                container.classList.remove('finished');
                startBtn.disabled = true;
                messageEl.textContent = '倒计时进行中...';

                updateCountdown();
                countdownInterval = setInterval(updateCountdown, 1000);
            });

            // 初始化
            updateCountdown();
        })();
    </script>
</body>
</html>

代码解释

  • HTML 结构

    • 四个时间单位显示:天、时、分、秒
    • 输入框:设置倒计时时长(分钟)
    • 开始按钮:启动倒计时
  • CSS 样式

    • 渐变背景和卡片式容器
    • Flexbox 布局实现响应式时间显示
    • 完成状态(.finished)改变配色方案
  • JavaScript 核心功能

    • setTarget(minutes):根据输入的分钟数计算目标时间(当前时间 + 分钟数 × 60秒 × 1000毫秒)
    • formatNumber(num):个位数补零,确保显示为两位数
    • updateCountdown()
      • 计算当前时间与目标时间的差值(毫秒)
      • 分别提取天数、小时数、分钟数、秒数
      • 更新显示内容
      • 倒计时结束时清除定时器,显示完成消息
    • 点击开始按钮:验证输入 → 设置目标时间 → 启动定时器(每秒执行一次)
  • 实际应用:秒杀活动倒计时、限时优惠提醒、考试倒计时、活动开始倒计时等

8.5 案例五:图片懒加载(基础版)

应用场景:图片较多的列表页,先加载首屏可见图片,用户滚动时再加载后续图片。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>基础图片懒加载</title>
    <style>
        .wrapper {
            display: flex;
            flex-wrap: wrap;
            width: 1100px;
            margin: 20px auto;
        }
        .wrapper img {
            width: 480px;
            height: 480px;
            margin: 10px;
            object-fit: cover;
            background: #eee;
        }
    </style>
</head>
<body>
    <div class="wrapper">
        <img data-src="images/db01.jpg" alt="图片 1">
        <img data-src="images/db02.jpg" alt="图片 2">
        <img data-src="images/db03.jpg" alt="图片 3">
        <img data-src="images/db04.jpg" alt="图片 4">
        <img data-src="images/db05.jpg" alt="图片 5">
        <img data-src="images/db06.jpg" alt="图片 6">
        <img data-src="images/db07.jpg" alt="图片 7">
        <img data-src="images/db08.jpg" alt="图片 8">
        <img data-src="images/db09.jpg" alt="图片 9">
        <img data-src="images/db10.jpg" alt="图片 10">
    </div>

    <script>
        (function () {
            const imgItems = document.querySelectorAll('.wrapper img');

            function loadImage() {
                imgItems.forEach(function (img) {
                    const rect = img.getBoundingClientRect();
                    const viewportHeight = document.documentElement.clientHeight;

                    if (rect.top < viewportHeight && img.dataset.src) {
                        img.src = img.dataset.src;
                        img.removeAttribute('data-src');
                    }
                });
            }

            loadImage();
            window.addEventListener('scroll', loadImage);
        })();
    </script>
</body>
</html>

代码解释

  • data-src 存储真实图片地址,页面初始不设置 src,避免一次性加载所有图片。
  • getBoundingClientRect().top 表示图片顶部到视口顶部的距离。
  • rect.top < document.documentElement.clientHeight 时,说明图片进入视口范围,可以把 data-src 赋值给 src
  • 加载后移除 data-src,防止重复赋值。
  • 网站应用:电商商品图、摄影作品集、文章配图列表、瀑布流信息流。

8.6 案例六:图片懒加载(进阶版)

应用场景:电商网站、图片列表页、瀑布流布局等。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>图片懒加载示例</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: Arial, sans-serif;
            background: #f5f5f5;
        }

        .header {
            background: #fff;
            padding: 20px;
            text-align: center;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            position: sticky;
            top: 0;
            z-index: 100;
        }

        .header h1 {
            color: #333;
            margin-bottom: 10px;
        }

        .header p {
            color: #666;
        }

        .gallery {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            padding: 20px;
            max-width: 1200px;
            margin: 0 auto;
        }

        .gallery-item {
            flex: 0 0 calc(50% - 10px);
            background: #fff;
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        .gallery-item img {
            width: 100%;
            height: 300px;
            object-fit: cover;
            display: block;
            background: #f0f0f0;
        }

        .gallery-item.loading img {
            opacity: 0.5;
            filter: blur(10px);
        }

        .gallery-item.loaded img {
            opacity: 1;
            filter: blur(0);
            transition: opacity 0.3s, filter 0.3s;
        }

        .gallery-item .caption {
            padding: 15px;
        }

        .gallery-item .caption h3 {
            color: #333;
            margin-bottom: 5px;
        }

        .gallery-item .caption p {
            color: #666;
            font-size: 14px;
        }

        .loading-indicator {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: rgba(0,0,0,0.8);
            color: white;
            padding: 10px 20px;
            border-radius: 20px;
            font-size: 14px;
        }

        @media (max-width: 768px) {
            .gallery-item {
                flex: 0 0 100%;
            }
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>图片懒加载示例</h1>
        <p>向下滚动查看图片加载效果</p>
    </div>

    <div class="gallery" id="gallery">
        <!-- 图片将通过 JavaScript 动态生成 -->
    </div>

    <div class="loading-indicator" id="loadingIndicator">已加载: 0 / 0</div>

    <script>
        (function() {
            const gallery = document.querySelector('#gallery');
            const loadingIndicator = document.querySelector('#loadingIndicator');

            // 图片列表(使用 data-src 存储真实地址)
            const images = [
                { src: 'images/db01.jpg', title: '图库图片 1', desc: '本地图片资源示例' },
                { src: 'images/db02.jpg', title: '图库图片 2', desc: '本地图片资源示例' },
                { src: 'images/db03.jpg', title: '图库图片 3', desc: '本地图片资源示例' },
                { src: 'images/db04.jpg', title: '图库图片 4', desc: '本地图片资源示例' },
                { src: 'images/db05.jpg', title: '图库图片 5', desc: '本地图片资源示例' },
                { src: 'images/db06.jpg', title: '图库图片 6', desc: '本地图片资源示例' },
                { src: 'images/db07.jpg', title: '图库图片 7', desc: '本地图片资源示例' },
                { src: 'images/db08.jpg', title: '图库图片 8', desc: '本地图片资源示例' },
                { src: 'images/db09.jpg', title: '图库图片 9', desc: '本地图片资源示例' },
                { src: 'images/db10.jpg', title: '图库图片 10', desc: '本地图片资源示例' },
            ];

            let loadedCount = 0;

            // 生成图库 HTML
            function generateGallery() {
                images.forEach((img, index) => {
                    const item = document.createElement('div');
                    item.className = 'gallery-item loading';
                    item.innerHTML = `
                        <img data-src="${img.src}" alt="${img.title}" loading="lazy">
                        <div class="caption">
                            <h3>${img.title}</h3>
                            <p>${img.desc}</p>
                        </div>
                    `;
                    gallery.appendChild(item);
                });
            }

            // 检查元素是否在视口中
            function isInViewport(element) {
                const rect = element.getBoundingClientRect();
                const windowHeight = window.innerHeight || document.documentElement.clientHeight;
                const windowWidth = window.innerWidth || document.documentElement.clientWidth;

                const vertInView = (rect.top <= windowHeight) && ((rect.top + rect.height) >= 0);
                const horInView = (rect.left <= windowWidth) && ((rect.left + rect.width) >= 0);

                return vertInView && horInView;
            }

            // 加载图片
            function loadImages() {
                const lazyImages = gallery.querySelectorAll('img[data-src]');

                lazyImages.forEach(img => {
                    if (img.dataset.src && isInViewport(img)) {
                        img.src = img.dataset.src;
                        img.removeAttribute('data-src');

                        img.onload = function() {
                            this.closest('.gallery-item').classList.remove('loading');
                            this.closest('.gallery-item').classList.add('loaded');
                            loadedCount++;
                            updateLoadingIndicator();
                        };

                        img.onerror = function() {
                            this.alt = '图片加载失败';
                            this.closest('.gallery-item').classList.remove('loading');
                        }
                    }
                });
            }

            // 更新加载指示器
            function updateLoadingIndicator() {
                loadingIndicator.textContent = `已加载: ${loadedCount} / ${images.length}`;
            }

            // 初始化
            generateGallery();
            loadImages();

            // 监听滚动事件:保留函数引用,便于后续正确移除
            let scrollTimeout;
            function handleScroll() {
                if (scrollTimeout) {
                    clearTimeout(scrollTimeout);
                }
                scrollTimeout = setTimeout(loadImages, 100);
            }
            window.addEventListener('scroll', handleScroll);

            // 使用 Intersection Observer API(现代浏览器)
            if ('IntersectionObserver' in window) {
                const imageObserver = new IntersectionObserver(function(entries) {
                    entries.forEach(entry => {
                        if (entry.isIntersecting) {
                            const img = entry.target;
                            if (img.dataset.src) {
                                img.src = img.dataset.src;
                                img.removeAttribute('data-src');

                                img.onload = function() {
                                    img.closest('.gallery-item').classList.remove('loading');
                                    img.closest('.gallery-item').classList.add('loaded');
                                    loadedCount++;
                                    updateLoadingIndicator();
                                };

                                imageObserver.unobserve(img);
                            }
                        }
                    });
                }, {
                    rootMargin: '50px'
                });

                // 观察所有懒加载图片
                gallery.querySelectorAll('img[data-src]').forEach(img => {
                    imageObserver.observe(img);
                });

                // Intersection Observer 可替代滚动监听,移除手动滚动方案
                window.removeEventListener('scroll', handleScroll);
            }
        })();
    </script>
</body>
</html>

代码解释

  • HTML 结构

    • 固定头部标题
    • 图片容器(图片通过 JavaScript 动态生成)
    • 加载指示器(显示已加载图片数量)
  • CSS 样式

    • Flexbox 响应式布局(桌面端每行2张,移动端每行1张)
    • 加载中状态:半透明模糊效果
    • 加载完成状态:清晰显示,淡入过渡
  • JavaScript 核心功能

    • generateGallery():创建图片元素,使用 data-src 存储真实地址(不立即加载)
    • isInViewport(element):判断元素是否在视口中可见
    • loadImages()
      • 遍历所有懒加载图片
      • 如果图片进入视口,将 data-src 赋值给 src,触发实际加载
      • 加载成功后添加 loaded 类,移除 loading
    • 滚动监听:监听 scroll 事件,使用防抖(debounce)优化性能
    • Intersection Observer API (现代浏览器):
      • 更高效的懒加载方式
      • rootMargin: '50px':提前 50px 开始加载,提升用户体验
      • 自动监听元素进入/离开视口,无需手动监听滚动
  • 性能优化

    • 防抖处理滚动事件
    • 优先使用 Intersection Observer API
    • 图片加载后取消观察
  • 实际应用:电商网站商品图、新闻列表配图、社交平台图片流等

8.7 案例七:无缝滚动(scrollLeft 驱动)

应用场景 :横向图片带、品牌 Logo 墙、横向推荐列表。这个版本使用 scrollLeft 持续递增,最贴近 DOM 滚动属性的教学目标。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>scrollLeft 无缝滚动</title>
    <style>
        .wrapper {
            width: 800px;
            height: 200px;
            margin: 100px auto;
            display: flex;
            overflow: hidden;
            border-radius: 12px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, .16);
        }
        .wrapper img {
            width: 200px;
            height: 200px;
            flex: 0 0 200px;
            object-fit: cover;
        }
    </style>
</head>
<body>
    <div id="box" class="wrapper">
        <img src="images/db01.jpg" alt="图片 1">
        <img src="images/db02.jpg" alt="图片 2">
        <img src="images/db03.jpg" alt="图片 3">
        <img src="images/db04.jpg" alt="图片 4">
        <img src="images/db05.jpg" alt="图片 5">
        <img src="images/db06.jpg" alt="图片 6">
        <img src="images/db07.jpg" alt="图片 7">
        <img src="images/db08.jpg" alt="图片 8">
        <img src="images/db09.jpg" alt="图片 9">
        <img src="images/db10.jpg" alt="图片 10">
    </div>

    <script>
        (function () {
            const box = document.querySelector('#box');
            box.innerHTML += box.innerHTML;

            setInterval(function () {
                box.scrollLeft += 1;

                if (box.scrollLeft >= box.scrollWidth / 2) {
                    box.scrollLeft = 0;
                }
            }, 10);
        })();
    </script>
</body>
</html>

代码解释

  • overflow: hidden 让容器只显示固定宽度区域,超出的图片被裁掉。
  • box.innerHTML += box.innerHTML 复制一份图片,前后内容一致才能在重置滚动位置时保持视觉连续。
  • scrollLeft += 1 每 10ms 向右滚动 1px。
  • 当滚动到复制内容的一半时,将 scrollLeft 重置为 0,用户看到的是连续循环。
  • 网站应用:商品推荐横滑带、合作品牌展示、图片预览带、公告素材滚动。

8.8 案例八:无缝滚动(CSS 动画驱动)

应用场景:轮播图、新闻滚动、合作伙伴展示等。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>无缝滚动示例</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: Arial, sans-serif;
            background: #f5f5f5;
            padding: 50px 20px;
        }

        .marquee-container {
            max-width: 1000px;
            margin: 0 auto;
            background: #fff;
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 5px 20px rgba(0,0,0,0.1);
        }

        .marquee-header {
            padding: 20px;
            text-align: center;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }

        .marquee-wrapper {
            display: flex;
            overflow: hidden;
            position: relative;
        }

        .marquee-wrapper::before,
        .marquee-wrapper::after {
            content: '';
            position: absolute;
            top: 0;
            bottom: 0;
            width: 50px;
            z-index: 10;
            pointer-events: none;
        }

        .marquee-wrapper::before {
            left: 0;
            background: linear-gradient(to right, #fff, transparent);
        }

        .marquee-wrapper::after {
            right: 0;
            background: linear-gradient(to left, #fff, transparent);
        }

        .marquee-content {
            display: flex;
            animation: scroll 20s linear infinite;
        }

        .marquee-content:hover {
            animation-play-state: paused;
        }

        .marquee-item {
            flex: 0 0 200px;
            height: 150px;
            margin-right: 10px;
            border-radius: 8px;
            overflow: hidden;
            position: relative;
        }

        .marquee-item img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }

        .marquee-item .overlay {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            padding: 10px;
            background: rgba(0,0,0,0.7);
            color: white;
            font-size: 14px;
            text-align: center;
        }

        @keyframes scroll {
            0% {
                transform: translateX(0);
            }
            100% {
                transform: translateX(-50%);
            }
        }

        .controls {
            padding: 20px;
            text-align: center;
            display: flex;
            justify-content: center;
            gap: 10px;
        }

        .controls button {
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            transition: background 0.3s;
        }

        .btn-left {
            background: #4CAF50;
            color: white;
        }
        .btn-left:hover {
            background: #45a049;
        }

        .btn-right {
            background: #2196F3;
            color: white;
        }
        .btn-right:hover {
            background: #0b7dda;
        }

        .btn-pause {
            background: #ff9800;
            color: white;
        }
        .btn-pause:hover {
            background: #e68900;
        }

        /* 垂直滚动样式 */
        .vertical-marquee {
            max-width: 300px;
            margin: 50px auto;
            height: 200px;
            overflow: hidden;
            border-radius: 10px;
            box-shadow: 0 5px 20px rgba(0,0,0,0.1);
        }

        .vertical-content {
            animation: verticalScroll 10s linear infinite;
        }

        .vertical-content:hover {
            animation-play-state: paused;
        }

        .vertical-item {
            height: 50px;
            display: flex;
            align-items: center;
            padding: 0 20px;
            background: #fff;
            border-bottom: 1px solid #eee;
        }

        @keyframes verticalScroll {
            0% {
                transform: translateY(0);
            }
            100% {
                transform: translateY(-50%);
            }
        }
    </style>
</head>
<body>
    <!-- 水平无缝滚动 -->
    <div class="marquee-container">
        <div class="marquee-header">
            <h2>水平无缝滚动</h2>
            <p>鼠标悬停暂停滚动</p>
        </div>
        <div class="marquee-wrapper">
            <div class="marquee-content" id="horizontalMarquee">
                <!-- 内容通过 JavaScript 动态生成 -->
            </div>
        </div>
        <div class="controls">
            <button class="btn-left" id="scrollLeft">向左</button>
            <button class="btn-pause" id="togglePause">暂停</button>
            <button class="btn-right" id="scrollRight">向右</button>
        </div>
    </div>

    <!-- 垂直无缝滚动 -->
    <div class="marquee-container vertical-marquee">
        <div class="marquee-header">
            <h3>垂直无缝滚动</h3>
        </div>
        <div class="vertical-content" id="verticalMarquee">
            <!-- 内容通过 JavaScript 动态生成 -->
        </div>
    </div>

    <script>
        (function() {
            // 水平滚动数据
            const horizontalItems = [
                { src: 'images/db01.jpg', title: '图片 1' },
                { src: 'images/db02.jpg', title: '图片 2' },
                { src: 'images/db03.jpg', title: '图片 3' },
                { src: 'images/db04.jpg', title: '图片 4' },
                { src: 'images/db05.jpg', title: '图片 5' },
                { src: 'images/db06.jpg', title: '图片 6' },
                { src: 'images/db07.jpg', title: '图片 7' },
                { src: 'images/db08.jpg', title: '图片 8' },
            ];

            // 垂直滚动数据
            const verticalItems = [
                '最新消息:JavaScript 新特性发布',
                '热门文章:深入理解异步编程',
                '技术分享:前端性能优化技巧',
                '课程推荐:Vue.js 实战教程',
                '工具介绍:VS Code 高效插件',
                '行业动态:Web 技术发展趋势',
            ];

            // 生成水平滚动内容
            function generateHorizontalMarquee() {
                const marquee = document.querySelector('#horizontalMarquee');
                // 复制一份内容实现无缝效果
                const allItems = [...horizontalItems, ...horizontalItems];

                allItems.forEach(item => {
                    const div = document.createElement('div');
                    div.className = 'marquee-item';
                    div.innerHTML = `
                        <img src="${item.src}" alt="${item.title}">
                        <div class="overlay">${item.title}</div>
                    `;
                    marquee.appendChild(div);
                });
            }

            // 生成垂直滚动内容
            function generateVerticalMarquee() {
                const marquee = document.querySelector('#verticalMarquee');
                const allItems = [...verticalItems, ...verticalItems];

                allItems.forEach(text => {
                    const div = document.createElement('div');
                    div.className = 'vertical-item';
                    div.textContent = text;
                    marquee.appendChild(div);
                });
            }

            // 水平滚动控制
            const horizontalMarquee = document.querySelector('#horizontalMarquee');
            const scrollLeftBtn = document.querySelector('#scrollLeft');
            const scrollRightBtn = document.querySelector('#scrollRight');
            const togglePauseBtn = document.querySelector('#togglePause');
            let isPaused = false;

            scrollLeftBtn.addEventListener('click', function() {
                horizontalMarquee.style.animationDirection = 'normal';
            });

            scrollRightBtn.addEventListener('click', function() {
                horizontalMarquee.style.animationDirection = 'reverse';
            });

            togglePauseBtn.addEventListener('click', function() {
                isPaused = !isPaused;
                horizontalMarquee.style.animationPlayState = isPaused ? 'paused' : 'running';
                this.textContent = isPaused ? '继续' : '暂停';
            });

            // 初始化
            generateHorizontalMarquee();
            generateVerticalMarquee();
        })();
    </script>
</body>
</html>

代码解释

  • HTML 结构

    • 水平滚动容器:图片横向滚动展示
    • 垂直滚动容器:新闻/消息纵向滚动展示
    • 控制按钮:控制滚动方向和暂停状态
  • CSS 核心技术

    • @keyframes scroll 动画:定义滚动动作
    • animation-play-state: paused:鼠标悬停时暂停滚动
    • 渐变遮罩(::before/::after):两侧边缘淡入淡出效果
    • 复制内容实现无缝:内容列表复制一份,动画移动到一半时重置
  • JavaScript 核心功能

    • generateHorizontalMarquee():生成水平滚动内容,复制一份实现无缝效果
    • generateVerticalMarquee():生成垂直滚动内容
    • 方向控制 :通过 animationDirection 改变滚动方向(normal 正向,reverse 反向)
    • 暂停控制 :通过 animationPlayState 切换动画播放状态(running/paused
  • 无缝滚动原理

    • 将内容复制一份拼接在后面
    • 动画从 0% 移动到 -50%
    • 动画循环播放时,由于 -50% 位置与 0% 位置内容相同,视觉上形成无缝效果
  • 实际应用:轮播图、新闻滚动、合作伙伴展示、通知公告、促销信息展示等

8.9 延伸练习:随机点名器与选项卡

随机点名器

应用场景:学习互动、抽奖、任务分配、随机推荐。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>随机点名器</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            min-height: 100vh;
            justify-content: center;
            align-items: center;
            background: #f5f7fb;
        }
        .card {
            width: 420px;
            padding: 32px;
            text-align: center;
            border-radius: 16px;
            background: #fff;
            box-shadow: 0 12px 40px rgba(0, 0, 0, .12);
        }
        #name {
            margin: 24px 0;
            font-size: 48px;
            color: #4f46e5;
            font-weight: 700;
        }
        button {
            padding: 10px 20px;
            margin: 0 6px;
            border: none;
            border-radius: 8px;
            background: #4f46e5;
            color: #fff;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="card">
        <h1>随机点名器</h1>
        <div id="name">准备开始</div>
        <button id="startBtn">开始</button>
        <button id="stopBtn">停止</button>
    </div>

    <script>
        const names = ['小明', '小红', '小刚', '小丽', '小乐', '小安'];
        const nameBox = document.querySelector('#name');
        let timerId = null;

        document.querySelector('#startBtn').onclick = function () {
            clearInterval(timerId);
            timerId = setInterval(function () {
                const index = Math.floor(Math.random() * names.length);
                nameBox.textContent = names[index];
            }, 80);
        };

        document.querySelector('#stopBtn').onclick = function () {
            clearInterval(timerId);
        };
    </script>
</body>
</html>

代码解释

  • Math.random() 生成 [0, 1) 之间的随机数。
  • Math.floor(Math.random() * names.length) 将随机数转换为数组下标。
  • setInterval() 让名字快速切换,形成滚动效果;clearInterval() 停止切换。
  • 网站应用:抽奖组件、推荐卡片随机刷新、在线学习互动工具。
选项卡效果

应用场景:商品详情页、个人中心设置页、文档分栏阅读、后台配置面板。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>选项卡示例</title>
    <style>
        .tabs {
            width: 600px;
            margin: 60px auto;
            border: 1px solid #e5e7eb;
            border-radius: 12px;
            overflow: hidden;
            font-family: Arial, sans-serif;
        }
        .tab-header {
            display: flex;
            background: #f3f4f6;
        }
        .tab-btn {
            flex: 1;
            padding: 14px;
            border: none;
            background: transparent;
            cursor: pointer;
            font-size: 16px;
        }
        .tab-btn.active {
            background: #fff;
            color: #2563eb;
            font-weight: 700;
        }
        .tab-panel {
            display: none;
            padding: 24px;
            line-height: 1.8;
        }
        .tab-panel.active {
            display: block;
        }
    </style>
</head>
<body>
    <div class="tabs">
        <div class="tab-header">
            <button class="tab-btn active" data-index="0">基础知识</button>
            <button class="tab-btn" data-index="1">案例实战</button>
            <button class="tab-btn" data-index="2">性能优化</button>
        </div>
        <div class="tab-panel active">DOM 基础包括节点、元素查询、内容读写、属性样式操作。</div>
        <div class="tab-panel">案例实战包括全选反选、倒计时、懒加载、无缝滚动。</div>
        <div class="tab-panel">性能优化重点是批量操作、事件委托、读写分离。</div>
    </div>

    <script>
        const buttons = document.querySelectorAll('.tab-btn');
        const panels = document.querySelectorAll('.tab-panel');

        buttons.forEach(function (button) {
            button.onclick = function () {
                const index = Number(this.dataset.index);

                buttons.forEach(function (btn) {
                    btn.classList.remove('active');
                });
                panels.forEach(function (panel) {
                    panel.classList.remove('active');
                });

                this.classList.add('active');
                panels[index].classList.add('active');
            };
        });
    </script>
</body>
</html>

代码解释

  • data-index 保存按钮对应内容面板的下标。
  • 点击按钮时,先移除所有按钮和面板的 active 类,再给当前按钮和对应面板添加 active
  • 样式层通过 .active 控制选中态与显示隐藏,JS 只负责切换状态。
  • 网站应用:商品详情「介绍/规格/评价」、文档页面「示例/参数/返回值」、后台配置页分组。

8.10 预习衔接:HTML DOM 与事件

Day13 的 DOM 操作是基础,后续 HTML DOM 与事件会继续围绕「元素对象」展开:

知识点 核心能力 与本文关系
表单元素 读写 valuecheckedselectedIndex 全选案例、倒计时输入框都依赖表单属性
表格元素 使用 rowscellsrowIndexcellIndex 适合做可编辑表格、成绩表、数据管理页
快速创建图片 new Image()、设置 src、监听加载状态 图片懒加载、轮播图预加载会用到
事件监听 HTML 属性、元素事件属性、addEventListener() 本文按钮点击、滚动监听、定时器控制都依赖事件
事件解绑 onclick = nullremoveEventListener() 避免重复监听与内存占用
事件流 捕获、目标、冒泡 事件委托的理论基础
回调中的 this 指向触发事件的元素或绑定上下文 全选框联动、按钮状态切换常用

9. 最佳实践与性能优化

9.1 性能优化建议

优化项 说明 示例
减少 DOM 操作 批量处理,避免频繁操作 使用 DocumentFragment
使用 textContent 比 innerText 性能更好 el.textContent = 'text'
避免布局抖动 读写分离 先读所有属性,再写
事件委托 减少事件监听器数量 在父元素上监听

9.2 DocumentFragment 批量操作

javascript 复制代码
// 低效方式:每次添加都触发回流
for (let i = 0; i < 100; i++) {
    const li = document.createElement('li');
    li.textContent = `Item ${i}`;
    document.querySelector('ul').appendChild(li);
}

// 高效方式:使用 DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
    const li = document.createElement('li');
    li.textContent = `Item ${i}`;
    fragment.appendChild(li);
}
document.querySelector('ul').appendChild(fragment);

代码解释

  • 低效方式问题

    • 每次调用 appendChild() 都会触发 DOM 回流(reflow)
    • 100 次循环导致 100 次回流,性能开销巨大
    • 回流会重新计算布局、绘制页面,是非常耗时的操作
  • 高效方式优化

    • DocumentFragment 是一个轻量级的文档对象,不在 DOM 树中
    • 在内存中批量添加元素,不会触发页面回流
    • 最后一次性将 fragment 添加到 DOM,只触发一次回流
    • 性能提升:从 100 次回流降到 1 次回流
  • 应用场景:大量元素创建、列表渲染、表格数据填充等

9.3 避免布局抖动

javascript 复制代码
// 不好的做法
function updateElements() {
    elements.forEach(el => {
        const height = el.offsetHeight;  // 读
        el.style.height = height + 10 + 'px';  // 写
        const width = el.offsetWidth;  // 读
        el.style.width = width + 10 + 'px';  // 写
    });
}

// 好的做法:读写分离
function updateElements() {
    // 先读取所有需要的数据
    const measurements = elements.map(el => ({
        height: el.offsetHeight,
        width: el.offsetWidth
    }));

    // 再进行写入操作
    elements.forEach((el, i) => {
        el.style.height = measurements[i].height + 10 + 'px';
        el.style.width = measurements[i].width + 10 + 'px';
    });
}

代码解释

  • 布局抖动问题

    • 读操作(如 offsetHeight)会强制浏览器同步计算布局
    • 写操作(如修改 style)会使之前的计算失效
    • 读写交替导致浏览器反复计算布局,严重影响性能
  • 读写分离优化

    • 第一阶段:收集所有需要读取的数据
    • 第二阶段:一次性进行所有写入操作
    • 浏览器只需要计算一次布局
  • 性能对比

    • 不好的做法:每个元素触发 2 次回流,n 个元素 = 2n 次回流
    • 好的做法:所有元素只触发 1 次回流

9.4 事件委托

javascript 复制代码
// 不好的做法:给每个按钮添加监听器
buttons.forEach(btn => {
    btn.addEventListener('click', handleClick);
});

// 好的做法:使用事件委托
container.addEventListener('click', function(e) {
    if (e.target.matches('.button')) {
        handleClick(e);
    }
});

代码解释

  • 传统方式问题

    • 每个按钮都绑定一个事件监听器
    • 100 个按钮 = 100 个监听器,内存占用大
    • 动态添加的按钮需要手动绑定事件
  • 事件委托优化

    • 只在父容器上绑定一个监听器
    • 利用事件冒泡机制,子元素的事件会传递到父元素
    • 通过 e.target 判断实际点击的元素
    • 动态添加的按钮自动拥有事件处理
  • 性能对比

    • 传统方式:O(n) 个监听器,n 为按钮数量
    • 事件委托:O(1) 个监听器,无论多少按钮
    • 内存占用大幅减少,初始化更快
  • 注意事项 :需要使用 matches()contains() 正确判断事件目标

9.5 选择器性能

javascript 复制代码
// 从快到慢
// 1. ID 选择器
document.getElementById('id');

// 2. 类选择器
document.getElementsByClassName('class');

// 3. 标签选择器
document.getElementsByTagName('div');

// 4. querySelector(复杂选择器)
document.querySelector('#id .class');

代码解释

  • ID 选择器(最快)

    • getElementById() 是浏览器原生优化程度最高的方法
    • 浏览器维护 ID 到元素的映射表,查询时间复杂度接近 O(1)
  • 类选择器(较快)

    • getElementsByClassName() 返回动态集合
    • 现代浏览器对此有专门优化
  • 标签选择器(中等)

    • getElementsByTagName() 返回所有指定标签的元素
    • 需要遍历 DOM 树,速度相对较慢
  • querySelector(最慢)

    • 需要解析 CSS 选择器字符串
    • 复杂选择器(如后代选择器)性能更差
    • 但提供了最灵活的查询方式
  • 选择建议

    • 优先使用 ID 选择器
    • 批量操作使用 getElementsByXXX()
    • 复杂查询才使用 querySelector()

9.6 常见坑点与避坑指南

即便熟悉了所有 API,实战中仍会遇到一些反直觉的行为。下面整理了 8 个高频踩坑场景,每个都附上原因分析和修复方案。


坑 1:innerHTML += 会重建整个子树

javascript 复制代码
// ❌ 每次 += 都会序列化全部 HTML、再全量解析重建
container.innerHTML += '<li>新项目</li>';

// ✅ 只插入新节点,已有内容不受影响
const li = document.createElement('li');
li.textContent = '新项目';
container.appendChild(li);

原因+= 先将现有 innerHTML 转成字符串、拼接新内容、再整体重新解析成 DOM。原有子节点的事件监听器全部丢失,且触发全量回流。循环中使用会让性能急剧下降。


坑 2:removeEventListener 必须传相同函数引用

javascript 复制代码
// ❌ 匿名函数无法被移除,每次调用都注册了新监听器
btn.addEventListener('click', function() { doSomething(); });
btn.removeEventListener('click', function() { doSomething(); }); // 无效!

// ✅ 保存函数引用
function handleClick() { doSomething(); }
btn.addEventListener('click', handleClick);
btn.removeEventListener('click', handleClick); // 正确移除

原因removeEventListener 通过引用相等(===)匹配监听器,两个语法相同的函数表达式是两个不同的对象。


坑 3:scrollLeft / scrollTop 设置后无效

javascript 复制代码
// ❌ 没有效果,因为元素的 overflow 是默认值 visible
el.scrollLeft = 100;

// ✅ 确保元素的 overflow 不为 visible
el.style.overflow = 'auto'; // 或 'scroll' / 'hidden'
el.scrollLeft = 100;

原因 :只有 overflowautoscrollhidden 的元素才会维护滚动位置。visible 时内容直接溢出,不存在滚动容器的概念。


坑 4:元素 display: none 时尺寸和位置全为 0

javascript 复制代码
el.style.display = 'none';
console.log(el.offsetWidth);  // 0 --- 元素不参与布局
console.log(el.offsetLeft);   // 0

// ✅ 获取"隐藏元素"尺寸的临时方案:先让它不可见但参与布局
el.style.visibility = 'hidden';
el.style.position = 'absolute';
el.style.display = 'block';
const width = el.offsetWidth;
el.style.display = 'none';
el.style.visibility = '';
el.style.position = '';

原因display: none 让元素从渲染树中移除,浏览器不再为其分配尺寸和位置,因此所有几何属性都返回 0。


坑 5:getBoundingClientRect() 触发强制回流

javascript 复制代码
// ❌ 循环内多次读几何属性,每次都强制同步布局
elements.forEach(el => {
    const h = el.getBoundingClientRect().height; // 强制回流
    el.style.height = h + 10 + 'px';             // 使布局失效
});

// ✅ 先批量读,再批量写
const heights = elements.map(el => el.getBoundingClientRect().height);
elements.forEach((el, i) => {
    el.style.height = heights[i] + 10 + 'px';
});

原因 :任何读取布局信息(offsetWidthclientHeightgetBoundingClientRect()等)的操作,都会强制浏览器先完成挂起的样式计算,然后才能返回准确值。读写交替即"布局抖动"。


坑 6:querySelectorAll 返回的是静态快照,getElementsBy* 是动态集合

javascript 复制代码
const staticList  = document.querySelectorAll('li');  // 静态 NodeList,不随 DOM 变化
const dynamicList = document.getElementsByTagName('li'); // 动态 HTMLCollection

const li = document.createElement('li');
document.querySelector('ul').appendChild(li);

console.log(staticList.length);  // 插入前的数量,不变
console.log(dynamicList.length); // 已包含新插入的 li,自动更新

原因HTMLCollection 内部直接引用 DOM,每次访问 length / 下标都重新查询;NodeList(由 querySelectorAll 返回)是查询时的快照,后续 DOM 变化不影响它。


坑 7:cloneNode 不复制事件监听器

javascript 复制代码
btn.addEventListener('click', () => alert('点击'));
const clone = btn.cloneNode(true); // 深克隆,包含子节点
document.body.appendChild(clone);
// clone 点击后不会弹出 alert --- 事件监听器没有被复制

// ✅ 克隆后重新绑定事件
clone.addEventListener('click', () => alert('点击'));

原因cloneNode 复制元素的 HTML 结构和属性,但事件监听器存储在 JS 层(不是 HTML 属性),因此不会随克隆一起复制。


坑 8:outerHTML 赋值后原变量引用失效

javascript 复制代码
const box = document.querySelector('#box');
box.outerHTML = '<div id="box">新内容</div>'; // 替换了元素自身

console.log(box.textContent); // 仍然是"原始内容"!box 指向已移除的旧元素

// ✅ 赋值后重新查询
const newBox = document.querySelector('#box');
console.log(newBox.textContent); // "新内容"

原因outerHTML = ... 将当前节点从 DOM 中移除并插入新节点。JS 变量 box 保存的是对旧节点对象的引用,该对象已脱离文档树,但并未被垃圾回收,读取其属性仍然返回旧值。


坑点速查表

坑点 根本原因 修复方向
innerHTML += 丢失事件 全量序列化重建 DOM 改用 createElement + appendChild
removeEventListener 无效 匿名函数引用不等 保存函数引用再传入
scrollLeft 设置无效 overflow: visible 无滚动容器 设置 overflow: auto/scroll/hidden
尺寸位置全为 0 display: none 不参与布局 改用 visibility: hidden 或临时显示
布局抖动 读写几何属性交替触发回流 读写分离,先批量读再批量写
querySelectorAll 不更新 返回静态 NodeList 快照 动态列表用 getElementsBy*
克隆后事件失效 cloneNode 不复制 JS 监听器 克隆后重新 addEventListener
outerHTML 后变量失效 旧节点已移出 DOM 树 赋值后重新 querySelector

10. 参考资源

10.1 官方文档

10.2 延伸阅读与实践文章

10.3 相关知识图谱

DOM 操作
内容操作
innerHTML
outerHTML
innerText
textContent
尺寸获取
offsetWidth
clientWidth
scrollWidth
getBoundingClientRect
位置获取
offsetLeft
offsetTop
clientLeft
clientTop
节点操作
createElement
appendChild
insertBefore
removeChild
replaceChild
cloneNode
滚动控制
scrollTop
scrollLeft
scrollTo
scrollIntoView
属性与样式
setAttribute
dataset
style
getComputedStyle
classList
实战案例
全选反选
倒计时
图片懒加载
无缝滚动
随机点名器
选项卡

10.4 常用网站应用

网站类型 使用的 DOM 技术
电商网站 懒加载、购物车操作、筛选排序
社交媒体 无限滚动、动态内容加载、消息提示
在线文档 目录导航、搜索高亮、代码折叠
数据可视化 图表渲染、数据筛选、交互反馈
表单系统 动态验证、字段联动、数据提交

11. 经典使用场景与业务价值

DOM 操作真正的价值不只是「改页面」,而是把静态 HTML 转化为可交互、可反馈、可增长的业务界面。下面从常见业务场景出发,总结 DOM 技术的落地方式与价值。

11.1 电商商品列表:懒加载、筛选、批量选择

典型场景:商品搜索页、分类页、购物车、收藏夹。

使用的 DOM 能力

  • querySelectorAll() 获取商品卡片、复选框、筛选项。
  • dataset 存储商品 ID、价格、分类等业务字段。
  • classList 切换选中、禁用、加载中、售罄等状态。
  • getBoundingClientRect()IntersectionObserver 判断图片是否进入视口。
  • textContent 更新价格、数量、已选商品数。

业务价值

  • 图片懒加载减少首屏资源请求,提升页面打开速度。
  • 批量选择提升购物车、收藏夹、后台商品管理的操作效率。
  • 通过实时筛选和状态反馈减少用户等待,提升转化率。
  • 对移动端用户更友好,减少无效流量消耗。

落地建议

  • 首屏关键图片不要懒加载,避免影响核心内容展示。
  • 商品 ID、分类等业务信息适合放在 data-* 属性中,便于事件处理时读取。
  • 大量商品渲染时配合 DocumentFragment 或分页加载,避免一次性插入过多 DOM。

11.2 内容社区与资讯流:无限滚动、动态插入、状态提示

典型场景:文章列表、动态流、评论区、消息中心。

使用的 DOM 能力

  • scrollTopscrollHeightclientHeight 判断是否接近底部。
  • createElement()appendChild() 动态插入新内容。
  • innerHTML 渲染可信模板,textContent 渲染用户生成内容。
  • classList 控制加载中、空状态、错误提示。
  • 事件委托处理动态新增的点赞、收藏、展开按钮。

业务价值

  • 无限滚动降低用户继续浏览的操作成本。
  • 动态插入评论和消息,让页面反馈更即时。
  • 事件委托减少监听器数量,适合内容持续增长的列表。
  • 空状态、加载状态、错误状态能减少用户困惑,提升体验稳定性。

落地建议

  • 用户输入必须使用 textContent 或经过安全处理后再渲染。
  • 无限滚动要保留分页兜底,便于定位内容和恢复浏览位置。
  • 长列表需要考虑虚拟列表,否则 DOM 节点过多会影响滚动性能。

11.3 管理后台:表格操作、批量处理、状态联动

典型场景:用户管理、订单管理、权限配置、数据审核。

使用的 DOM 能力

  • 复选框 checkedindeterminate 实现全选、半选、反选。
  • parentElementchildrennextElementSibling 在表格行和操作区之间定位。
  • replaceChild() 更新某个单元格或状态标签。
  • removeChild() 删除行,cloneNode() 复制模板行。
  • classList 切换成功、警告、禁用、选中等状态。

业务价值

  • 批量操作能明显提升后台处理效率。
  • 半选状态让权限树、菜单树、表格多选更准确。
  • 即时状态反馈减少重复提交和误操作。
  • DOM 局部更新可以避免整页刷新,提升后台系统的响应速度。

落地建议

  • 批量操作前要同步维护选中数量和按钮状态。
  • 删除、替换 DOM 前最好先确认目标节点存在,避免空节点报错。
  • 权限类界面要把视觉选中状态与真实提交数据分开维护,避免状态不同步。

11.4 在线表单与活动页:校验、倒计时、引导提交

典型场景:报名表单、问卷、预约页、限时活动页。

使用的 DOM 能力

  • value 读取输入内容,checked 判断勾选状态。
  • textContent 显示校验结果和倒计时文案。
  • classList 切换输入框错误状态、按钮禁用状态。
  • setInterval() 配合 DOM 更新实现倒计时。
  • scrollIntoView() 将用户引导到错误字段或关键区域。

业务价值

  • 实时校验减少错误提交,提高表单完成率。
  • 倒计时增强时间感知,适合预约、抢购、限时提交。
  • 错误定位降低用户查找成本,减少放弃填写。
  • 按钮状态联动能避免重复提交和无效请求。

落地建议

  • 前端校验提升体验,但不能替代服务端校验。
  • 倒计时应以可信时间为准,重要业务不要只依赖本地时间。
  • 错误提示要写清楚"哪里错、怎么改",而不是只改变颜色。

11.5 在线文档与教程页面:目录导航、代码折叠、阅读进度

典型场景:技术文档、博客详情页、学习平台、接口说明页。

使用的 DOM 能力

  • querySelectorAll('h2, h3') 生成文章目录。
  • getBoundingClientRect() 判断标题是否进入视口。
  • classList 高亮当前阅读章节。
  • scrollTo()scrollIntoView() 实现平滑定位。
  • innerText 获取可见标题文本,textContent 获取完整文本。

业务价值

  • 目录导航降低长文阅读成本。
  • 当前章节高亮帮助用户建立位置感。
  • 代码折叠让内容更紧凑,提升阅读效率。
  • 阅读进度能增强完成感,适合长教程和学习产品。

落地建议

  • 目录生成要过滤空标题,避免无意义导航项。
  • 滚动监听需要节流或使用 IntersectionObserver,避免频繁计算布局。
  • 锚点跳转要考虑吸顶导航高度,防止标题被遮挡。

11.6 图片与媒体展示:轮播、预览、无缝滚动

典型场景:首页 Banner、商品图预览、作品集、合作品牌墙。

使用的 DOM 能力

  • scrollLeft 控制横向滚动。
  • cloneNode() 或复制内容实现无缝循环。
  • classList 控制当前图、缩略图、暂停状态。
  • style.transform 与 CSS animation 实现位移和动效。
  • onload / onerror 处理图片加载成功与失败。

业务价值

  • 轮播和品牌墙能集中展示重点内容。
  • 图片预览提升商品详情、作品展示的理解效率。
  • 加载失败兜底减少页面破碎感。
  • 悬停暂停、手动切换提升用户控制感。

落地建议

  • 自动轮播要提供暂停或手动控制,避免干扰阅读。
  • 图片必须写 alt,既利于可访问性,也利于加载失败时展示说明。
  • 无缝滚动要确保复制内容宽度计算准确,否则会出现跳动。

11.7 业务价值总览

业务目标 对应 DOM 技术 可带来的价值
提升首屏速度 图片懒加载、按需渲染、DocumentFragment 减少资源请求和渲染压力
提高操作效率 全选/反选、事件委托、批量 DOM 更新 降低重复操作成本
增强用户反馈 classList 状态切换、textContent 文案更新 让用户知道当前发生了什么
降低出错概率 表单校验、错误定位、按钮禁用 减少无效提交和误操作
提升内容消费 目录导航、阅读进度、代码折叠 降低长内容阅读门槛
支撑动态业务 createElement、appendChild、replaceChild 让页面能响应数据变化
优化性能体验 读写分离、事件委托、IntersectionObserver 减少卡顿,提升交互流畅度

总结一句话:DOM 操作的业务价值,核心在于把「页面展示」变成「可交互的业务流程」。掌握 DOM,不只是掌握几个 API,而是理解如何用结构、状态、事件和性能优化共同服务用户体验。


12. 知识点归纳总结

12.1 API 速查手册

内容读写

API 读/写 解析 HTML 受 CSS 影响 适用场景
innerHTML 读写 渲染可信 HTML 模板
outerHTML 读写 完全替换元素自身
innerText 读写 获取用户可见文本
textContent 读写 安全写入纯文本(推荐)

尺寸属性

API 包含内容 包含 padding 包含 border 包含溢出 只读
offsetWidth/Height
clientWidth/Height
scrollWidth/Height
getBoundingClientRect().width

位置属性

API 参照物 说明
offsetLeft/Top 定位祖先元素 到最近定位祖先的距离
clientLeft/Top 元素自身 左/上边框宽度
getBoundingClientRect().left/top 视口 元素到视口边缘的距离
getBoundingClientRect().left + scrollX 页面 元素到页面原点的距离

滚动控制

API 说明 前提条件
el.scrollLeft/Top 读写元素内容滚动位置 overflowvisible
document.documentElement.scrollTop 页面滚动距离 ---
window.scrollTo({ behavior:'smooth' }) 平滑滚动到绝对位置 ---
window.scrollBy({ top:100 }) 相对滚动 ---
el.scrollIntoView({ behavior:'smooth' }) 滚动使元素可见 ---

节点操作

API 说明
document.createElement(tag) 创建元素节点
parent.appendChild(node) 末尾插入子节点
parent.insertBefore(new, ref) 在 ref 前插入节点
parent.removeChild(node) 删除子节点
parent.replaceChild(new, old) 替换子节点
el.cloneNode(true/false) 深/浅克隆节点
document.createDocumentFragment() 创建文档片段(批量操作)

12.2 面试高频考点

Q1:innerHTMLtextContent 有什么区别?什么时候用哪个?

innerHTML 会将字符串解析为 HTML 并插入 DOM,支持富文本但存在 XSS 风险;textContent 把内容作为纯文本处理,不解析 HTML,更安全也更快(不触发额外样式计算)。

选用原则 :渲染来自服务端的可信 HTML 模板用 innerHTML;渲染用户输入、纯文本内容一律用 textContent


Q2:offsetWidth / clientWidth / scrollWidth 三者的区别?

  • offsetWidthcontent + padding + border,最常用,表示元素占据的实际尺寸。
  • clientWidthcontent + padding,不含边框,表示内容可绘制区域。
  • scrollWidthcontent + padding + 溢出部分,包含因 overflow 被裁掉的内容。

判断是否有溢出:el.scrollWidth > el.clientWidth


Q3:offsetLeft 的参照物是什么?如何获取元素相对于页面的位置?

offsetLeft 相对于最近的定位祖先(positionstatic),若无则相对于文档根元素。

获取页面绝对位置推荐用:

javascript 复制代码
const rect = el.getBoundingClientRect();
const x = rect.left + window.scrollX;
const y = rect.top  + window.scrollY;

Q4:如何实现图片懒加载?有哪些方案?

方案 原理 特点
scroll + getBoundingClientRect() 监听滚动,判断 rect.top < clientHeight 兼容性好,需节流优化
IntersectionObserver 浏览器自动检测元素进出视口 性能更优,推荐现代项目
<img loading="lazy"> 原生 HTML 属性 最简单,浏览器控制策略

Q5:为什么频繁操作 DOM 会慢?如何优化?

每次读写影响布局的属性(offsetHeightgetBoundingClientRect 等),浏览器必须完成"样式计算 → 布局 → 绘制"的完整流程(回流)。读写交替会导致多次回流,称为布局抖动

优化方案:

  1. 读写分离:先批量读取所有几何属性,再批量写入。
  2. DocumentFragment:批量创建节点后一次性插入 DOM。
  3. 事件委托:用一个监听器覆盖多个子元素,减少监听器数量。
  4. IntersectionObserver :代替手动监听 scroll 事件判断可见性。

Q6:querySelectorAllgetElementsByClassName 有什么区别?

querySelectorAll 返回静态 NodeList(调用时的快照),后续 DOM 变化不影响它。getElementsByClassName 返回动态 HTMLCollection,会实时反映 DOM 变化。

javascript 复制代码
// 如果在遍历过程中修改了 DOM,动态集合可能导致死循环或跳过元素
// 安全做法:用 querySelectorAll 或将动态集合先转为数组
const arr = Array.from(document.getElementsByClassName('item'));

Q7:事件委托是什么?解决了什么问题?

事件委托是将子元素的事件监听器绑定在父元素上,利用事件冒泡机制捕获子元素触发的事件。

解决了两个问题:

  • 性能:100 个子元素只需 1 个监听器,减少内存占用。
  • 动态元素:后续动态插入的子元素无需重新绑定事件,自动被父元素捕获。
javascript 复制代码
list.addEventListener('click', function(e) {
    const item = e.target.closest('.list-item');
    if (item) {
        console.log('点击了:', item.dataset.id);
    }
});

Q8:cloneNode(true)cloneNode(false) 的区别?克隆后事件为什么丢失?

  • cloneNode(true):深克隆,包含所有后代节点和属性。
  • cloneNode(false):浅克隆,只克隆元素本身,不包含子节点。

事件监听器存储在 JavaScript 引擎的内部结构中,不属于 DOM 属性,cloneNode 只复制 DOM 结构和 HTML 属性,因此事件监听器不会被复制。克隆后需要手动重新绑定事件。


12.3 学习路径建议

HTML + CSS 基础
JS 核心语法
DOM 基础操作

本文内容
DOM 事件系统

事件流·委托·自定义事件
BOM

window·location·history
异步编程

Promise·Fetch·async/await
前端框架

React / Vue

掌握 DOM 是打通"静态页面 → 交互应用"的关键节点。不要急于跳进框架,把 DOM 的原理吃透,学框架时会快很多------因为框架本质上是在 DOM 操作之上建立的高层抽象。


12.4 知识体系回顾

本文全面介绍了 JavaScript DOM 操作的核心知识点:

  1. 名词与原理:DOM 树、节点类型、回流重绘、视口、事件委托等核心概念
  2. 内容读写innerHTML / outerHTML / innerText / textContent 的区别与选用场景
  3. 元素尺寸offset / client / scroll 三族 + getBoundingClientRect 完整对比
  4. 元素位置offsetLeft/TopclientLeft/Top、视口坐标与页面坐标转换
  5. 滚动控制scrollLeft/Top 读写、scrollTo / scrollBy / scrollIntoView 平滑滚动
  6. 节点操作:创建、插入、删除、替换、克隆的完整 API 与 TodoList 实战
  7. document 对象title / cookie / createElement 等核心属性方法
  8. 实战案例:全选反选、倒计时、图片懒加载、无缝滚动、随机点名器、选项卡
  9. 性能优化:DocumentFragment、读写分离、事件委托、选择器性能
  10. 常见坑点:8 个高频踩坑场景及修复方案
  11. 业务价值:6 大真实业务场景的 DOM 落地方式

通过掌握这些 DOM 操作技巧,开发者可以构建出功能丰富、性能优异的交互式网页应用,并在遇到问题时快速定位根本原因。


本文所有代码示例均为完整可运行的 HTML 文件,建议复制到本地浏览器中亲自调试。遇到疑问时优先查阅 MDN 文档,它是最权威的一手资料。

相关推荐
Darling噜啦啦8 小时前
前端三权分立与AI编程工具实践:从Clock案例看现代前端开发
前端
難釋懷8 小时前
Redis内存回收-内存淘汰策略
前端·数据库·redis
用户900305093628 小时前
2026年Cursor平替工具推荐:免费高性价比替代方案
前端
我头上有犄角ovo8 小时前
HarmonyOS 测肤拍照页实战:Metadata 实时取景 + Core Vision 拍后校验,从 0.001 的 widthRatio 踩坑到可上线
前端·harmonyos
画画的阿飞8 小时前
里程碑三:基于 Vue3 领域模型架构建设
前端·node.js
玉米Yvmi8 小时前
大文件上传的基石:切片上传原理与实现详解
前端·javascript·面试
Gauss松鼠会8 小时前
【GaussDB】GaussDB 常见问题及解决方案汇总
java·数据库·算法·性能优化·gaussdb·经验总结
Brilliantwxx9 小时前
【C++】深度剖析 · 继承 (虚基表+虚函数表)
开发语言·c++
用户4099322502129 小时前
Composable的命名规矩和参数约定,别再瞎写了
前端·javascript·后端