DAY_12JavaScript DOM 完全指南(二):实战与性能篇

JavaScript DOM 完全指南(二):实战与性能篇

本篇由《JavaScript DOM 完全指南》拆分而来,覆盖七至十二章。通过常见交互案例理解 DOM 事件、渲染流程、性能优化与现代 Observer API。

目录

  • 七、实战案例
    • [7.1 购物车全选功能](#7.1 购物车全选功能)
    • [7.2 电子时钟](#7.2 电子时钟)
    • [7.3 选项卡切换](#7.3 选项卡切换)
    • [7.4 全屏分页滚动](#7.4 全屏分页滚动)
    • [7.5 列表复选框:全选、全不选与反选](#7.5 列表复选框:全选、全不选与反选)
    • [7.6 主复选框联动子项](#7.6 主复选框联动子项)
  • 八、最佳实践与性能优化
    • [8.1 DOM 操作性能优化](#8.1 DOM 操作性能优化)
    • [8.2 内存管理](#8.2 内存管理)
    • [8.3 现代 DOM API 推荐用法](#8.3 现代 DOM API 推荐用法)
  • [九、DOM 事件机制详解](#九、DOM 事件机制详解)
    • [9.1 事件流](#9.1 事件流)
    • [9.2 事件捕获与冒泡](#9.2 事件捕获与冒泡)
    • [9.3 事件委托](#9.3 事件委托)
    • [9.4 事件对象详解](#9.4 事件对象详解)
    • [9.5 preventDefault 与 stopPropagation](#9.5 preventDefault 与 stopPropagation)
    • [9.6 事件监听器选项](#9.6 事件监听器选项)
    • [9.7 常用 DOM 事件](#9.7 常用 DOM 事件)
    • [9.8 本章归纳](#9.8 本章归纳)
  • [十、DOM 渲染与性能深度解析](#十、DOM 渲染与性能深度解析)
    • [10.1 浏览器渲染流程](#10.1 浏览器渲染流程)
    • [10.2 回流(Reflow)与重绘(Repaint)](#10.2 回流(Reflow)与重绘(Repaint))
    • [10.3 强制同步布局(Layout Thrashing)](#10.3 强制同步布局(Layout Thrashing))
    • [10.4 减少回流与重绘的策略](#10.4 减少回流与重绘的策略)
    • [10.5 渲染性能优化实战](#10.5 渲染性能优化实战)
    • [10.6 本章归纳](#10.6 本章归纳)
  • [十一、现代 DOM API](#十一、现代 DOM API)
    • [11.1 IntersectionObserver - 交叉观察器](#11.1 IntersectionObserver - 交叉观察器)
    • [11.2 MutationObserver - 变化观察器](#11.2 MutationObserver - 变化观察器)
    • [11.3 ResizeObserver - 尺寸观察器](#11.3 ResizeObserver - 尺寸观察器)
    • [11.4 现代选择器与遍历 API](#11.4 现代选择器与遍历 API)
    • [11.5 本章归纳](#11.5 本章归纳)
  • 十二、高级实战案例
    • [12.1 虚拟滚动(Virtual Scroll)](#12.1 虚拟滚动(Virtual Scroll))
    • [12.2 拖放 API(Drag and Drop)](#12.2 拖放 API(Drag and Drop))
    • [12.3 无限滚动加载](#12.3 无限滚动加载)
    • [12.4 本章归纳](#12.4 本章归纳)

七、实战案例

本章案例覆盖:购物车联动、时钟、选项卡、全屏分页滚动、列表全选/反选、主复选框联动 ,均基于 DOM 获取、属性与 classList 等 API 组合实现。

7.1 购物车全选功能

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>购物车全选功能</title>
    <style>
        * {
            box-sizing: border-box;
        }
        body {
            font-family: Arial, sans-serif;
            background: #f5f5f5;
            padding: 20px;
        }
        .cart-container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        .cart-header {
            background: #2196F3;
            color: white;
            padding: 15px 20px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .cart-header h2 {
            margin: 0;
        }
        .cart-items {
            padding: 0;
            list-style: none;
        }
        .cart-item {
            display: flex;
            align-items: center;
            padding: 15px 20px;
            border-bottom: 1px solid #eee;
            transition: background 0.2s;
        }
        .cart-item:hover {
            background: #f9f9f9;
        }
        .cart-item input[type="checkbox"] {
            width: 20px;
            height: 20px;
            margin-right: 15px;
            cursor: pointer;
        }
        .item-image {
            width: 60px;
            height: 60px;
            margin-right: 15px;
            border-radius: 4px;
            background: #ddd;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24px;
        }
        .item-info {
            flex: 1;
        }
        .item-name {
            font-weight: bold;
            margin-bottom: 5px;
        }
        .item-price {
            color: #666;
        }
        .item-quantity {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .item-quantity button {
            width: 30px;
            height: 30px;
            border: 1px solid #ddd;
            background: white;
            cursor: pointer;
            border-radius: 4px;
        }
        .item-quantity button:hover {
            background: #f0f0f0;
        }
        .item-subtotal {
            width: 100px;
            text-align: right;
            font-weight: bold;
            color: #f44336;
        }
        .cart-footer {
            padding: 20px;
            background: #f9f9f9;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .select-all {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .select-all input {
            width: 18px;
            height: 18px;
            cursor: pointer;
        }
        .total-section {
            text-align: right;
        }
        .total-price {
            font-size: 24px;
            font-weight: bold;
            color: #f44336;
        }
        .checkout-btn {
            margin-left: 20px;
            padding: 12px 30px;
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            transition: background 0.2s;
        }
        .checkout-btn:hover {
            background: #45a049;
        }
        .checkout-btn:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
        .actions {
            display: flex;
            gap: 10px;
        }
        .action-btn {
            padding: 8px 15px;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .action-btn:hover {
            background: #0b7dda;
        }
    </style>
</head>
<body>
    <div class="cart-container">
        <div class="cart-header">
            <h2>购物车</h2>
            <div class="actions">
                <button class="action-btn" onclick="selectAll()">全选</button>
                <button class="action-btn" onclick="deselectAll()">取消</button>
                <button class="action-btn" onclick="invertSelection()">反选</button>
                <button class="action-btn" onclick="deleteSelected()">删除选中</button>
            </div>
        </div>

        <ul class="cart-items" id="cartItems">
            <li class="cart-item" data-price="99">
                <input type="checkbox" class="item-checkbox">
                <div class="item-image">📱</div>
                <div class="item-info">
                    <div class="item-name">iPhone 手机壳</div>
                    <div class="item-price">¥99.00</div>
                </div>
                <div class="item-quantity">
                    <button onclick="changeQuantity(this, -1)">-</button>
                    <span class="quantity">1</span>
                    <button onclick="changeQuantity(this, 1)">+</button>
                </div>
                <div class="item-subtotal">¥99.00</div>
            </li>
            <li class="cart-item" data-price="199">
                <input type="checkbox" class="item-checkbox">
                <div class="item-image">🎧</div>
                <div class="item-info">
                    <div class="item-name">蓝牙耳机</div>
                    <div class="item-price">¥199.00</div>
                </div>
                <div class="item-quantity">
                    <button onclick="changeQuantity(this, -1)">-</button>
                    <span class="quantity">1</span>
                    <button onclick="changeQuantity(this, 1)">+</button>
                </div>
                <div class="item-subtotal">¥199.00</div>
            </li>
            <li class="cart-item" data-price="49">
                <input type="checkbox" class="item-checkbox">
                <div class="item-image">🔌</div>
                <div class="item-info">
                    <div class="item-name">USB 数据线</div>
                    <div class="item-price">¥49.00</div>
                </div>
                <div class="item-quantity">
                    <button onclick="changeQuantity(this, -1)">-</button>
                    <span class="quantity">2</span>
                    <button onclick="changeQuantity(this, 1)">+</button>
                </div>
                <div class="item-subtotal">¥98.00</div>
            </li>
            <li class="cart-item" data-price="299">
                <input type="checkbox" class="item-checkbox">
                <div class="item-image">⌨️</div>
                <div class="item-info">
                    <div class="item-name">机械键盘</div>
                    <div class="item-price">¥299.00</div>
                </div>
                <div class="item-quantity">
                    <button onclick="changeQuantity(this, -1)">-</button>
                    <span class="quantity">1</span>
                    <button onclick="changeQuantity(this, 1)">+</button>
                </div>
                <div class="item-subtotal">¥299.00</div>
            </li>
            <li class="cart-item" data-price="149">
                <input type="checkbox" class="item-checkbox">
                <div class="item-image">🖱️</div>
                <div class="item-info">
                    <div class="item-name">无线鼠标</div>
                    <div class="item-price">¥149.00</div>
                </div>
                <div class="item-quantity">
                    <button onclick="changeQuantity(this, -1)">-</button>
                    <span class="quantity">1</span>
                    <button onclick="changeQuantity(this, 1)">+</button>
                </div>
                <div class="item-subtotal">¥149.00</div>
            </li>
        </ul>

        <div class="cart-footer">
            <div class="select-all">
                <input type="checkbox" id="selectAllCheckbox" onchange="toggleSelectAll()">
                <label for="selectAllCheckbox">全选</label>
            </div>
            <div class="total-section">
                <span>合计:</span>
                <span class="total-price" id="totalPrice">¥0.00</span>
                <button class="checkout-btn" id="checkoutBtn" onclick="checkout()" disabled>结算(0)</button>
            </div>
        </div>
    </div>

    <script>
        const cartItems = document.querySelectorAll('.cart-item');
        const selectAllCheckbox = document.getElementById('selectAllCheckbox');
        const totalPrice = document.getElementById('totalPrice');
        const checkoutBtn = document.getElementById('checkoutBtn');

        // 为每个复选框添加事件
        document.querySelectorAll('.item-checkbox').forEach(checkbox => {
            checkbox.addEventListener('change', function() {
                updateTotal();
                updateSelectAllCheckbox();
            });
        });

        function updateTotal() {
            let total = 0;
            let count = 0;

            cartItems.forEach(item => {
                const checkbox = item.querySelector('.item-checkbox');
                if (checkbox.checked) {
                    const price = parseFloat(item.dataset.price);
                    const quantity = parseInt(item.querySelector('.quantity').textContent);
                    total += price * quantity;
                    count++;
                }
            });

            totalPrice.textContent = `¥${total.toFixed(2)}`;
            checkoutBtn.textContent = `结算(${count})`;
            checkoutBtn.disabled = count === 0;
        }

        function updateSelectAllCheckbox() {
            const allChecked = Array.from(document.querySelectorAll('.item-checkbox'))
                .every(checkbox => checkbox.checked);
            selectAllCheckbox.checked = allChecked && document.querySelectorAll('.item-checkbox').length > 0;
        }

        function toggleSelectAll() {
            const checkboxes = document.querySelectorAll('.item-checkbox');
            checkboxes.forEach(checkbox => {
                checkbox.checked = selectAllCheckbox.checked;
            });
            updateTotal();
        }

        function selectAll() {
            document.querySelectorAll('.item-checkbox').forEach(checkbox => {
                checkbox.checked = true;
            });
            selectAllCheckbox.checked = true;
            updateTotal();
        }

        function deselectAll() {
            document.querySelectorAll('.item-checkbox').forEach(checkbox => {
                checkbox.checked = false;
            });
            selectAllCheckbox.checked = false;
            updateTotal();
        }

        function invertSelection() {
            document.querySelectorAll('.item-checkbox').forEach(checkbox => {
                checkbox.checked = !checkbox.checked;
            });
            updateSelectAllCheckbox();
            updateTotal();
        }

        function changeQuantity(button, delta) {
            const item = button.closest('.cart-item');
            const quantitySpan = item.querySelector('.quantity');
            const subtotalSpan = item.querySelector('.item-subtotal');
            const price = parseFloat(item.dataset.price);

            let quantity = parseInt(quantitySpan.textContent) + delta;
            if (quantity < 1) quantity = 1;
            if (quantity > 99) quantity = 99;

            quantitySpan.textContent = quantity;
            subtotalSpan.textContent = `¥${(price * quantity).toFixed(2)}`;

            updateTotal();
        }

        function deleteSelected() {
            const selectedItems = document.querySelectorAll('.item-checkbox:checked');
            if (selectedItems.length === 0) {
                alert('请先选择要删除的商品');
                return;
            }

            if (confirm(`确定要删除选中的 ${selectedItems.length} 件商品吗?`)) {
                selectedItems.forEach(checkbox => {
                    checkbox.closest('.cart-item').remove();
                });
                updateTotal();
                updateSelectAllCheckbox();
            }
        }

        function checkout() {
            const selectedItems = document.querySelectorAll('.item-checkbox:checked');
            alert(`结算成功!共 ${selectedItems.length} 件商品,总价 ${totalPrice.textContent}`);
        }
    </script>
</body>
</html>

代码解释: 数据流 :每行 .cart-itemdata-price (对应脚本里 dataset.price )存单价,数量在 DOM 文本节点里,勾选状态在 input.checkedupdateTotal 遍历所有行,仅对勾选行 price * quantity 累加,并驱动结算按钮 disabled 与文案。updateSelectAllCheckboxevery 判断是否全选,避免手写 for。toggleSelectAll 把主框状态广播到所有子框。invertSelection 逐框取反 checkedchangeQuantityclosest('.cart-item') 从按钮冒泡上下文定位所在行,再 querySelector 子节点,体现「局部根 + 相对查询」。deleteSelected:checked 伪类一次性取选中 input,再 remove() 整行------删除后别忘了调用总价与全选同步。工程扩展:大量商品时应把价格数量放内存模型,DOM 只作视图,避免双向数据源不一致。

7.2 电子时钟

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>电子时钟</title>
    <style>
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            font-family: 'Courier New', monospace;
        }
        .clock-container {
            text-align: center;
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            padding: 40px 60px;
            border-radius: 20px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
            border: 1px solid rgba(255, 255, 255, 0.2);
        }
        .clock {
            font-size: 72px;
            font-weight: bold;
            color: white;
            text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
            letter-spacing: 5px;
            margin: 20px 0;
        }
        .date {
            font-size: 24px;
            color: rgba(255, 255, 255, 0.8);
            margin-bottom: 10px;
        }
        .day {
            font-size: 18px;
            color: rgba(255, 255, 255, 0.6);
        }
        .format-toggle {
            margin-top: 30px;
        }
        .format-toggle button {
            padding: 10px 20px;
            margin: 0 5px;
            background: rgba(255, 255, 255, 0.2);
            color: white;
            border: 1px solid rgba(255, 255, 255, 0.3);
            border-radius: 20px;
            cursor: pointer;
            transition: all 0.3s ease;
        }
        .format-toggle button:hover {
            background: rgba(255, 255, 255, 0.3);
        }
        .format-toggle button.active {
            background: rgba(255, 255, 255, 0.4);
            border-color: rgba(255, 255, 255, 0.6);
        }
    </style>
</head>
<body>
    <div class="clock-container">
        <div class="date" id="date"></div>
        <div class="clock" id="clock">00:00:00</div>
        <div class="day" id="day"></div>

        <div class="format-toggle">
            <button id="btn24" onclick="setFormat(24)" class="active">24小时制</button>
            <button id="btn12" onclick="setFormat(12)">12小时制</button>
        </div>
    </div>

    <script>
        const clockEl = document.getElementById('clock');
        const dateEl = document.getElementById('date');
        const dayEl = document.getElementById('day');
        const btn24 = document.getElementById('btn24');
        const btn12 = document.getElementById('btn12');

        let is24Hour = true;

        function updateClock() {
            const now = new Date();

            // 获取日期信息
            const year = now.getFullYear();
            const month = String(now.getMonth() + 1).padStart(2, '0');
            const day = String(now.getDate()).padStart(2, '0');

            // 获取时间信息
            let hours = now.getHours();
            const minutes = String(now.getMinutes()).padStart(2, '0');
            const seconds = String(now.getSeconds()).padStart(2, '0');

            // 获取星期
            const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
            const dayName = days[now.getDay()];

            // 12小时制处理
            let period = '';
            if (!is24Hour) {
                period = hours >= 12 ? ' PM' : ' AM';
                hours = hours % 12 || 12;
            }

            hours = String(hours).padStart(2, '0');

            // 更新显示
            dateEl.textContent = `${year}-${month}-${day}`;
            clockEl.textContent = `${hours}:${minutes}:${seconds}${period}`;
            dayEl.textContent = dayName;
        }

        function setFormat(format) {
            is24Hour = format === 24;
            btn24.classList.toggle('active', is24Hour);
            btn12.classList.toggle('active', !is24Hour);
            updateClock();
        }

        // 初始化
        updateClock();
        setInterval(updateClock, 1000);
    </script>
</body>
</html>

代码解释: 时间源new Date(),每秒由 setInterval(updateClock, 1000) 驱动刷新,属于「拉模型」时钟;若需与服务器对齐可改为定期 NTP/API 校准。padStart(2, '0') 保证两位数显示。12 小时制 通过 hours % 12 || 12 把 0 点转为 12,并拼接 AM/PMclassList.toggle('active', is24Hour) 使用双参数形式,避免先 remove 再 add 的竞态。性能getElementById 在定时器外缓存节点引用,避免每秒重复查询 DOM。清理 :在 SPA 中组件销毁时应 clearInterval ,防止内存泄漏与后台仍更新已卸载节点。无障碍 :可额外更新 aria-live="polite" 区域,供读屏软件朗读时间变化。

7.3 选项卡切换

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>选项卡切换</title>
    <style>
        * {
            box-sizing: border-box;
        }
        body {
            font-family: Arial, sans-serif;
            background: #f5f5f5;
            padding: 20px;
        }
        .tabs-container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        .tabs-header {
            display: flex;
            background: #f0f0f0;
            border-bottom: 1px solid #ddd;
        }
        .tab-btn {
            flex: 1;
            padding: 15px 20px;
            background: none;
            border: none;
            cursor: pointer;
            font-size: 16px;
            color: #666;
            transition: all 0.3s ease;
            border-bottom: 3px solid transparent;
        }
        .tab-btn:hover {
            background: rgba(33, 150, 243, 0.1);
            color: #2196F3;
        }
        .tab-btn.active {
            color: #2196F3;
            border-bottom-color: #2196F3;
            background: white;
        }
        .tabs-content {
            padding: 20px;
            min-height: 300px;
        }
        .tab-pane {
            display: none;
            animation: fadeIn 0.3s ease;
        }
        .tab-pane.active {
            display: block;
        }
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }
        .tab-pane h3 {
            margin-top: 0;
            color: #333;
        }
        .tab-pane p {
            color: #666;
            line-height: 1.8;
        }
        .feature-list {
            list-style: none;
            padding: 0;
        }
        .feature-list li {
            padding: 10px 0;
            border-bottom: 1px solid #eee;
        }
        .feature-list li:before {
            content: "✓ ";
            color: #4CAF50;
            font-weight: bold;
            margin-right: 10px;
        }
    </style>
</head>
<body>
    <div class="tabs-container">
        <div class="tabs-header">
            <button class="tab-btn active" data-tab="home">首页</button>
            <button class="tab-btn" data-tab="features">功能</button>
            <button class="tab-btn" data-tab="about">关于</button>
            <button class="tab-btn" data-tab="contact">联系</button>
        </div>

        <div class="tabs-content">
            <div class="tab-pane active" id="home">
                <h3>欢迎来到首页</h3>
                <p>这是一个使用纯 JavaScript 和 DOM 操作实现的选项卡组件示例。通过点击上方的选项卡按钮,可以切换不同的内容面板。</p>
                <p>本示例展示了如何使用 classList API 来管理元素的类名,实现选项卡的切换效果。</p>
            </div>

            <div class="tab-pane" id="features">
                <h3>功能特点</h3>
                <ul class="feature-list">
                    <li>纯 JavaScript 实现,无需任何框架</li>
                    <li>使用 classList API 进行类名管理</li>
                    <li>流畅的动画过渡效果</li>
                    <li>响应式设计,适配各种屏幕</li>
                    <li>简洁易懂的代码结构</li>
                    <li>可扩展性强,易于定制</li>
                </ul>
            </div>

            <div class="tab-pane" id="about">
                <h3>关于本项目</h3>
                <p>本项目是一个 DOM 操作教程的示例代码,展示了如何使用原生 JavaScript 实现常见的 UI 交互功能。</p>
                <p>通过学习这些示例,您将掌握:</p>
                <ul class="feature-list">
                    <li>元素的选择和获取</li>
                    <li>类名的动态管理</li>
                    <li>事件处理机制</li>
                    <li>DOM 结构的动态操作</li>
                </ul>
            </div>

            <div class="tab-pane" id="contact">
                <h3>联系方式</h3>
                <p>如果您有任何问题或建议,欢迎通过以下方式联系我们:</p>
                <ul class="feature-list">
                    <li>邮箱:example@demo.com</li>
                    <li>电话:400-123-4567</li>
                    <li>地址:某某市某某区某某街道</li>
                </ul>
            </div>
        </div>
    </div>

    <script>
        const tabBtns = document.querySelectorAll('.tab-btn');
        const tabPanes = document.querySelectorAll('.tab-pane');

        tabBtns.forEach(btn => {
            btn.addEventListener('click', function() {
                // 移除所有激活状态
                tabBtns.forEach(b => b.classList.remove('active'));
                tabPanes.forEach(p => p.classList.remove('active'));

                // 激活当前选中的选项卡
                this.classList.add('active');
                const tabId = this.dataset.tab;
                document.getElementById(tabId).classList.add('active');
            });
        });
    </script>
</body>
</html>

代码解释: 状态机 :每个按钮带 data-tab 与对应面板 id 一致,点击时先 querySelectorAll 清掉 所有 .tab-btn.tab-paneactive ,再给当前按钮与 getElementById(tabId) 面板加上 active ,保证任意时刻仅一组可见。CSS.tab-pane { display: none }.tab-pane.active { display: block } 控制显隐,比反复改 style.display 更易维护。键盘无障碍 :生产环境应加 role="tablist" / role="tab" / role="tabpanel"aria-selectedaria-controls左右键 切换焦点。扩展 :若面板内容重,可懒加载首次激活时再 fetch 填内容,减少首屏 DOM。

7.4 全屏分页滚动

结合 DOM 设置每屏高度BOM scrollBy / scrollTo,实现整屏翻页与回到顶部(落地页、产品介绍、H5 长页常见)。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>全屏分页滚动</title>
    <style>
        body { margin: 0; }
        .box {
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            overflow: hidden;
        }
        .box1 { background: #e3f2fd; }
        .box2 { background: #fff3e0; }
        .box3 { background: #e8f5e9; }
        .box4 { background: #fce4ec; }
        .box5 { background: #ede7f6; }
        .btns {
            position: fixed;
            right: 16px;
            bottom: 24px;
            display: flex;
            flex-direction: column;
            gap: 8px;
        }
        .btn {
            width: 100px;
            height: 36px;
            line-height: 36px;
            color: #fff;
            font-size: 13px;
            text-align: center;
            background: #333;
            border-radius: 4px;
            cursor: pointer;
            user-select: none;
        }
        .btn:hover { background: #555; }
    </style>
</head>
<body>
    <div class="box box1"><h2>第一屏</h2></div>
    <div class="box box2"><h2>第二屏</h2></div>
    <div class="box box3"><h2>第三屏</h2></div>
    <div class="box box4"><h2>第四屏</h2></div>
    <div class="box box5"><h2>第五屏</h2></div>

    <div class="btns">
        <div class="btn" id="prevBtn">上一屏</div>
        <div class="btn" id="nextBtn">下一屏</div>
        <div class="btn" id="topBtn">返回顶部</div>
    </div>

    <script>
        const boxes = document.querySelectorAll('.box');
        const vh = window.innerHeight + 'px';
        boxes.forEach(function (el) {
            el.style.height = vh;
        });

        document.getElementById('nextBtn').onclick = function () {
            scrollBy({ top: innerHeight, behavior: 'smooth' });
        };
        document.getElementById('prevBtn').onclick = function () {
            scrollBy({ top: -innerHeight, behavior: 'smooth' });
        };
        document.getElementById('topBtn').onclick = function () {
            scrollTo({ top: 0, behavior: 'smooth' });
        };
    </script>
</body>
</html>

代码解释: window.innerHeight 为视口 CSS 像素高度,赋给每屏 el.style.height 使每块占满一屏,形成「整屏滚动」体验。scrollBy({ top: ±innerHeight, behavior: 'smooth' }) 相对当前滚动位置移动一整屏;scrollTo({ top: 0, behavior: 'smooth' }) 回顶。behavior: 'smooth' 依赖浏览器原生平滑滚动,部分旧环境会降级为瞬时滚动。resize :旋转手机或调整窗口后 innerHeight 变化,应 addEventListener('resize', ...) (注意防抖)重新赋值高度,否则会出现断层。可访问性 :全屏滚动站建议提供 跳过链接键盘可操作 的导航,避免仅靠右下角按钮。

7.5 列表复选框:全选、全不选与反选

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>全选 / 全不选 / 反选</title>
    <style>
        ul { list-style: none; padding: 0; }
        li { margin: 8px 0; }
        .toolbar { margin-top: 16px; }
        button { margin-right: 8px; padding: 8px 14px; cursor: pointer; }
    </style>
</head>
<body>
    <ul id="checkboxList">
        <li><label><input type="checkbox"> 任务项 A</label></li>
        <li><label><input type="checkbox"> 任务项 B</label></li>
        <li><label><input type="checkbox"> 任务项 C</label></li>
        <li><label><input type="checkbox"> 任务项 D</label></li>
        <li><label><input type="checkbox"> 任务项 E</label></li>
        <li><label><input type="checkbox"> 任务项 F</label></li>
    </ul>
    <div class="toolbar">
        <button id="selectAllBtn">全选</button>
        <button id="selectNotAllBtn">取消全选</button>
        <button id="invertBtn">反选</button>
    </div>

    <script>
        const checkboxItems = document.querySelectorAll('#checkboxList input[type="checkbox"]');

        document.getElementById('selectAllBtn').onclick = function () {
            checkboxItems.forEach(function (item) { item.checked = true; });
        };
        document.getElementById('selectNotAllBtn').onclick = function () {
            checkboxItems.forEach(function (item) { item.checked = false; });
        };
        document.getElementById('invertBtn').onclick = function () {
            checkboxItems.forEach(function (item) { item.checked = !item.checked; });
        };
    </script>
</body>
</html>

代码解释: querySelectorAll('#checkboxList input[type="checkbox"]') 在容器内限定范围,避免误选页面其它复选框;返回 静态 NodeList ,本例只做布尔赋值无增删节点问题。全选 / 全不选 即对同一引用集合 forEachchecked = true/false反选 对每项 checked = !checked ,时间复杂度 O(n)。与 7.6 区别 :本例无主框联动;若需「头行全选」与「部分选中」态,要同步 indeterminate (见 7.6)。提交表单 :若在 <form> 内,未勾选 name 的 checkbox 不会出现在 FormData 中;若需显式传 false,可用隐藏域或服务端默认。

7.6 主复选框联动子项

顶部「全选」控制列表每一项,并支持全不选(电商购物车、邮件客户端、后台表格)。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>主复选框联动</title>
    <style>
        ul { list-style: none; padding: 0; margin: 12px 0; }
        li { margin: 6px 0; }
        .master { font-weight: bold; margin-bottom: 8px; }
    </style>
</head>
<body>
    <label class="master">
        <input type="checkbox" id="masterCheck"> 全选
    </label>
    <ul id="checkboxList">
        <li><label><input type="checkbox" class="item-check"> 选项 1</label></li>
        <li><label><input type="checkbox" class="item-check"> 选项 2</label></li>
        <li><label><input type="checkbox" class="item-check"> 选项 3</label></li>
        <li><label><input type="checkbox" class="item-check"> 选项 4</label></li>
        <li><label><input type="checkbox" class="item-check"> 选项 5</label></li>
        <li><label><input type="checkbox" class="item-check"> 选项 6</label></li>
    </ul>

    <script>
        const master = document.getElementById('masterCheck');
        const items = document.querySelectorAll('.item-check');

        master.addEventListener('change', function () {
            items.forEach(function (cb) {
                cb.checked = master.checked;
            });
        });

        items.forEach(function (cb) {
            cb.addEventListener('change', function () {
                const allChecked = Array.from(items).every(function (c) { return c.checked; });
                const noneChecked = Array.from(items).every(function (c) { return !c.checked; });
                master.checked = allChecked;
                master.indeterminate = !allChecked && !noneChecked;
            });
        });
    </script>
</body>
</html>

代码解释: 主 → 子masterchange 事件里把 master.checked 广播到所有 item-check ,一键全选/全不选。子 → 主 :每个子框变化时用 Array.from(items).every 判断是否全选全未选 ;若介于两者之间,设置 master.indeterminate = true 并通常 master.checked = false ,以符合常见 UI(横杠表示部分选中)。indeterminate 仅为 IDL 属性,不能 通过 HTML 属性静态声明,只能脚本设置。无障碍 :主框应 aria-controls 指向列表 id,子框带 aria-labelledby 或统一 name,便于读屏理解分组关系。


八、最佳实践与性能优化

8.1 DOM 操作性能优化

8.1.1 减少 DOM 访问次数
javascript 复制代码
// ❌ 不好的做法 - 多次访问 DOM
for (let i = 0; i < 100; i++) {
    document.getElementById('list').innerHTML += '<li>Item ' + i + '</li>';
}

// ✅ 好的做法 - 缓存 DOM 引用
const list = document.getElementById('list');
let html = '';
for (let i = 0; i < 100; i++) {
    html += '<li>Item ' + i + '</li>';
}
list.innerHTML = html;

代码解释: 反例中 innerHTML += 在循环里每次都会 序列化整棵子树 + 重新解析 ,触发大量 布局与解析 ,且丢失已绑定在该列表上的 事件监听器 。正例先 getElementById 缓存 list,再在内存里拼好字符串 一次写入 innerHTML,将「读---改---写」从 O(n²) 降到 O(n)。更优还可 Document.createDocumentFragmentinsertAdjacentHTML 。若列表项需独立事件,应用 createElement + appendChild 或事件委托,避免整表 innerHTML 重绘。

8.1.2 使用文档片段批量添加元素
javascript 复制代码
// ✅ 使用 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.getElementById('list').appendChild(fragment);

代码解释: DocumentFragment 是存在于内存中的「轻量父节点」,把多个子节点先挂到 fragment 上时不会 触发页面多次 reflow ;最终 appendChild(fragment) 时,子节点会一次性接到真实 DOM 上,只触发一次 合并后的布局更新(具体次数仍受浏览器优化影响,但远优于循环 appendChildbody)。适合批量插入列表行、表格单元、虚拟滚动补窗等。注意fragment 被插入后其下子节点会被移出 fragment,fragment 本身不会出现在文档树中。

8.1.3 合理使用选择器
javascript 复制代码
// ✅ 快速选择器
document.getElementById('id');           // 最快
document.getElementsByClassName('class'); // 较快

// ⚠️ 较慢的选择器(复杂 CSS)
document.querySelectorAll('.container div.item:first-child');

代码解释: getElementById 由 id 哈希直接定位,通常最快。getElementsByClassName 走类索引,也较省。querySelectorAll 需 CSS 引擎解析选择器,:first-child、组合后代、通配 等会提高成本;热点路径(滚动、输入防抖回调)应避免在每一帧里跑重选择器,可 缓存 NodeList 或给目标打 稳定 class / data-hook 。若只需第一个匹配,用 querySelector 可在引擎找到首个后即停(仍取决于选择器复杂度)。

8.2 内存管理

8.2.1 及时清理引用
javascript 复制代码
// 创建元素
let element = document.createElement('div');
document.body.appendChild(element);

// 使用完毕后移除
document.body.removeChild(element);
// 清空引用(需使用 let/var 才可重新赋值)
element = null;

代码解释: 从文档 removeChild (或 remove() )后,节点不再显示,但若仍有 闭包、全局数组、Map 引用该节点,则无法被 GC ,并可能继续持有大型子树。element = null 仅在你用 let / var 声明且可重新赋值时才有意义;若示例写成 const element 则不能再赋 null,应改用 element.remove() 后让块级作用域结束或显式从集合里删除引用。现代写法 推荐 element.remove() ,等价于 parent.removeChild(element) 且更短。Detach 再复用 :有时先把子树 appendChildfragment 暂存,比反复 innerHTML 更可控。

8.2.2 移除事件监听器
javascript 复制代码
function handleClick() {
    console.log('Clicked');
}

button.addEventListener('click', handleClick);

// 移除事件监听器
button.removeEventListener('click', handleClick);

代码解释: removeEventListener 必须与 addEventListener函数引用、类型、capture 选项 完全一致,匿名函数无法移除。用途 :单页应用切路由、销毁组件时注销监听,避免 幽灵回调 与内存泄漏。{ once: true } 适合一次性任务。被动事件touchstart/wheel 若需 preventDefault,需 { passive: false } 且谨慎使用以免影响滚动性能。

8.3 现代 DOM API 推荐用法

javascript 复制代码
// ✅ 使用 querySelector/querySelectorAll
const element = document.querySelector('.class');
const elements = document.querySelectorAll('.class');

// ✅ 使用 classList
element.classList.add('active');
element.classList.remove('active');
element.classList.toggle('active');

// ✅ 使用 dataset
element.dataset.userId = '123';
const userId = element.dataset.userId;

// ✅ 使用 closest 查找最近的祖先元素
const parent = element.closest('.parent-class');

代码解释: 这一组是日常开发「查询---打标---读配置---找上下文 」的推荐组合:querySelector(All) 写清选择器;classList 管状态类;dataset 存与渲染相关的轻量元数据(复杂数据仍建议 JSON 或状态库);closest 从事件目标向上找最近的匹配选择器祖先,常用于 table 行内按钮找 tr卡片内按钮找 .card ,比手写 while (p && !p.matches(...)) 更短更稳。类型注意dataset 的值始终是 字符串 ,比较数字前要 Number()parseInt


九、DOM 事件机制详解

事件是 DOM 交互的核心,理解事件机制对于开发复杂的交互应用至关重要。

9.1 事件流

事件在 DOM 中的传播分为三个阶段:
1.捕获阶段
捕获
捕获
捕获
捕获
2.目标阶段
3.冒泡阶段
冒泡
冒泡
冒泡
冒泡
Window
Document
html
body
div
button 目标

名词解析:

阶段 说明 触发顺序
捕获阶段 事件从 Window 向下传播到目标元素 1️⃣
目标阶段 事件到达目标元素 2️⃣
冒泡阶段 事件从目标元素向上传播回 Window 3️⃣

9.2 事件捕获与冒泡

javascript 复制代码
// 捕获阶段监听(第三个参数为 true)
element.addEventListener('click', handler, true);

// 冒泡阶段监听(第三个参数为 false 或省略)
element.addEventListener('click', handler, false);
element.addEventListener('click', handler);

完整示例:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>事件流演示</title>
    <style>
        .box {
            padding: 40px;
            margin: 20px auto;
            text-align: center;
            border: 2px solid #333;
            background: #f0f0f0;
        }
        .outer {
            width: 400px;
            background: #ffebee;
        }
        .middle {
            width: 300px;
            background: #fff3e0;
        }
        .inner {
            width: 200px;
            background: #e8f5e9;
        }
        #log {
            max-width: 600px;
            margin: 20px auto;
            padding: 15px;
            background: #263238;
            color: #aed581;
            border-radius: 4px;
            font-family: monospace;
            max-height: 300px;
            overflow-y: auto;
        }
        button {
            padding: 10px 20px;
            margin: 5px;
            cursor: pointer;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 4px;
        }
    </style>
</head>
<body>
    <div style="text-align: center; margin: 20px;">
        <button onclick="clearLog()">清空日志</button>
        <button onclick="resetDemo()">重置演示</button>
    </div>

    <div class="box outer" id="outer">
        外层盒子
        <div class="box middle" id="middle">
            中间盒子
            <div class="box inner" id="inner">
                内层盒子
                <button id="triggerBtn">点击触发事件</button>
            </div>
        </div>
    </div>

    <div id="log"></div>

    <script>
        const log = document.getElementById('log');
        let eventCount = 0;

        function addLog(message, phase) {
            eventCount++;
            const indent = '  '.repeat(eventCount);
            const phaseIcon = {
                'capture': '⬇️',
                'target': '🎯',
                'bubble': '⬆️'
            }[phase] || '•';
            log.innerHTML = `${indent}[${phaseIcon}] ${message}\n` + log.innerHTML;
        }

        function clearLog() {
            log.innerHTML = '';
            eventCount = 0;
        }

        function resetDemo() {
            clearLog();
            addLog('=== 事件流演示已重置 ===', 'target');
        }

        // 捕获阶段监听
        document.getElementById('outer').addEventListener('click', (e) => {
            addLog('外层盒子 - 捕获阶段', 'capture');
        }, true);

        document.getElementById('middle').addEventListener('click', (e) => {
            addLog('中间盒子 - 捕获阶段', 'capture');
        }, true);

        document.getElementById('inner').addEventListener('click', (e) => {
            addLog('内层盒子 - 捕获阶段', 'capture');
        }, true);

        // 冒泡阶段监听
        document.getElementById('outer').addEventListener('click', (e) => {
            addLog('外层盒子 - 冒泡阶段', 'bubble');
        }, false);

        document.getElementById('middle').addEventListener('click', (e) => {
            addLog('中间盒子 - 冒泡阶段', 'bubble');
        }, false);

        document.getElementById('inner').addEventListener('click', (e) => {
            addLog('内层盒子 - 冒泡阶段', 'bubble');
        }, false);

        // 目标元素
        document.getElementById('triggerBtn').addEventListener('click', (e) => {
            addLog('🎯 目标元素(按钮)被点击', 'target');
            eventCount = 0;
        });

        // 初始化
        addLog('=== 点击按钮查看事件流 ===', 'target');
    </script>
</body>
</html>

代码解释: 捕获阶段从 Window → Document → html → body → outer → middle → inner → button ;目标阶段在 button 上触发;冒泡阶段按相反顺序回传。注意:在目标元素上注册的监听器,无论 useCapturetrue 还是 false,都会在目标阶段 按注册顺序触发,不会分两次。经典应用event.stopPropagation() 可中断后续传播;event.stopImmediatePropagation() 进一步阻止同一元素上其他监听器。

9.3 事件委托

事件委托是利用事件冒泡机制,在父元素上统一管理子元素事件的高效模式。

名词解析:

  • 事件委托(Event Delegation):将事件监听器设置在父元素上,利用冒泡处理子元素事件
  • 性能优势:减少监听器数量,动态添加的子元素自动继承事件处理
javascript 复制代码
// ❌ 不好的做法 - 为每个子元素单独绑定
const items = document.querySelectorAll('.item');
items.forEach(item => {
    item.addEventListener('click', handleClick);
});

// ✅ 好的做法 - 使用事件委托
document.querySelector('.container').addEventListener('click', (e) => {
    if (e.target.matches('.item')) {
        handleClick(e);
    }
});

完整示例:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>事件委托演示</title>
    <style>
        * {
            box-sizing: border-box;
        }
        body {
            font-family: Arial, sans-serif;
            background: #f5f5f5;
            padding: 20px;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        .toolbar {
            padding: 15px 20px;
            background: #2196F3;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        .toolbar button {
            padding: 8px 15px;
            background: white;
            color: #2196F3;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
        }
        .toolbar button:hover {
            background: #f0f0f0;
        }
        .list {
            padding: 20px;
        }
        .list-item {
            display: flex;
            align-items: center;
            padding: 15px;
            margin-bottom: 10px;
            background: #f9f9f9;
            border-radius: 4px;
            cursor: pointer;
            transition: all 0.2s;
        }
        .list-item:hover {
            background: #e3f2fd;
            transform: translateX(5px);
        }
        .list-item.selected {
            background: #e8f5e9;
            border-left: 4px solid #4CAF50;
        }
        .list-item.completed {
            opacity: 0.6;
            text-decoration: line-through;
        }
        .item-checkbox {
            width: 20px;
            height: 20px;
            margin-right: 15px;
            cursor: pointer;
        }
        .item-text {
            flex: 1;
        }
        .item-delete {
            padding: 5px 10px;
            background: #f44336;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            opacity: 0;
            transition: opacity 0.2s;
        }
        .list-item:hover .item-delete {
            opacity: 1;
        }
        .stats {
            padding: 15px 20px;
            background: #f9f9f9;
            border-top: 1px solid #eee;
            display: flex;
            justify-content: space-between;
            color: #666;
        }
        #log {
            max-width: 800px;
            margin: 20px auto;
            padding: 15px;
            background: #263238;
            color: #aed581;
            border-radius: 4px;
            font-family: monospace;
            font-size: 12px;
            max-height: 200px;
            overflow-y: auto;
        }
        .new-item-input {
            flex: 1;
            padding: 8px 12px;
            border: 1px solid #ddd;
            border-radius: 4px;
            margin-right: 10px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="toolbar">
            <input type="text" id="newItemInput" class="new-item-input" placeholder="输入新任务...">
            <button onclick="addNewItem()">添加任务</button>
            <button onclick="selectFirst()">选择第一个</button>
            <button onclick="selectAll()">全选</button>
            <button onclick="deselectAll()">取消全选</button>
            <button onclick="deleteSelected()">删除选中</button>
            <button onclick="clearCompleted()">清除已完成</button>
        </div>

        <div class="list" id="taskList">
            <div class="list-item" data-id="1">
                <input type="checkbox" class="item-checkbox">
                <span class="item-text">学习 JavaScript DOM 基础</span>
                <button class="item-delete">删除</button>
            </div>
            <div class="list-item" data-id="2">
                <input type="checkbox" class="item-checkbox">
                <span class="item-text">掌握事件委托机制</span>
                <button class="item-delete">删除</button>
            </div>
            <div class="list-item" data-id="3">
                <input type="checkbox" class="item-checkbox">
                <span class="item-text">理解浏览器渲染原理</span>
                <button class="item-delete">删除</button>
            </div>
            <div class="list-item" data-id="4">
                <input type="checkbox" class="item-checkbox">
                <span class="item-text">实践性能优化技巧</span>
                <button class="item-delete">删除</button>
            </div>
            <div class="list-item" data-id="5">
                <input type="checkbox" class="item-checkbox">
                <span class="item-text">构建完整的知识体系</span>
                <button class="item-delete">删除</button>
            </div>
        </div>

        <div class="stats">
            <span id="totalCount">总计: 5</span>
            <span id="selectedCount">已选: 0</span>
            <span id="completedCount">已完成: 0</span>
        </div>
    </div>

    <div id="log"></div>

    <script>
        const taskList = document.getElementById('taskList');
        const log = document.getElementById('log');
        let nextId = 6;

        function addLog(message) {
            const time = new Date().toLocaleTimeString();
            log.innerHTML = `[${time}] ${message}\n` + log.innerHTML;
        }

        // ========== 事件委托核心代码 ==========
        // 整个列表只使用一个事件监听器
        taskList.addEventListener('click', (e) => {
            const item = e.target.closest('.list-item');
            if (!item) return;

            // 处理复选框点击
            if (e.target.matches('.item-checkbox')) {
                item.classList.toggle('completed', e.target.checked);
                addLog(`复选框变化: ${item.querySelector('.item-text').textContent} - ${e.target.checked ? '已完成' : '未完成'}`);
                updateStats();
                return;
            }

            // 处理删除按钮点击
            if (e.target.matches('.item-delete')) {
                const text = item.querySelector('.item-text').textContent;
                item.remove();
                addLog(`删除任务: ${text}`);
                updateStats();
                return;
            }

            // 处理列表项点击(选中/取消选中)
            if (e.target.closest('.list-item')) {
                // 如果点击的不是复选框和删除按钮,则切换选中状态
                if (!e.target.matches('.item-checkbox') && !e.target.matches('.item-delete')) {
                    item.classList.toggle('selected');
                    addLog(`${item.classList.contains('selected') ? '选中' : '取消选中'}: ${item.querySelector('.item-text').textContent}`);
                    updateStats();
                }
            }
        });

        // 双击事件委托 - 编辑任务
        taskList.addEventListener('dblclick', (e) => {
            const item = e.target.closest('.list-item');
            if (!item) return;

            const textSpan = item.querySelector('.item-text');
            if (e.target === textSpan) {
                const newText = prompt('编辑任务:', textSpan.textContent);
                if (newText && newText.trim()) {
                    addLog(`编辑任务: "${textSpan.textContent}" → "${newText.trim()}"`);
                    textSpan.textContent = newText.trim();
                }
            }
        });
        // ======================================

        function addNewItem() {
            const input = document.getElementById('newItemInput');
            const text = input.value.trim();
            if (!text) {
                alert('请输入任务内容');
                return;
            }

            const newItem = document.createElement('div');
            newItem.className = 'list-item';
            newItem.dataset.id = nextId++;
            newItem.innerHTML = `
                <input type="checkbox" class="item-checkbox">
                <span class="item-text">${text}</span>
                <button class="item-delete">删除</button>
            `;

            taskList.appendChild(newItem);
            input.value = '';
            addLog(`添加新任务: ${text}`);
            updateStats();
        }

        function selectFirst() {
            const first = taskList.querySelector('.list-item');
            if (first) {
                first.classList.add('selected');
                addLog('选中第一个任务');
                updateStats();
            }
        }

        function selectAll() {
            taskList.querySelectorAll('.list-item').forEach(item => {
                item.classList.add('selected');
            });
            addLog('全选所有任务');
            updateStats();
        }

        function deselectAll() {
            taskList.querySelectorAll('.list-item').forEach(item => {
                item.classList.remove('selected');
            });
            addLog('取消全选');
            updateStats();
        }

        function deleteSelected() {
            const selected = taskList.querySelectorAll('.list-item.selected');
            if (selected.length === 0) {
                alert('请先选择要删除的任务');
                return;
            }

            if (confirm(`确定要删除选中的 ${selected.length} 个任务吗?`)) {
                selected.forEach(item => item.remove());
                addLog(`删除了 ${selected.length} 个选中的任务`);
                updateStats();
            }
        }

        function clearCompleted() {
            const completed = taskList.querySelectorAll('.list-item.completed');
            if (completed.length === 0) {
                alert('没有已完成的任务');
                return;
            }

            if (confirm(`确定要清除 ${completed.length} 个已完成的任务吗?`)) {
                completed.forEach(item => item.remove());
                addLog(`清除了 ${completed.length} 个已完成的任务`);
                updateStats();
            }
        }

        function updateStats() {
            const total = taskList.querySelectorAll('.list-item').length;
            const selected = taskList.querySelectorAll('.list-item.selected').length;
            const completed = taskList.querySelectorAll('.list-item.completed').length;

            document.getElementById('totalCount').textContent = `总计: ${total}`;
            document.getElementById('selectedCount').textContent = `已选: ${selected}`;
            document.getElementById('completedCount').textContent = `已完成: ${completed}`;
        }

        // 回车添加任务
        document.getElementById('newItemInput').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                addNewItem();
            }
        });

        // 初始化
        updateStats();
        addLog('事件委托演示已就绪 - 所有子元素事件通过父元素统一处理');
    </script>
</body>
</html>

代码解释: 核心思想 :无论列表有多少项(包括动态添加的),只在父容器 #taskList 上绑定一个 click 监听器 。事件冒泡到父元素后,用 e.target.closest('.list-item') 找到被点击的列表项,再用 e.target.matches() 判断具体点击的是复选框、删除按钮还是其他区域。优势 :① 动态添加的项自动享有事件处理,无需重新绑定;② 内存占用小,10000 项只需 1 个监听器而非 10000 个;③ 代码集中,便于维护。注意closest 从当前元素开始向上找,若点击本身就是 .list-item 则直接返回;matches 精确匹配选择器。双击编辑 :通过同一父元素监听 dblclick,实现复用事件委托。

9.4 事件对象详解

javascript 复制代码
element.addEventListener('click', (event) => {
    // 事件对象的核心属性
    console.log('事件类型:', event.type);           // 'click'
    console.log('目标元素:', event.target);          // 实际被点击的元素
    console.log('当前元素:', event.currentTarget);    // 绑定监听器的元素
    console.log('时间戳:', event.timeStamp);         // 事件创建时间

    // 坐标信息
    console.log('页面坐标:', event.pageX, event.pageY);
    console.log('客户区坐标:', event.clientX, event.clientY);
    console.log('屏幕坐标:', event.screenX, event.screenY);

    // 按键信息(鼠标事件)
    console.log('按键:', event.button);              // 0:左键 1:中键 2:右键
    console.log('Ctrl键:', event.ctrlKey);
    console.log('Shift键:', event.shiftKey);
    console.log('Alt键:', event.altKey);

    // 按键信息(键盘事件)
    console.log('按键码:', event.keyCode);           // 已废弃,用 key
    console.log('按键:', event.key);                 // 'a', 'Enter', 'Escape' 等
    console.log('代码:', event.code);                // 'KeyA', 'Enter' 等
});

代码解释: event.target 是事件源头 (可能随冒泡变化),event.currentTarget监听器绑定的元素this 等价)。坐标系pageX/Y 相对于文档左上角(含滚动),clientX/Y 相对于视口左上角,screenX/Y 相对于屏幕左上角。event.key 可读字符串,优于 keyCode 的数字码(已从标准移除)。修饰键ctrlKey/shiftKey/altKey/metaKey 常用于快捷键实现。

9.5 preventDefault 与 stopPropagation

javascript 复制代码
// preventDefault - 阻止默认行为
document.querySelector('a[href]').addEventListener('click', (e) => {
    e.preventDefault();  // 阻止链接跳转
    console.log('链接被点击,但不会跳转');
});

// 表单提交验证
document.querySelector('form').addEventListener('submit', (e) => {
    if (!isValid()) {
        e.preventDefault();  // 验证失败阻止提交
    }
});

// stopPropagation - 阻止事件传播
document.querySelector('.child').addEventListener('click', (e) => {
    e.stopPropagation();  // 阻止继续冒泡
    console.log('子元素点击,不会触发父元素');
});

// stopImmediatePropagation - 阻止同元素其他监听器
element.addEventListener('click', (e) => {
    e.stopImmediatePropagation();
    console.log('第一个监听器');
});
element.addEventListener('click', (e) => {
    console.log('这个监听器不会执行');
});

完整示例:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>事件控制演示</title>
    <style>
        .demo-container {
            max-width: 600px;
            margin: 20px auto;
            padding: 20px;
        }
        .box {
            padding: 40px;
            margin: 20px 0;
            text-align: center;
            border: 2px solid #333;
            background: #f0f0f0;
        }
        .parent {
            background: #ffebee;
        }
        .child {
            background: #fff3e0;
            margin: 20px;
        }
        .link-demo {
            padding: 20px;
            background: #e8f5e9;
        }
        .form-demo {
            padding: 20px;
            background: #e3f2fd;
        }
        input {
            padding: 8px;
            margin: 5px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            padding: 10px 20px;
            margin: 5px;
            cursor: pointer;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 4px;
        }
        a {
            color: #2196F3;
            text-decoration: none;
        }
        #log {
            max-width: 600px;
            margin: 20px auto;
            padding: 15px;
            background: #263238;
            color: #aed581;
            border-radius: 4px;
            font-family: monospace;
            min-height: 100px;
        }
    </style>
</head>
<body>
    <div class="demo-container">
        <h2>preventDefault 演示</h2>

        <div class="link-demo">
            <h3>链接默认行为</h3>
            <a href="https://example.com" class="normal-link">普通链接(会跳转)</a>
            <a href="https://example.com" class="prevent-link">阻止默认的链接(不会跳转)</a>
        </div>

        <div class="form-demo">
            <h3>表单验证</h3>
            <form id="demoForm">
                <input type="text" id="username" placeholder="用户名(至少3个字符)" required>
                <input type="email" id="email" placeholder="邮箱地址" required>
                <button type="submit">提交</button>
            </form>
        </div>

        <h2>stopPropagation 演示</h2>
        <div class="box parent" id="parentBox">
            父盒子
            <div class="box child" id="childBox">
                子盒子(点击时阻止冒泡)
            </div>
        </div>
    </div>

    <div id="log"></div>

    <script>
        const log = document.getElementById('log');

        function addLog(message) {
            const time = new Date().toLocaleTimeString();
            log.innerHTML = `[${time}] ${message}\n` + log.innerHTML;
        }

        // preventDefault 演示
        document.querySelector('.prevent-link').addEventListener('click', (e) => {
            e.preventDefault();
            addLog('🔗 链接默认跳转被阻止');
        });

        document.querySelector('.normal-link').addEventListener('click', () => {
            addLog('🔗 普通链接被点击(即将跳转)');
        });

        // 表单验证
        document.getElementById('demoForm').addEventListener('submit', (e) => {
            const username = document.getElementById('username').value;
            const email = document.getElementById('email').value;

            addLog(`📝 表单提交 - 用户名: ${username}, 邮箱: ${email}`);

            if (username.length < 3) {
                e.preventDefault();
                addLog('❌ 验证失败:用户名至少需要3个字符');
                alert('用户名至少需要3个字符');
                return;
            }

            if (!email.includes('@')) {
                e.preventDefault();
                addLog('❌ 验证失败:邮箱格式不正确');
                alert('请输入有效的邮箱地址');
                return;
            }

            addLog('✅ 验证通过,表单将被提交');
        });

        // stopPropagation 演示
        document.getElementById('parentBox').addEventListener('click', () => {
            addLog('🔵 父盒子被点击');
        });

        document.getElementById('childBox').addEventListener('click', (e) => {
            e.stopPropagation();
            addLog('🟠 子盒子被点击(冒泡已阻止)');
        });
    </script>
</body>
</html>

代码解释: preventDefault 只阻止浏览器默认行为(如链接跳转、表单提交、右键菜单、文本选择),不影响事件传播。stopPropagation 只阻止事件传播,不影响默认行为。组合使用 :若要同时阻止默认行为与传播,需同时调用 两者;常见场景:自定义下拉框点击外部关闭时,内部点击需 e.stopPropagation() 防止误触发关闭。注意 :被动事件监听器({ passive: true })中无法调用 preventDefault,否则浏览器会警告。

9.6 事件监听器选项

javascript 复制代码
// once - 只触发一次
button.addEventListener('click', handler, { once: true });

// passive - 承诺不调用 preventDefault(提升滚动性能)
document.addEventListener('touchstart', handler, { passive: true });

// capture - 捕获阶段监听
element.addEventListener('click', handler, { capture: true });

// signal - 使用 AbortController 取消监听
const controller = new AbortController();
element.addEventListener('click', handler, { signal: controller.signal });
controller.abort(); // 取消所有关联的监听器

代码解释: { once: true } 等价于监听器首次触发后自动 removeEventListener,适合初始化、一次性统计。{ passive: true } 告诉浏览器该监听器不会 调用 preventDefault,浏览器可并行执行滚动而不等待 JS 回调完成,显著提升 触摸滚动帧率。{ signal } 是现代取消机制,比 removeEventListener 更适合批量管理。兼容性once/passive/signal 在现代浏览器均支持,IE 需 polyfill。

9.7 常用 DOM 事件

事件类别 事件名 触发时机 典型应用
鼠标事件 click 点击 按钮交互
dblclick 双击 编辑文本
mousedown/mouseup 按下/抬起 拖拽开始/结束
mouseover/mouseout 悬入/悬出 提示框
mouseenter/mouseleave 进入/离开(不冒泡) 菜单高亮
mousemove 移动 绘图、跟随
键盘事件 keydown 按下 快捷键、游戏
keyup 抬起 组合键检测
keypress 按下(已废弃) ------
表单事件 submit 提交表单 验证
input 输入变化 实时搜索
change 值改变且失焦 下拉框、单选框
focus/blur 获得失去焦点 表单验证
文档事件 DOMContentLoaded DOM 解析完成 初始化脚本
load 资源加载完成 图片加载后操作
beforeunload 页面卸载前 防止误操作
scroll 滚动 懒加载、回到顶部
resize 窗口大小变化 响应式调整

9.8 本章归纳

捕获
冒泡
阻止默认
阻止传播
只执行一次
事件触发
捕获/冒泡?
Window → 目标
目标 → Window
事件委托
需要控制?
preventDefault
stopPropagation
once: true

技巧 应用场景
事件委托 列表、表格、动态内容
preventDefault 表单验证、自定义链接、拖放
stopPropagation 下拉框、模态框点击外部关闭
closest + matches 精准识别事件源
{ passive: true } 滚动性能优化

十、DOM 渲染与性能深度解析

10.1 浏览器渲染流程

理解浏览器如何将 DOM 渲染到屏幕上,对于性能优化至关重要。
HTML
DOM Tree
CSS
CSSOM Tree
Render Tree
Layout 回流
Paint 重绘
Composite 合成
显示到屏幕

名词解析:

阶段 说明 触发条件
DOM Tree HTML 解析为节点树 HTML 解析
CSSOM Tree CSS 解析为样式树 CSS 解析
Render Tree DOM + CSSOM 合并,只含可见节点 DOM/CSSOM 就绪
Layout / Reflow 计算每个节点的几何信息(位置、大小) 几何属性变化
Paint / Repaint 填充像素(颜色、阴影等) 样式属性变化
Composite 创建图层并合成 transform/opacity/滤镜

10.2 回流(Reflow)与重绘(Repaint)

回流(重排):元素的大小、位置发生变化,浏览器需要重新计算布局。

重绘:元素的外观样式发生变化,但布局不变。

javascript 复制代码
// 触发回流
element.style.width = '100px';           // 宽度变化
element.style.height = '100px';          // 高度变化
element.style.position = 'absolute';     // 定位变化
element.style.display = 'none';          // 显示/隐藏
element.appendChild(newChild);           // 添加/删除节点

// 触发重绘(不触发回流)
element.style.color = 'red';             // 颜色变化
element.style.background = '#fff';       // 背景变化
element.style.visibility = 'hidden';     // 可见性变化

// 只触发合成(最优)
element.style.transform = 'translateX(100px)';  // 变换
element.style.opacity = '0.5';                   // 透明度

代码解释: 回流一定 触发重绘,重绘不一定 触发回流。回流成本远高于重绘,因为需要重新计算整棵树。transform / opacity合成器线程处理,不触发主线程回流,是动画性能优化的关键。

完整示例:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>回流与重绘演示</title>
    <style>
        .demo-container {
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
        }
        .box {
            width: 100px;
            height: 100px;
            margin: 20px;
            background: #2196F3;
            display: inline-block;
            text-align: center;
            line-height: 100px;
            color: white;
            font-weight: bold;
            transition: all 0.3s ease;
        }
        .controls {
            margin: 20px 0;
        }
        button {
            padding: 10px 20px;
            margin: 5px;
            cursor: pointer;
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
        }
        .performance-stats {
            margin: 20px 0;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 4px;
        }
        #renderLog {
            max-width: 800px;
            margin: 20px auto;
            padding: 15px;
            background: #263238;
            color: #aed581;
            border-radius: 4px;
            font-family: monospace;
            font-size: 12px;
            max-height: 300px;
            overflow-y: auto;
        }
        .gpu-accelerated {
            will-change: transform, opacity;
            transform: translateZ(0);
        }
    </style>
</head>
<body>
    <div class="demo-container">
        <h2>回流与重绘性能对比</h2>

        <div class="controls">
            <button onclick="testReflow()">测试回流(改宽度)</button>
            <button onclick="testRepaint()">测试重绘(改颜色)</button>
            <button onclick="testComposite()">测试合成(transform)</button>
            <button onclick="batchChanges()">批量优化</button>
            <button onclick="toggleGPU()">切换 GPU 加速</button>
        </div>

        <div class="performance-stats">
            <div id="reflowBox1" class="box">回流</div>
            <div id="repaintBox" class="box">重绘</div>
            <div id="compositeBox" class="box">合成</div>
        </div>

        <div id="renderLog"></div>
    </div>

    <script>
        const renderLog = document.getElementById('renderLog');
        let reflowCount = 0;
        let repaintCount = 0;
        let compositeCount = 0;

        function addLog(message, type) {
            const time = new Date().toLocaleTimeString();
            const icon = {
                'reflow': '🔄',
                'repaint': '🎨',
                'composite': '⚡',
                'info': 'ℹ️'
            }[type] || '•';
            renderLog.innerHTML = `[${time}] ${icon} ${message}\n` + renderLog.innerHTML;
        }

        // 测试回流
        function testReflow() {
            const box = document.getElementById('reflowBox1');
            const start = performance.now();

            for (let i = 0; i < 100; i++) {
                box.style.width = (100 + i) + 'px';
            }

            const end = performance.now();
            reflowCount++;
            addLog(`回流测试完成: 100次宽度变化,耗时 ${(end - start).toFixed(2)}ms`, 'reflow');
        }

        // 测试重绘
        function testRepaint() {
            const box = document.getElementById('repaintBox');
            const start = performance.now();

            for (let i = 0; i < 100; i++) {
                box.style.background = `rgb(${i * 2}, ${100 + i}, ${200 - i})`;
            }

            const end = performance.now();
            repaintCount++;
            addLog(`重绘测试完成: 100次颜色变化,耗时 ${(end - start).toFixed(2)}ms`, 'repaint');
        }

        // 测试合成(GPU加速)
        function testComposite() {
            const box = document.getElementById('compositeBox');
            const start = performance.now();

            for (let i = 0; i < 100; i++) {
                box.style.transform = `translateX(${i}px)`;
            }

            const end = performance.now();
            compositeCount++;
            addLog(`合成测试完成: 100次transform变化,耗时 ${(end - start).toFixed(2)}ms`, 'composite');
        }

        // 批量优化示例
        function batchChanges() {
            const box = document.getElementById('reflowBox1');
            const start = performance.now();

            // ❌ 不好的做法 - 每次修改都触发回流
            // box.style.width = '150px';
            // box.style.height = '150px';
            // box.style.margin = '30px';

            // ✅ 好的做法 - 批量修改,只触发一次回流
            box.style.cssText = 'width: 150px; height: 150px; margin: 30px;';

            const end = performance.now();
            addLog(`批量优化: 使用 cssText 一次性修改多个属性,耗时 ${(end - start).toFixed(2)}ms`, 'info');
        }

        // GPU 加速
        function toggleGPU() {
            const boxes = document.querySelectorAll('.box');
            boxes.forEach(box => {
                box.classList.toggle('gpu-accelerated');
            });
            const isGPU = document.querySelector('.gpu-accelerated') !== null;
            addLog(`GPU 加速${isGPU ? '已启用' : '已禁用'} (will-change + translateZ(0))`, 'info');
        }
    </script>
</body>
</html>

代码解释: 回流测试 循环改 width,每次都触发重排 ,耗时最长。重绘测试 改颜色,只触发绘制 ,耗时居中。合成测试transform,由 GPU 处理,耗时最短。cssText 一次性写入多项样式,减少中间状态的重排次数。will-change: transform, opacity 提前告知浏览器优化,translateZ(0) 强制创建独立图层;两者组合常见于高性能动画。注意will-change 滥用会消耗内存,应仅对关键动画元素使用。

10.3 强制同步布局(Layout Thrashing)

Layout Thrashing 指在 JavaScript 中交替读写布局属性,导致浏览器被迫反复同步计算布局。

javascript 复制代码
// ❌ 强制同步布局 - 性能杀手
function bad() {
    for (let i = 0; i < 100; i++) {
        const height = elements[i].offsetHeight;  // 读布局,触发回流
        elements[i].style.height = height + 10 + 'px';  // 写布局,再次触发回流
    }
}

// ✅ 正确做法 - 批量读 + 批量写
function good() {
    const heights = [];
    // 先批量读取
    for (let i = 0; i < 100; i++) {
        heights.push(elements[i].offsetHeight);
    }
    // 再批量写入
    for (let i = 0; i < 100; i++) {
        elements[i].style.height = heights[i] + 10 + 'px';
    }
}

代码解释: 浏览器会延迟 布局计算,直到脚本"需要"结果(读布局属性)或帧结束。在循环里读 → 写 → 读 → 写 ,每次读都会触发强制同步布局。批量读 + 批量写 把计算压缩到一次。现代 APIrequestAnimationFrame 回调内读写,或用 ResizeObserver 监听尺寸变化。

完整示例:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>布局抖动演示</title>
    <style>
        .container {
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
        }
        .items {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }
        .item {
            width: 100px;
            height: 100px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-weight: bold;
            border-radius: 8px;
            transition: height 0.3s ease;
        }
        .controls {
            margin: 20px 0;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        button {
            padding: 10px 20px;
            cursor: pointer;
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
        }
        #results {
            margin: 20px 0;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 4px;
            font-family: monospace;
        }
        .result-item {
            padding: 10px;
            margin: 5px 0;
            background: white;
            border-radius: 4px;
            display: flex;
            justify-content: space-between;
        }
        .fast {
            border-left: 4px solid #4CAF50;
        }
        .slow {
            border-left: 4px solid #f44336;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>布局抖动性能对比</h2>

        <div class="controls">
            <button onclick="testBadApproach()">❌ 糟糕做法(交错读写)</button>
            <button onclick="testGoodApproach()">✅ 正确做法(批量读写)</button>
            <button onclick="testRAFApproach()">🚀 RAF 优化</button>
            <button onclick="resetItems()">重置</button>
        </div>

        <div class="items" id="itemsContainer"></div>

        <div id="results"></div>
    </div>

    <script>
        const container = document.getElementById('itemsContainer');
        const results = document.getElementById('results');
        const itemCount = 100;

        // 初始化项目
        function initItems() {
            container.innerHTML = '';
            for (let i = 0; i < itemCount; i++) {
                const item = document.createElement('div');
                item.className = 'item';
                item.textContent = i + 1;
                container.appendChild(item);
            }
        }

        function addResult(label, time, type) {
            const div = document.createElement('div');
            div.className = `result-item ${type}`;
            div.innerHTML = `
                <span>${label}</span>
                <span>${time.toFixed(2)}ms</span>
            `;
            results.appendChild(div);
        }

        function resetItems() {
            initItems();
            results.innerHTML = '';
        }

        // ❌ 糟糕做法 - 强制同步布局
        function testBadApproach() {
            const items = document.querySelectorAll('.item');
            const start = performance.now();

            for (let i = 0; i < items.length; i++) {
                // 读取布局属性,强制浏览器同步计算
                const height = items[i].offsetHeight;
                // 写入布局属性,使刚才的计算失效
                items[i].style.height = (height + 10) + 'px';
            }

            const end = performance.now();
            addResult('❌ 糟糕做法(交错读写)', end - start, 'slow');
        }

        // ✅ 正确做法 - 批量读写
        function testGoodApproach() {
            const items = document.querySelectorAll('.item');
            const start = performance.now();

            // 第一阶段:批量读取所有布局信息
            const heights = [];
            for (let i = 0; i < items.length; i++) {
                heights.push(items[i].offsetHeight);
            }

            // 第二阶段:批量写入所有更改
            for (let i = 0; i < items.length; i++) {
                items[i].style.height = (heights[i] + 10) + 'px';
            }

            const end = performance.now();
            addResult('✅ 正确做法(批量读写)', end - start, 'fast');
        }

        // 🚀 RAF 优化
        function testRAFApproach() {
            const items = document.querySelectorAll('.item');
            const start = performance.now();

            requestAnimationFrame(() => {
                // 在 RAF 回调中批量读取
                const heights = Array.from(items).map(item => item.offsetHeight);

                requestAnimationFrame(() => {
                    // 在下一个 RAF 中批量写入
                    items.forEach((item, i) => {
                        item.style.height = (heights[i] + 10) + 'px';
                    });

                    const end = performance.now();
                    addResult('🚀 RAF 优化', end - start, 'fast');
                });
            });
        }

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

代码解释: 糟糕做法 在循环里读 → 写 → 读 → 写 ,每次迭代触发一次强制同步布局,共 100 次回流正确做法收集所有读 ,再批量写入 ,只触发 1 次回流RAF 优化 利用浏览器的帧同步机制,在第一个 RAF 中读布局,第二个 RAF 中写更改,保证读写分离requestAnimationFrame 回调在浏览器重绘前执行,是动画与布局操作的理想时机。

10.4 减少回流与重绘的策略

策略 说明 示例
批量修改 DOM 合并多次修改 cssTextDocumentFragment
避免逐条修改样式 用类名替代行内样式 classList.add('active')
分离读写操作 先读后写,避免交错 批量读 → 批量写
使用 transform 动画优先用合成层属性 translateX 替代 left
使用 will-change 提示浏览器优化 will-change: transform
避免 table 布局 table 回流成本高 display: grid / flex
虚拟滚动 只渲染可见区域 大列表用虚拟滚动

10.5 渲染性能优化实战

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>渲染性能优化实战</title>
    <style>
        .demo-container {
            max-width: 1000px;
            margin: 20px auto;
            padding: 20px;
        }
        .scene {
            width: 100%;
            height: 400px;
            position: relative;
            background: #1a1a2e;
            overflow: hidden;
            border-radius: 8px;
        }
        .moving-element {
            width: 50px;
            height: 50px;
            background: #e94560;
            position: absolute;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-weight: bold;
        }
        .controls {
            margin: 20px 0;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        button {
            padding: 10px 20px;
            cursor: pointer;
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
        }
        .stats {
            display: flex;
            gap: 20px;
            margin: 20px 0;
        }
        .stat-box {
            flex: 1;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 4px;
            text-align: center;
        }
        .fps-meter {
            font-size: 24px;
            font-weight: bold;
            color: #4CAF50;
        }
    </style>
</head>
<body>
    <div class="demo-container">
        <h2>动画性能对比</h2>

        <div class="stats">
            <div class="stat-box">
                <div>当前 FPS</div>
                <div class="fps-meter" id="fpsDisplay">60</div>
            </div>
            <div class="stat-box">
                <div>动画方法</div>
                <div id="methodDisplay">-</div>
            </div>
            <div class="stat-box">
                <div>性能评级</div>
                <div id="ratingDisplay">-</div>
            </div>
        </div>

        <div class="controls">
            <button onclick="animateWithLeft()">❌ 使用 left(回流)</button>
            <button onclick="animateWithTransform()">✅ 使用 transform(合成)</button>
            <button onclick="animateWithWASM()">🚀 批量动画 (100个)</button>
            <button onclick="stopAnimation()">停止</button>
        </div>

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

    <script>
        const scene = document.getElementById('scene');
        const fpsDisplay = document.getElementById('fpsDisplay');
        const methodDisplay = document.getElementById('methodDisplay');
        const ratingDisplay = document.getElementById('ratingDisplay');

        let animationId = null;
        let elements = [];
        let lastTime = performance.now();
        let frameCount = 0;

        // 创建单个元素
        function createElement() {
            const el = document.createElement('div');
            el.className = 'moving-element';
            el.textContent = '🚀';
            el.style.left = '0px';
            el.style.top = '175px';
            scene.appendChild(el);
            return el;
        }

        // FPS 计算
        function updateFPS() {
            frameCount++;
            const currentTime = performance.now();
            if (currentTime - lastTime >= 1000) {
                const fps = Math.round(frameCount * 1000 / (currentTime - lastTime));
                fpsDisplay.textContent = fps;

                // 评级
                let rating = '⚡ 极佳';
                if (fps < 30) rating = '🐢 卡顿';
                else if (fps < 45) rating = '😐 一般';
                else if (fps < 55) rating = '👍 良好';
                ratingDisplay.textContent = rating;

                frameCount = 0;
                lastTime = currentTime;
            }
            animationId = requestAnimationFrame(updateFPS);
        }

        // ❌ 使用 left(触发回流)
        function animateWithLeft() {
            stopAnimation();
            scene.innerHTML = '';
            elements = [createElement()];
            methodDisplay.textContent = 'left 属性';

            let pos = 0;
            const startTime = performance.now();

            function animate() {
                pos += 5;
                if (pos > 900) pos = 0;

                elements[0].style.left = pos + 'px';  // 触发回流
                updateFPS();
            }

            animationId = requestAnimationFrame(animate);
        }

        // ✅ 使用 transform(只触发合成)
        function animateWithTransform() {
            stopAnimation();
            scene.innerHTML = '';
            elements = [createElement()];
            elements[0].style.willChange = 'transform';  // GPU 加速提示
            methodDisplay.textContent = 'transform 属性';

            let pos = 0;

            function animate() {
                pos += 5;
                if (pos > 900) pos = 0;

                elements[0].style.transform = `translateX(${pos}px)`;  // 只触发合成
                updateFPS();
            }

            animationId = requestAnimationFrame(animate);
        }

        // 🚀 批量动画
        function animateWithWASM() {
            stopAnimation();
            scene.innerHTML = '';
            elements = [];

            // 创建100个元素
            for (let i = 0; i < 100; i++) {
                const el = document.createElement('div');
                el.className = 'moving-element';
                el.style.width = '30px';
                el.style.height = '30px';
                el.style.left = '0px';
                el.style.top = (Math.random() * 350) + 'px';
                el.style.willChange = 'transform';
                el.style.opacity = '0.7';
                scene.appendChild(el);
                elements.push({ element: el, x: 0, speed: 1 + Math.random() * 3 });
            }

            methodDisplay.textContent = '批量 transform (100个)';

            function animate() {
                elements.forEach(item => {
                    item.x += item.speed;
                    if (item.x > 950) item.x = 0;
                    item.element.style.transform = `translateX(${item.x}px)`;
                });
                updateFPS();
            }

            animationId = requestAnimationFrame(animate);
        }

        function stopAnimation() {
            if (animationId) {
                cancelAnimationFrame(animationId);
                animationId = null;
            }
        }
    </script>
</body>
</html>

代码解释: left 动画 每次修改都会触发回流 ,FPS 下降明显。transform 动画合成层 上运行,由 GPU 处理,FPS 稳定在 60。will-change: transform 提前告知浏览器创建独立图层,避免合成时的重排。批量动画 证明即使 100 个元素同时运动,只要用 transform 仍能保持高 FPS。关键 :动画元素尽量减少影响布局的属性(width/height/left/top/margin),改用 transform/opacity/filters

10.6 本章归纳

几何属性
外观属性
transform/opacity
DOM 修改
修改类型?
回流 Layout
重绘 Paint
合成 Composite
最慢
中等
最快

优化策略 核心要点
减少回流 用 transform 替代位置属性,批量读写分离
减少重绘 使用类名切换样式,避免频繁修改外观
GPU 加速 will-change + transform/opacity
批量操作 DocumentFragmentcssText
RAF 时机 requestAnimationFrame 中操作 DOM

十一、现代 DOM API

11.1 IntersectionObserver - 交叉观察器

IntersectionObserver API 提供了一种异步检测元素是否进入视口的方法,性能远超传统的滚动事件监听。

名词解析:

  • 交叉比例(Intersection Ratio):目标元素可见部分与目标元素总面积的比例
  • 根元素(Root):用作视口的元素,默认为浏览器视口
  • 根边距(Root Margin):根元素的边距,用于扩大或缩小交叉区域
javascript 复制代码
// 基本用法
const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            console.log('元素进入视口');
            // 停止观察
            observer.unobserve(entry.target);
        }
    });
}, {
    threshold: 0.5,  // 目标元素50%可见时触发
    rootMargin: '100px'  // 扩大100px的触发区域
});

observer.observe(targetElement);

完整示例:图片懒加载

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>IntersectionObserver 懒加载</title>
    <style>
        * {
            box-sizing: border-box;
        }
        body {
            margin: 0;
            font-family: Arial, sans-serif;
            background: #f5f5f5;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
        }
        .info-bar {
            position: sticky;
            top: 0;
            background: rgba(33, 150, 243, 0.95);
            color: white;
            padding: 15px 20px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            z-index: 100;
            backdrop-filter: blur(10px);
            border-radius: 8px;
            margin-bottom: 20px;
        }
        .stats {
            display: flex;
            gap: 20px;
        }
        .stat-item {
            display: flex;
            align-items: center;
            gap: 5px;
        }
        .image-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
            gap: 20px;
        }
        .image-card {
            background: white;
            border-radius: 12px;
            overflow: hidden;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            transition: transform 0.3s ease;
        }
        .image-card:hover {
            transform: translateY(-5px);
        }
        .image-wrapper {
            position: relative;
            aspect-ratio: 16/9;
            background: #e0e0e0;
            overflow: hidden;
        }
        .image-wrapper img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            opacity: 0;
            transition: opacity 0.3s ease;
        }
        .image-wrapper img.loaded {
            opacity: 1;
        }
        .placeholder {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #999;
            font-size: 48px;
        }
        .loading-spinner {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 40px;
            height: 40px;
            border: 4px solid #f3f3f3;
            border-top: 4px solid #2196F3;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        @keyframes spin {
            0% { transform: translate(-50%, -50%) rotate(0deg); }
            100% { transform: translate(-50%, -50%) rotate(360deg); }
        }
        .image-info {
            padding: 15px;
        }
        .image-info h3 {
            margin: 0 0 10px 0;
            font-size: 16px;
        }
        .image-info p {
            margin: 0;
            color: #666;
            font-size: 14px;
        }
        .status-badge {
            position: absolute;
            top: 10px;
            right: 10px;
            padding: 5px 10px;
            border-radius: 20px;
            font-size: 12px;
            font-weight: bold;
        }
        .status-pending {
            background: #ff9800;
            color: white;
        }
        .status-loading {
            background: #2196F3;
            color: white;
        }
        .status-loaded {
            background: #4CAF50;
            color: white;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="info-bar">
            <h2>🖼️ 图片懒加载演示</h2>
            <div class="stats">
                <div class="stat-item">
                    <span>总计:</span>
                    <strong id="totalCount">0</strong>
                </div>
                <div class="stat-item">
                    <span>已加载:</span>
                    <strong id="loadedCount">0</strong>
                </div>
                <div class="stat-item">
                    <span>待加载:</span>
                    <strong id="pendingCount">0</strong>
                </div>
            </div>
        </div>

        <div class="image-grid" id="imageGrid"></div>
    </div>

    <script>
        const imageGrid = document.getElementById('imageGrid');
        const totalCountEl = document.getElementById('totalCount');
        const loadedCountEl = document.getElementById('loadedCount');
        const pendingCountEl = document.getElementById('pendingCount');

        let totalImages = 0;
        let loadedImages = 0;

        // 生成随机图片URL(使用占位服务)
        function getImageUrl(width, height) {
            return `https://picsum.photos/${width}/${height}?random=${Math.random()}`;
        }

        // 创建图片卡片
        function createImageCard(index) {
            const card = document.createElement('div');
            card.className = 'image-card';
            card.innerHTML = `
                <div class="image-wrapper">
                    <div class="placeholder">🖼️</div>
                    <div class="loading-spinner" style="display: none;"></div>
                    <div class="status-badge status-pending">待加载</div>
                    <img data-src="${getImageUrl(400, 300)}" alt="图片 ${index}">
                </div>
                <div class="image-info">
                    <h3>图片 ${index}</h3>
                    <p>滚动到此处将自动加载</p>
                </div>
            `;
            return card;
        }

        // 初始化图片网格
        function initImageGrid(count) {
            for (let i = 1; i <= count; i++) {
                imageGrid.appendChild(createImageCard(i));
            }
            totalImages = count;
            updateStats();
        }

        // 更新统计
        function updateStats() {
            totalCountEl.textContent = totalImages;
            loadedCountEl.textContent = loadedImages;
            pendingCountEl.textContent = totalImages - loadedImages;
        }

        // 创建 Intersection Observer
        const imageObserver = new IntersectionObserver((entries, observer) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const img = entry.target;
                    const wrapper = img.parentElement;
                    const spinner = wrapper.querySelector('.loading-spinner');
                    const placeholder = wrapper.querySelector('.placeholder');
                    const statusBadge = wrapper.querySelector('.status-badge');

                    // 更新状态
                    statusBadge.className = 'status-badge status-loading';
                    statusBadge.textContent = '加载中';
                    spinner.style.display = 'block';

                    // 加载图片
                    img.src = img.dataset.src;

                    img.onload = () => {
                        img.classList.add('loaded');
                        statusBadge.className = 'status-badge status-loaded';
                        statusBadge.textContent = '已加载';
                        spinner.style.display = 'none';
                        placeholder.style.display = 'none';
                        loadedImages++;
                        updateStats();
                    };

                    img.onerror = () => {
                        statusBadge.className = 'status-badge status-pending';
                        statusBadge.textContent = '加载失败';
                        spinner.style.display = 'none';
                    };

                    // 停止观察已加载的图片
                    observer.unobserve(img);
                }
            });
        }, {
            rootMargin: '100px',  // 提前100px开始加载
            threshold: 0.01       // 只要1%可见就触发
        });

        // 初始化并观察图片
        initImageGrid(30);

        // 观察所有图片元素
        setTimeout(() => {
            document.querySelectorAll('#imageGrid img').forEach(img => {
                imageObserver.observe(img);
            });
        }, 100);
    </script>
</body>
</html>

代码解释: IntersectionObserver 异步回调,不在主线程阻塞,比滚动事件+getBoundingClientRect性能好得多。rootMargin: '100px' 提前100px开始加载,让用户感知不到加载延迟。threshold: 0.01 只需1%可见就触发,适合懒加载。unobserve(img) 避免重复加载。占位符与加载中状态 提升用户体验。生产环境:图片 URL 可改用 CDN 或服务端按需生成。

11.2 MutationObserver - 变化观察器

MutationObserver API 用于监听 DOM 树的变化,替代已废弃的 MutationEvents

javascript 复制代码
// 监听 DOM 变化
const observer = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
        console.log('变化类型:', mutation.type);
        console.log('变化目标:', mutation.target);
    });
});

observer.observe(targetNode, {
    childList: true,       // 监听子节点变化
    attributes: true,      // 监听属性变化
    characterData: true,   // 监听文本变化
    subtree: true,         // 监听所有后代节点
    attributeFilter: ['class', 'src']  // 只监听特定属性
});

// 停止观察
observer.disconnect();

完整示例:DOM 变化监控器

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>MutationObserver 监控</title>
    <style>
        .container {
            max-width: 1000px;
            margin: 20px auto;
            padding: 20px;
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
        .panel {
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        .panel-header {
            background: #2196F3;
            color: white;
            padding: 15px;
            font-weight: bold;
        }
        .panel-content {
            padding: 20px;
            min-height: 400px;
        }
        .controls {
            margin-bottom: 20px;
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }
        button {
            padding: 8px 15px;
            cursor: pointer;
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
        }
        button:hover {
            background: #45a049;
        }
        #targetElement {
            padding: 20px;
            background: #f9f9f9;
            border: 2px dashed #999;
            border-radius: 4px;
            min-height: 100px;
        }
        #targetElement.changed {
            background: #fff3e0;
            border-color: #ff9800;
        }
        .log-entry {
            padding: 8px;
            margin: 5px 0;
            background: #f5f5f5;
            border-radius: 4px;
            font-size: 12px;
            font-family: monospace;
        }
        .log-entry .type {
            font-weight: bold;
            color: #2196F3;
        }
        .log-entry .time {
            color: #999;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="panel">
            <div class="panel-header">🎮 操作面板</div>
            <div class="panel-content">
                <div class="controls">
                    <button onclick="addElement()">➕ 添加元素</button>
                    <button onclick="removeElement()">➖ 删除元素</button>
                    <button onclick="changeText()">📝 修改文本</button>
                    <button onclick="changeClass()">🎨 修改类名</button>
                    <button onclick="changeAttribute()">⚙️ 修改属性</button>
                    <button onclick="clearLog()">🗑️ 清空日志</button>
                </div>

                <div id="targetElement">
                    <p>这是目标元素的初始内容</p>
                </div>
            </div>
        </div>

        <div class="panel">
            <div class="panel-header">📊 变化日志</div>
            <div class="panel-content">
                <div id="logContainer"></div>
            </div>
        </div>
    </div>

    <script>
        const targetElement = document.getElementById('targetElement');
        const logContainer = document.getElementById('logContainer');
        let changeCount = 0;

        // 创建 MutationObserver
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(mutation => {
                changeCount++;
                const logEntry = document.createElement('div');
                logEntry.className = 'log-entry';

                let details = '';

                switch(mutation.type) {
                    case 'childList':
                        if (mutation.addedNodes.length > 0) {
                            details = `添加了 ${mutation.addedNodes.length} 个节点`;
                        }
                        if (mutation.removedNodes.length > 0) {
                            details = `删除了 ${mutation.removedNodes.length} 个节点`;
                        }
                        break;
                    case 'attributes':
                        details = `属性 ${mutation.attributeName} 从 "${mutation.oldValue}" 变为 "${targetElement.getAttribute(mutation.attributeName)}"`;
                        break;
                    case 'characterData':
                        details = `文本内容变化`;
                        break;
                }

                logEntry.innerHTML = `
                    <span class="time">[${new Date().toLocaleTimeString()}]</span>
                    <span class="type">${mutation.type}</span>
                    <span>${details}</span>
                `;

                logContainer.insertBefore(logEntry, logContainer.firstChild);

                // 视觉反馈
                targetElement.classList.add('changed');
                setTimeout(() => targetElement.classList.remove('changed'), 300);
            });
        });

        // 配置观察选项
        const config = {
            childList: true,        // 观察子节点的添加或删除
            attributes: true,       // 观察属性变化
            characterData: true,    // 观察文本内容变化
            subtree: true,          // 观察所有后代节点
            attributeOldValue: true // 记录属性旧值
        };

        // 开始观察
        observer.observe(targetElement, config);

        // 操作函数
        function addElement() {
            const p = document.createElement('p');
            p.textContent = `新元素 ${Date.now()}`;
            targetElement.appendChild(p);
        }

        function removeElement() {
            const lastChild = targetElement.lastElementChild;
            if (lastChild && lastChild.tagName === 'P') {
                targetElement.removeChild(lastChild);
            }
        }

        function changeText() {
            const firstP = targetElement.querySelector('p');
            if (firstP) {
                firstP.textContent = `修改于 ${new Date().toLocaleTimeString()}`;
            }
        }

        function changeClass() {
            targetElement.classList.toggle('highlight');
        }

        function changeAttribute() {
            const currentId = targetElement.id;
            const newId = currentId === 'targetElement' ? 'modifiedElement' : 'targetElement';
            targetElement.id = newId;
        }

        function clearLog() {
            logContainer.innerHTML = '';
            changeCount = 0;
        }

        // 初始化日志
        addLog('MutationObserver 已启动,正在监听目标元素的所有变化...');
    </script>
</body>
</html>

代码解释: MutationObserver 回调接收 MutationRecord 数组,每个记录包含 typetargetaddedNodesremovedNodesattributeNameoldValue 等信息。配置项childListattributescharacterData 至少开启其一;subtree: true 递归监听后代;attributeOldValue: true 需与 attributes: true 配合。disconnect() 停止监听,takeRecords() 取出但不清空队列。典型应用:自动保存草稿、注入脚本检测 DOM 变化、调试工具。

11.3 ResizeObserver - 尺寸观察器

ResizeObserver API 用于监听元素尺寸变化,比 window.resize 更精确。

javascript 复制代码
const resizeObserver = new ResizeObserver(entries => {
    entries.forEach(entry => {
        const { width, height } = entry.contentRect;
        console.log(`元素尺寸: ${width}px × ${height}px`);
    });
});

resizeObserver.observe(element);

完整示例:响应式布局监控

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>ResizeObserver 监控</title>
    <style>
        .container {
            max-width: 1000px;
            margin: 20px auto;
            padding: 20px;
        }
        .resizable-box {
            width: 80%;
            min-height: 200px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 12px;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 24px;
            font-weight: bold;
            resize: both;
            overflow: hidden;
            transition: box-shadow 0.3s ease;
        }
        .resizable-box:hover {
            box-shadow: 0 8px 30px rgba(0,0,0,0.3);
        }
        .size-display {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: rgba(0,0,0,0.8);
            color: white;
            padding: 15px 20px;
            border-radius: 8px;
            font-family: monospace;
            backdrop-filter: blur(10px);
        }
        .size-history {
            max-width: 600px;
            margin: 20px auto;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 8px;
            max-height: 200px;
            overflow-y: auto;
        }
        .history-item {
            padding: 8px;
            margin: 5px 0;
            background: white;
            border-radius: 4px;
            font-family: monospace;
            font-size: 12px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>📏 ResizeObserver 演示</h2>
        <p>拖动右下角调整盒子大小,观察尺寸变化实时监控</p>

        <div class="resizable-box" id="resizableBox">
            拖动我调整大小
        </div>

        <div class="size-history">
            <h3>尺寸变化历史</h3>
            <div id="historyLog"></div>
        </div>
    </div>

    <div class="size-display" id="sizeDisplay">
        宽: 0px | 高: 0px
    </div>

    <script>
        const box = document.getElementById('resizableBox');
        const sizeDisplay = document.getElementById('sizeDisplay');
        const historyLog = document.getElementById('historyLog');

        // 创建 ResizeObserver
        const resizeObserver = new ResizeObserver(entries => {
            entries.forEach(entry => {
                const { width, height } = entry.contentRect;
                const displayWidth = Math.round(width);
                const displayHeight = Math.round(height);

                // 更新实时显示
                sizeDisplay.textContent = `宽: ${displayWidth}px | 高: ${displayHeight}px`;

                // 更新盒子内容
                box.textContent = `${displayWidth}px × ${displayHeight}px`;

                // 添加历史记录
                const historyItem = document.createElement('div');
                historyItem.className = 'history-item';
                historyItem.innerHTML = `
                    <span>${new Date().toLocaleTimeString()}</span>
                    <span>宽: ${displayWidth}px</span>
                    <span>高: ${displayHeight}px</span>
                `;
                historyLog.insertBefore(historyItem, historyLog.firstChild);

                // 限制历史记录数量
                while (historyLog.children.length > 10) {
                    historyLog.removeChild(historyLog.lastChild);
                }
            });
        });

        // 开始观察
        resizeObserver.observe(box);

        // 也观察窗口尺寸
        resizeObserver.observe(document.body);
    </script>
</body>
</html>

代码解释: ResizeObserver 监听盒模型 尺寸,包含 borderBoxSizecontentRectcontentBoxSize 等信息。contentRect 返回内容区(不含 padding/border),与 getBoundingClientRect 类似但不触发回流。响应式设计 :可根据元素宽度(而非视口宽度)切换布局,适合组件级响应式。注意 :回调可能在同帧内多次触发,可用 requestAnimationFrame 防抖。

11.4 现代选择器与遍历 API

javascript 复制代码
// matches - 检查元素是否匹配选择器
element.matches('.active');  // true/false

// closest - 查找最近的匹配祖先
element.closest('.container');

// contains - 检查是否包含后代元素
parent.contains(child);

// 便利方法
element.before(insertNode);    // 在元素前插入
element.after(insertNode);     // 在元素后插入
element.replaceWith(newNode);  // 替换元素
element.remove();              // 删除元素(现代写法)

// insertAdjacentHTML/Text/Element
element.insertAdjacentHTML('beforebegin', '<span>前</span>');
element.insertAdjacentHTML('afterbegin', '<span>首子</span>');
element.insertAdjacentHTML('beforeend', '<span>末子</span>');
element.insertAdjacentHTML('afterend', '<span>后</span>');

代码解释: matches 常用于事件委托判断目标。closest 从当前元素向上找,包括自身。contains 判断包含关系,替代 parentNode 链式查找。before/after/replaceWith/removeappendChild/removeChild 更直观。insertAdjacentHTML 按位置插入,不破坏现有引用,比 innerHTML += 更安全。

11.5 本章归纳

API 用途 优势
IntersectionObserver 懒加载、无限滚动、可见性检测 异步、高性能
MutationObserver DOM 变化监听、调试 替代 MutationEvents
ResizeObserver 响应式组件、尺寸监控 精确到元素级别
matches/closest 事件委托、向上查找 简化遍历逻辑

十二、高级实战案例

12.1 虚拟滚动(Virtual Scroll)

虚拟滚动是一种性能优化技术,只渲染可见区域的列表项,适合处理大量数据。

核心原理:
大数据集
计算可见范围
只渲染可见项
用户滚动
更新可见范围

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>虚拟滚动演示</title>
    <style>
        .container {
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
        }
        .controls {
            margin-bottom: 20px;
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        button {
            padding: 10px 20px;
            cursor: pointer;
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
        }
        .stats {
            display: flex;
            gap: 20px;
            margin-bottom: 20px;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 4px;
        }
        .virtual-scroll-container {
            height: 600px;
            border: 2px solid #ddd;
            border-radius: 8px;
            overflow-y: auto;
            position: relative;
        }
        .virtual-scroll-content {
            position: relative;
        }
        .virtual-item {
            padding: 15px;
            border-bottom: 1px solid #eee;
            display: flex;
            align-items: center;
            gap: 15px;
            background: white;
        }
        .virtual-item:nth-child(even) {
            background: #f9f9f9;
        }
        .item-index {
            width: 50px;
            height: 50px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: bold;
        }
        .item-content {
            flex: 1;
        }
        .item-title {
            font-weight: bold;
            margin-bottom: 5px;
        }
        .item-desc {
            color: #666;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>🚀 虚拟滚动演示</h2>

        <div class="controls">
            <button onclick="virtualList.setItemCount(1000)">1,000 项</button>
            <button onclick="virtualList.setItemCount(10000)">10,000 项</button>
            <button onclick="virtualList.setItemCount(100000)">100,000 项</button>
            <button onclick="virtualList.scrollToIndex(500)">跳转到第 500 项</button>
        </div>

        <div class="stats">
            <div>总数据: <strong id="totalCount">0</strong></div>
            <div>渲染节点: <strong id="renderedCount">0</strong></div>
            <div>可见范围: <strong id="visibleRange">-</strong></div>
        </div>

        <div class="virtual-scroll-container" id="scrollContainer">
            <div class="virtual-scroll-content" id="scrollContent"></div>
        </div>
    </div>

    <script>
        class VirtualList {
            constructor(options) {
                this.container = options.container;
                this.content = options.content;
                this.itemHeight = options.itemHeight || 80;
                this.renderBuffer = options.renderBuffer || 5;
                this.totalItems = 0;
                this.visibleItems = [];
                this.scrollTop = 0;

                this.init();
            }

            init() {
                this.container.addEventListener('scroll', () => this.onScroll());
            }

            setItemCount(count) {
                this.totalItems = count;
                this.content.style.height = (count * this.itemHeight) + 'px';
                this.render();
                this.updateStats();
            }

            onScroll() {
                this.scrollTop = this.container.scrollTop;
                requestAnimationFrame(() => this.render());
            }

            getVisibleRange() {
                const containerHeight = this.container.clientHeight;
                const startIndex = Math.floor(this.scrollTop / this.itemHeight);
                const endIndex = Math.ceil((this.scrollTop + containerHeight) / this.itemHeight);

                return {
                    start: Math.max(0, startIndex - this.renderBuffer),
                    end: Math.min(this.totalItems, endIndex + this.renderBuffer)
                };
            }

            render() {
                const { start, end } = this.getVisibleRange();
                const fragment = document.createDocumentFragment();

                for (let i = start; i < end; i++) {
                    const item = this.createItem(i);
                    fragment.appendChild(item);
                }

                this.content.innerHTML = '';
                this.content.appendChild(fragment);

                // 调整偏移
                const offset = start * this.itemHeight;
                this.content.style.paddingTop = offset + 'px';

                this.visibleItems = { start, end };
                this.updateStats();
            }

            createItem(index) {
                const div = document.createElement('div');
                div.className = 'virtual-item';
                div.style.position = 'absolute';
                div.style.top = (index * this.itemHeight) + 'px';
                div.style.width = '100%';
                div.style.boxSizing = 'border-box';
                div.innerHTML = `
                    <div class="item-index">${index + 1}</div>
                    <div class="item-content">
                        <div class="item-title">数据项 ${index + 1}</div>
                        <div class="item-desc">这是第 ${index + 1} 条数据的内容描述,虚拟滚动只渲染可见部分。</div>
                    </div>
                `;
                return div;
            }

            scrollToIndex(index) {
                const scrollTop = index * this.itemHeight;
                this.container.scrollTo({
                    top: scrollTop,
                    behavior: 'smooth'
                });
            }

            updateStats() {
                document.getElementById('totalCount').textContent = this.totalItems.toLocaleString();
                document.getElementById('renderedCount').textContent =
                    ((this.visibleItems.end - this.visibleItems.start) || 0).toLocaleString();
                document.getElementById('visibleRange').textContent =
                    `${this.visibleItems.start + 1} - ${this.visibleItems.end}`;
            }
        }

        // 初始化虚拟列表
        const virtualList = new VirtualList({
            container: document.getElementById('scrollContainer'),
            content: document.getElementById('scrollContent'),
            itemHeight: 80,
            renderBuffer: 5
        });

        // 默认显示 1000 项
        virtualList.setItemCount(1000);
    </script>
</body>
</html>

代码解释: 虚拟滚动 核心思想:容器高度固定 ,内容区设置总高度 撑开滚动条,但只渲染可见区域 + 缓冲区 的节点。getVisibleRange 根据 scrollTop 计算应该渲染的起止索引。paddingTop 把渲染项推到正确位置,避免"跳动"。缓冲区 上下多渲染几项,避免快速滚动时出现白屏。优势 :10 万条数据只需渲染约 20 个 DOM 节点 (视口高度/项高 + 2×缓冲区),大幅降低内存占用与渲染成本。注意:项高必须固定或可预测,动态高度需额外测量逻辑。

12.2 拖放 API(Drag and Drop)

HTML5 拖放 API 允许用户拖动元素并在指定位置放置。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>拖放 API 演示</title>
    <style>
        .container {
            max-width: 1000px;
            margin: 20px auto;
            padding: 20px;
        }
        .dnd-area {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
        .zone {
            min-height: 400px;
            padding: 20px;
            background: #f9f9f9;
            border: 3px dashed #ccc;
            border-radius: 8px;
        }
        .zone.drag-over {
            border-color: #2196F3;
            background: #e3f2fd;
        }
        .zone h3 {
            margin-top: 0;
            text-align: center;
            color: #666;
        }
        .draggable {
            padding: 15px;
            margin: 10px 0;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 8px;
            cursor: move;
            display: flex;
            align-items: center;
            gap: 10px;
            user-select: none;
        }
        .draggable:active {
            cursor: grabbing;
        }
        .draggable.dragging {
            opacity: 0.5;
        }
        .draggable .icon {
            font-size: 24px;
        }
        .controls {
            margin-bottom: 20px;
            display: flex;
            gap: 10px;
        }
        button {
            padding: 10px 20px;
            cursor: pointer;
            background: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
        }
        .log {
            margin-top: 20px;
            padding: 15px;
            background: #263238;
            color: #aed581;
            border-radius: 4px;
            font-family: monospace;
            max-height: 200px;
            overflow-y: auto;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>🎯 拖放 API 演示</h2>

        <div class="controls">
            <button onclick="addDraggable()">➕ 添加可拖拽项</button>
            <button onclick="resetAll()">🔄 重置</button>
        </div>

        <div class="dnd-area">
            <div class="zone" id="zone1">
                <h3>📦 源区域</h3>
                <div class="draggable" draggable="true" data-id="1">
                    <span class="icon">📁</span>
                    <span>项目文档</span>
                </div>
                <div class="draggable" draggable="true" data-id="2">
                    <span class="icon">🖼️</span>
                    <span>设计素材</span>
                </div>
                <div class="draggable" draggable="true" data-id="3">
                    <span class="icon">🎵</span>
                    <span>音频文件</span>
                </div>
            </div>

            <div class="zone" id="zone2">
                <h3>📥 目标区域</h3>
            </div>
        </div>

        <div class="log" id="dragLog"></div>
    </div>

    <script>
        const draggables = document.querySelectorAll('.draggable');
        const zones = document.querySelectorAll('.zone');
        const dragLog = document.getElementById('dragLog');
        let draggedElement = null;

        function addLog(message) {
            const time = new Date().toLocaleTimeString();
            dragLog.innerHTML = `[${time}] ${message}\n` + dragLog.innerHTML;
        }

        // 为所有可拖拽元素添加事件监听
        document.addEventListener('dragstart', (e) => {
            if (e.target.classList.contains('draggable')) {
                draggedElement = e.target;
                e.target.classList.add('dragging');

                // 设置拖拽数据
                e.dataTransfer.setData('text/plain', e.target.dataset.id);
                e.dataTransfer.effectAllowed = 'move';

                addLog(`🎯 开始拖拽: ${e.target.querySelector('.icon').nextElementSibling.textContent}`);
            }
        });

        document.addEventListener('dragend', (e) => {
            if (e.target.classList.contains('draggable')) {
                e.target.classList.remove('dragging');
                zones.forEach(zone => zone.classList.remove('drag-over'));
                draggedElement = null;
            }
        });

        // 为所有放置区域添加事件监听
        zones.forEach(zone => {
            zone.addEventListener('dragover', (e) => {
                e.preventDefault();  // 允许放置
                e.dataTransfer.dropEffect = 'move';
                zone.classList.add('drag-over');
            });

            zone.addEventListener('dragleave', (e) => {
                if (e.target === zone) {
                    zone.classList.remove('drag-over');
                }
            });

            zone.addEventListener('drop', (e) => {
                e.preventDefault();
                zone.classList.remove('drag-over');

                if (draggedElement) {
                    // 查找放置目标
                    const dropTarget = e.target.closest('.zone');
                    if (dropTarget) {
                        // 移动元素
                        dropTarget.appendChild(draggedElement);
                        const zoneName = dropTarget.id === 'zone1' ? '源区域' : '目标区域';
                        addLog(`📥 放置到: ${zoneName}`);
                    }
                }
            });
        });

        // 添加新的可拖拽项
        function addDraggable() {
            const icons = ['📄', '📊', '📈', '🎬', '🎮', '📱', '💾', '🔧'];
            const names = ['报告', '图表', '数据', '视频', '游戏', '应用', '备份', '工具'];
            const randomIndex = Math.floor(Math.random() * icons.length);

            const newDraggable = document.createElement('div');
            newDraggable.className = 'draggable';
            newDraggable.draggable = true;
            newDraggable.dataset.id = Date.now();
            newDraggable.innerHTML = `
                <span class="icon">${icons[randomIndex]}</span>
                <span>${names[randomIndex]} ${Math.floor(Math.random() * 100)}</span>
            `;

            document.getElementById('zone1').appendChild(newDraggable);
            addLog(`➕ 添加了新的可拖拽项`);
        }

        // 重置
        function resetAll() {
            document.getElementById('zone1').innerHTML = `
                <h3>📦 源区域</h3>
                <div class="draggable" draggable="true" data-id="1">
                    <span class="icon">📁</span>
                    <span>项目文档</span>
                </div>
                <div class="draggable" draggable="true" data-id="2">
                    <span class="icon">🖼️</span>
                    <span>设计素材</span>
                </div>
                <div class="draggable" draggable="true" data-id="3">
                    <span class="icon">🎵</span>
                    <span>音频文件</span>
                </div>
            `;
            document.getElementById('zone2').innerHTML = '<h3>📥 目标区域</h3>';
            addLog('🔄 已重置所有内容');
        }
    </script>
</body>
</html>

代码解释: draggable="true" 开启拖拽。dragstart 时设置 dataTransfer 数据与 effectAllowedmove/copy/link/none)。dragover 必须 e.preventDefault() 才能允许放置,同时设置 dropEffect 反馈视觉。drop 处理实际放置逻辑,常用 appendChild 移动节点。dragend 清理状态。事件委托 :在 document 上监听 dragstart/dragend,避免动态元素重新绑定。dataTransfer 可传递字符串、文件等,适合跨窗口/跨域(需 CORS)。

12.3 无限滚动加载

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>无限滚动演示</title>
    <style>
        .container {
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        .item-list {
            min-height: 100vh;
        }
        .list-item {
            padding: 20px;
            margin-bottom: 15px;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            display: flex;
            align-items: center;
            gap: 15px;
            animation: fadeIn 0.3s ease;
        }
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(20px); }
            to { opacity: 1; transform: translateY(0); }
        }
        .item-avatar {
            width: 60px;
            height: 60px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 24px;
        }
        .item-content {
            flex: 1;
        }
        .item-title {
            font-weight: bold;
            margin-bottom: 5px;
        }
        .item-desc {
            color: #666;
            font-size: 14px;
        }
        .loading {
            text-align: center;
            padding: 20px;
            color: #999;
        }
        .loading-spinner {
            display: inline-block;
            width: 30px;
            height: 30px;
            border: 3px solid #f3f3f3;
            border-top: 3px solid #2196F3;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        .stats {
            position: sticky;
            top: 0;
            background: rgba(33, 150, 243, 0.95);
            color: white;
            padding: 15px;
            margin: -20px -20px 20px -20px;
            display: flex;
            justify-content: space-between;
            border-radius: 8px 8px 0 0;
        }
        .end-message {
            text-align: center;
            padding: 40px;
            color: #999;
            font-size: 18px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="stats">
            <span>已加载: <strong id="loadedCount">0</strong> 项</span>
            <span id="statusText">滚动加载更多</span>
        </div>

        <div class="item-list" id="itemList"></div>

        <div class="loading" id="loadingIndicator" style="display: none;">
            <div class="loading-spinner"></div>
            <p>正在加载更多内容...</p>
        </div>
    </div>

    <script>
        const itemList = document.getElementById('itemList');
        const loadingIndicator = document.getElementById('loadingIndicator');
        const loadedCountEl = document.getElementById('loadedCount');
        const statusTextEl = document.getElementById('statusText');

        let currentPage = 0;
        let itemsPerPage = 20;
        let totalItems = 0;
        let isLoading = false;
        let hasMore = true;

        // 生成模拟数据
        function generateItems(page, count) {
            const items = [];
            const startIndex = page * count;

            for (let i = 0; i < count; i++) {
                const index = startIndex + i;
                items.push({
                    id: index,
                    title: `项目 ${index + 1}`,
                    desc: `这是第 ${index + 1} 项的详细描述内容,展示无限滚动加载效果。`,
                    avatar: ['👤', '👥', '👨', '👩', '🧑'][index % 5]
                });
            }

            return items;
        }

        // 创建列表项 DOM
        function createItemElement(item) {
            const div = document.createElement('div');
            div.className = 'list-item';
            div.innerHTML = `
                <div class="item-avatar">${item.avatar}</div>
                <div class="item-content">
                    <div class="item-title">${item.title}</div>
                    <div class="item-desc">${item.desc}</div>
                </div>
            `;
            return div;
        }

        // 加载更多数据
        async function loadMore() {
            if (isLoading || !hasMore) return;

            isLoading = true;
            loadingIndicator.style.display = 'block';
            statusTextEl.textContent = '正在加载...';

            // 模拟网络延迟
            await new Promise(resolve => setTimeout(resolve, 1000));

            const items = generateItems(currentPage, itemsPerPage);

            if (items.length === 0) {
                hasMore = false;
                loadingIndicator.innerHTML = '<div class="end-message">🎉 已加载全部内容</div>';
            } else {
                items.forEach(item => {
                    itemList.appendChild(createItemElement(item));
                });

                totalItems += items.length;
                loadedCountEl.textContent = totalItems;
                currentPage++;
                statusTextEl.textContent = '滚动加载更多';
            }

            isLoading = false;
        }

        // 使用 Intersection Observer 实现无限滚动
        const observer = new IntersectionObserver((entries) => {
            if (entries[0].isIntersecting) {
                loadMore();
            }
        }, {
            rootMargin: '100px',  // 提前100px开始加载
            threshold: 0.01
        });

        observer.observe(loadingIndicator);

        // 初始加载
        loadMore();
    </script>
</body>
</html>

代码解释: IntersectionObserver 监听加载指示器 是否进入视口,进入则触发 loadMorerootMargin: '100px' 提前加载,用户不会感到等待。isLoading 锁防止重复请求。模拟异步setTimeout 等待 1 秒,生产环境换成 fetch/axios到达末尾 时显示"已加载全部"。优点 :比监听 scroll 事件+scrollTop 计算简单高效,自带防抖。

12.4 本章归纳

案例 核心技术 适用场景
虚拟滚动 只渲染可见项 大数据列表
拖放 API DragEvent + dataTransfer 看板、文件上传
无限滚动 IntersectionObserver 社交媒体、电商列表

相关推荐
发现一只大呆瓜7 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
Maimai108088 小时前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly
candyTong8 小时前
Claude Code 的 Edit 工具是怎么工作的
javascript·后端·架构
cen__y8 小时前
Linux12(Git01)
linux·运维·服务器·c语言·开发语言·git
AI人工智能+电脑小能手8 小时前
【大白话说Java面试题 第65题】【JVM篇】第25题:谈谈对 OOM 的认识
java·开发语言·jvm
社交怪人9 小时前
【算平均分】信息学奥赛一本通C语言解法(题号2071)
c语言·开发语言
kyriewen9 小时前
产品经理把PRD写成“天书”,我用AI半小时重写了一遍,他当场愣住
前端·ai编程·cursor
郭涤生10 小时前
不同主机之间网络通信-以太网连接复习
开发语言·rk3588
山居秋暝LS10 小时前
【无标题】RTX00安装paddle OCR,win11不能装最新的,也不能用GPU
开发语言·r语言