Day15_JavaScript DOM 事件完全指南:从基础到实战(下)

七、过渡与动画事件

7.1 过渡事件 transition

事件名 说明 典型场景
transitionstart 过渡开始 记录动画起点
transitionrun 过渡进行 与 start 成对出现
transitionend 过渡结束 收起面板、切换步骤
transitioncancel 过渡取消 中途改样式时触发
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>
        .transition-demo {
            width: 600px;
            margin: 50px auto;
            padding: 30px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .transition-box {
            width: 200px;
            height: 200px;
            margin: 50px auto;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 18px;
            cursor: pointer;
            transition: all 2s ease-in-out 1s;
        }

        .transition-box.active {
            transform: rotate(360deg) scale(1.2);
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
            border-radius: 50%;
        }

        .event-log {
            margin-top: 30px;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 5px;
            max-height: 250px;
            overflow-y: auto;
        }

        .event-item {
            padding: 8px;
            margin: 5px 0;
            background: white;
            border-radius: 3px;
            font-family: monospace;
            font-size: 14px;
            border-left: 4px solid #667eea;
        }

        .event-item.start { border-left-color: #28a745; }
        .event-item.run { border-left-color: #ffc107; }
        .event-item.end { border-left-color: #dc3545; }

        .controls {
            text-align: center;
            margin-top: 20px;
        }

        .btn {
            padding: 10px 20px;
            margin: 0 5px;
            border: none;
            border-radius: 5px;
            background: #667eea;
            color: white;
            cursor: pointer;
            font-size: 14px;
            transition: background 0.3s;
        }

        .btn:hover {
            background: #764ba2;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">CSS 过渡事件演示</h1>

    <div class="transition-demo">
        <div class="transition-box" id="transitionBox">
            ???
        </div>

        <div class="controls">
            <button class="btn" id="toggleBtn">切换样式</button>
            <button class="btn" id="clearBtn">清空日志</button>
        </div>

        <div class="event-log" id="eventLog">
            <div class="event-item">等待操作...</div>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const transitionBox = document.getElementById('transitionBox');
            const eventLog = document.getElementById('eventLog');
            const toggleBtn = document.getElementById('toggleBtn');
            const clearBtn = document.getElementById('clearBtn');

            function logEvent(eventName, className) {
                const time = new Date().toLocaleTimeString();
                const item = document.createElement('div');
                item.className = `event-item ${className}`;
                item.textContent = `[${time}] ${eventName}`;
                eventLog.appendChild(item);
                eventLog.scrollTop = eventLog.scrollHeight;
            }

            // **【代码注释】**见下方说明块
            transitionBox.addEventListener('transitionstart', function(e) {
                console.log('transitionstart:', e);
                logEvent(`transitionstart - ??: ${e.propertyName}`, 'start');
            });

            // **【代码注释】**见下方说明块
            transitionBox.addEventListener('transitionrun', function(e) {
                console.log('transitionrun:', e);
                logEvent(`transitionrun - ??: ${e.propertyName}`, 'run');
            });

            // **【代码注释】**见下方说明块
            transitionBox.addEventListener('transitionend', function(e) {
                console.log('transitionend:', e);
                logEvent(`transitionend - 属性: ${e.propertyName}, 耗时: ${e.elapsedTime.toFixed(2)}ms`, 'end');
            });

            // **【代码注释】**见下方说明块
            transitionBox.addEventListener('click', function() {
                this.classList.toggle('active');
            });

            toggleBtn.addEventListener('click', function() {
                transitionBox.classList.toggle('active');
            });

            clearBtn.addEventListener('click', function() {
                eventLog.innerHTML = '<div class="event-item">等待操作...</div>';
            });
        })();
    </script>
</body>
</html>

【代码注释】

  1. mousedown 记录 offsetX/offsetY,表示鼠标在元素内的点击偏移,避免拖动时元素「跳动」。
  2. mousemove 绑定在 document 上,防止快速拖动时指针离开元素导致中断。
  3. 使用 clientX/clientY 配合偏移计算 left/top,并做视口边界钳制。
  4. 生产环境可改用 HTML5 Drag and Drop APIPointer Events 统一鼠标/触控。

市面应用:Trello 看板卡片拖拽、网盘文件拖入上传区、可视化搭建工具组件拖放。

7.2 动画事件 animation

事件名 说明 典型场景
animationstart 动画开始 入场动效
animationend 动画结束 移除 loading
animationiteration 每次循环 无限旋转指示器
animationcancel 动画取消 切换 class 时
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>
        .animation-demo {
            width: 600px;
            margin: 50px auto;
            padding: 30px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .animation-stage {
            height: 200px;
            background: linear-gradient(to bottom, #87CEEB 0%, #E0F7FA 100%);
            border-radius: 10px;
            position: relative;
            overflow: hidden;
            margin-bottom: 20px;
        }

        .animated-element {
            position: absolute;
            width: 60px;
            height: 60px;
            background: #ff6b6b;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24px;
            animation: bounce 2s ease-in-out infinite;
        }

        @keyframes bounce {
            0%, 100% {
                left: 20px;
                top: 50%;
                transform: translateY(-50%);
            }
            50% {
                left: calc(100% - 80px);
                top: 30%;
                transform: translateY(-50%) scale(1.2);
            }
        }

        .stats-panel {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 15px;
            margin-bottom: 20px;
        }

        .stat-card {
            padding: 15px;
            background: #f5f5f5;
            border-radius: 8px;
            text-align: center;
        }

        .stat-card .label {
            font-size: 12px;
            color: #666;
            margin-bottom: 5px;
        }

        .stat-card .value {
            font-size: 24px;
            font-weight: bold;
            color: #667eea;
        }

        .event-log {
            padding: 15px;
            background: #2d2d2d;
            color: #00ff00;
            border-radius: 5px;
            height: 200px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 12px;
        }

        .event-log .log-item {
            margin: 3px 0;
            padding: 3px 5px;
            border-left: 3px solid;
            padding-left: 8px;
        }

        .event-log .start { border-left-color: #28a745; }
        .event-log .iteration { border-left-color: #ffc107; }
        .event-log .end { border-left-color: #dc3545; }

        .controls {
            display: flex;
            gap: 10px;
            margin-top: 20px;
        }

        .btn {
            flex: 1;
            padding: 12px;
            border: none;
            border-radius: 5px;
            background: #667eea;
            color: white;
            cursor: pointer;
            font-size: 14px;
            transition: background 0.3s;
        }

        .btn:hover {
            background: #764ba2;
        }

        .btn.secondary {
            background: #6c757d;
        }

        .btn.secondary:hover {
            background: #5a6268;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">CSS 过渡事件演示</h1>

    <div class="animation-demo">
        <div class="animation-stage">
            <div class="animated-element" id="animElement">等待操作...</div>
        </div>

        <div class="stats-panel">
            <div class="stat-card">
                <div class="label">等待操作...</div>
                <div class="value" id="startCount">0</div>
            </div>
            <div class="stat-card">
                <div class="label">等待操作...</div>
                <div class="value" id="iterationCount">0</div>
            </div>
            <div class="stat-card">
                <div class="label">等待操作...</div>
                <div class="value" id="endCount">0</div>
            </div>
        </div>

        <div class="event-log" id="eventLog">
            <div class="log-item">等待操作...</div>
        </div>

        <div class="controls">
            <button class="btn" id="pauseBtn">暂停动画</button>
            <button class="btn" id="resumeBtn">继续动画</button>
            <button class="btn secondary" id="clearBtn">清空日志</button>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const animElement = document.getElementById('animElement');
            const eventLog = document.getElementById('eventLog');
            const startCount = document.getElementById('startCount');
            const iterationCount = document.getElementById('iterationCount');
            const endCount = document.getElementById('endCount');
            const pauseBtn = document.getElementById('pauseBtn');
            const resumeBtn = document.getElementById('resumeBtn');
            const clearBtn = document.getElementById('clearBtn');

            let counts = { start: 0, iteration: 0, end: 0 };

            function logEvent(message, className) {
                const time = new Date().toLocaleTimeString();
                const item = document.createElement('div');
                item.className = `log-item ${className}`;
                item.textContent = `[${time}] ${message}`;
                eventLog.appendChild(item);
                eventLog.scrollTop = eventLog.scrollHeight;
            }

            function updateCounts() {
                startCount.textContent = counts.start;
                iterationCount.textContent = counts.iteration;
                endCount.textContent = counts.end;
            }

            // **【代码注释】**见下方说明块
            animElement.addEventListener('animationstart', function(e) {
                counts.start++;
                updateCounts();
                console.log('animationstart:', e);
                logEvent(`animationstart - ??: ${e.animationName}`, 'start');
            });

            // **【代码注释】**见下方说明块
            animElement.addEventListener('animationiteration', function(e) {
                counts.iteration++;
                updateCounts();
                console.log('animationiteration:', e);
                logEvent(`animationiteration - ${e.animationName} 新一轮`, 'iteration');
            });

            // **【代码注释】**见下方说明块
            animElement.addEventListener('animationend', function(e) {
                counts.end++;
                updateCounts();
                console.log('animationend:', e);
                logEvent(`animationend - 动画: ${e.animationName}, 耗时: ${e.elapsedTime.toFixed(2)}ms`, 'end');
            });

            // **【代码注释】**见下方说明块
            pauseBtn.addEventListener('click', function() {
                animElement.style.animationPlayState = 'paused';
            });

            resumeBtn.addEventListener('click', function() {
                animElement.style.animationPlayState = 'running';
            });

            clearBtn.addEventListener('click', function() {
                eventLog.innerHTML = '<div class="log-item">等待操作...</div>';
                counts = { start: 0, iteration: 0, end: 0 };
                updateCounts();
            });
        })();
    </script>
</body>
</html>

【代码注释】

  1. mousedown 记录 offsetX/offsetY,表示鼠标在元素内的点击偏移,避免拖动时元素「跳动」。
  2. mousemove 绑定在 document 上,防止快速拖动时指针离开元素导致中断。
  3. 使用 clientX/clientY 配合偏移计算 left/top,并做视口边界钳制。
  4. 生产环境可改用 HTML5 Drag and Drop APIPointer Events 统一鼠标/触控。

市面应用:Trello 看板卡片拖拽、网盘文件拖入上传区、可视化搭建工具组件拖放。

【本章小结】

|------|----------|----------|

| CSS transition | transitionrun / transitionstart / transitionend | 属性过渡结束再改 DOM |

| CSS animation | animationstart / animationiteration / animationend | @keyframes 多阶段动效 |


八、其他重要事件

8.1 滚动事件 scroll

scroll 在元素滚动时触发(冒泡),常用于吸顶导航、阅读进度、回到顶部。监听容器用 element.addEventListener('scroll', ...)window 滚动监听 documentwindow

  • 说明 :高频触发,应用 requestAnimationFrame 或节流合并计算。
  • 说明scrollTop / scrollLeft 读位置;scrollHeight 算进度百分比。
  • 说明position: fixed 导航常配合 classList.toggle('scrolled')
  • 说明 :移动端注意 -webkit-overflow-scrolling: touch 与滚动穿透。
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>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

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

        .navbar {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            padding: 20px;
            background: transparent;
            transition: all 0.3s;
            z-index: 1000;
        }

        .navbar.scrolled {
            background: rgba(102, 126, 234, 0.95);
            padding: 15px 20px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        .navbar ul {
            list-style: none;
            display: flex;
            justify-content: center;
            gap: 30px;
        }

        .navbar a {
            color: white;
            text-decoration: none;
            font-weight: bold;
        }

        .hero {
            height: 100vh;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 48px;
        }

        .scroll-indicator {
            position: absolute;
            bottom: 30px;
            animation: bounce 2s infinite;
        }

        @keyframes bounce {
            0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
            40% { transform: translateY(-20px); }
            60% { transform: translateY(-10px); }
        }

        .content-section {
            padding: 100px 20px;
            max-width: 1000px;
            margin: 0 auto;
        }

        .content-section h2 {
            font-size: 32px;
            margin-bottom: 20px;
            color: #333;
        }

        .content-section p {
            font-size: 16px;
            line-height: 1.8;
            color: #666;
            margin-bottom: 15px;
        }

        .scroll-progress {
            position: fixed;
            top: 0;
            left: 0;
            height: 3px;
            background: linear-gradient(90deg, #00d2ff 0%, #3a7bd5 100%);
            width: 0%;
            z-index: 1001;
            transition: width 0.1s;
        }

        .scroll-info {
            position: fixed;
            bottom: 20px;
            right: 20px;
            padding: 15px;
            background: rgba(0,0,0,0.8);
            color: white;
            border-radius: 5px;
            font-family: monospace;
            z-index: 1000;
        }

        .back-to-top {
            position: fixed;
            bottom: 80px;
            right: 20px;
            width: 50px;
            height: 50px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 50%;
            cursor: pointer;
            opacity: 0;
            visibility: hidden;
            transition: all 0.3s;
            z-index: 1000;
            font-size: 24px;
        }

        .back-to-top.show {
            opacity: 1;
            visibility: visible;
        }

        .back-to-top:hover {
            background: #764ba2;
            transform: translateY(-5px);
        }

        .lazy-image {
            width: 100%;
            height: 300px;
            margin: 20px 0;
            background: #f0f0f0;
            border-radius: 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #999;
            font-size: 18px;
            transition: opacity 0.5s;
        }

        .lazy-image img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            border-radius: 10px;
            opacity: 0;
        }

        .lazy-image img.loaded {
            opacity: 1;
        }
    </style>
</head>
<body>
    <div class="scroll-progress" id="scrollProgress"></div>

    <nav class="navbar" id="navbar">
        <ul>
            <li><a href="#home">??</a></li>
            <li><a href="#about">??</a></li>
            <li><a href="#services">??</a></li>
            <li><a href="#contact">??</a></li>
        </ul>
    </nav>

    <div class="hero" id="home">
        <div>
            <h1 style="text-align: center;">示例页面</h1>
            <p>请按键盘或点击操作</p>
        </div>
        <div class="scroll-indicator">等待操作...</div>
    </div>

    <div class="content-section" id="about">
        <h2>事件演示</h2>
        <p>请按键盘或点击操作</p>
        <p>请按键盘或点击操作</p>
        <div class="lazy-image" data-src="images/db01.svg">
            <span>懒加载占位...</span>
        </div>
    </div>

    <div class="content-section" id="services">
        <h2>事件演示</h2>
        <p>请按键盘或点击操作</p>
        <p>请按键盘或点击操作</p>
        <div class="lazy-image" data-src="images/db02.svg">
            <span>懒加载占位...</span>
        </div>
    </div>

    <div class="content-section" id="contact">
        <h2>事件演示</h2>
        <p>请按键盘或点击操作</p>
        <p>请按键盘或点击操作</p>
        <div class="lazy-image" data-src="images/db03.svg">
            <span>懒加载占位...</span>
        </div>
    </div>

    <div style="height: 500px;"></div>

    <div class="scroll-info" id="scrollInfo">
        <div>????: <span id="scrollPos">0</span>px</div>
        <div>????: <span id="docHeight">0</span>px</div>
        <div>????: <span id="viewHeight">0</span>px</div>
    </div>

    <button class="back-to-top" id="backToTop">?</button>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const navbar = document.getElementById('navbar');
            const scrollProgress = document.getElementById('scrollProgress');
            const scrollPos = document.getElementById('scrollPos');
            const docHeight = document.getElementById('docHeight');
            const viewHeight = document.getElementById('viewHeight');
            const backToTop = document.getElementById('backToTop');
            const lazyImages = document.querySelectorAll('.lazy-image');

            // **【代码注释】**见下方说明块
            function updateScrollInfo() {
                const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
                const documentHeight = document.documentElement.scrollHeight;
                const viewportHeight = window.innerHeight;

                scrollPos.textContent = Math.round(scrollTop);
                docHeight.textContent = documentHeight;
                viewHeight.textContent = viewportHeight;

                // **【代码注释】**见下方说明块
                const progress = (scrollTop / (documentHeight - viewportHeight)) * 100;
                scrollProgress.style.width = progress + '%';

                // **【代码注释】**见下方说明块
                if (scrollTop > 50) {
                    navbar.classList.add('scrolled');
                } else {
                    navbar.classList.remove('scrolled');
                }

                // **【代码注释】**见下方说明块
                if (scrollTop > 500) {
                    backToTop.classList.add('show');
                } else {
                    backToTop.classList.remove('show');
                }
            }

            // **【代码注释】**见下方说明块
            function lazyLoad() {
                lazyImages.forEach(function(container) {
                    const rect = container.getBoundingClientRect();
                    const img = container.querySelector('img');

                    if (rect.top < window.innerHeight && rect.bottom > 0 && !img) {
                        const src = container.dataset.src;
                        const image = document.createElement('img');
                        image.alt = 'Lazy loaded image';
                        image.addEventListener('load', function() {
                            this.classList.add('loaded');
                            container.querySelector('span').style.display = 'none';
                        });
                        image.src = src;
                        container.appendChild(image);
                    }
                });
            }

            // **【代码注释】**见下方说明块
            let ticking = false;
            window.addEventListener('scroll', function() {
                if (!ticking) {
                    window.requestAnimationFrame(function() {
                        updateScrollInfo();
                        lazyLoad();
                        ticking = false;
                    });
                    ticking = true;
                }
            });

            // **【代码注释】**见下方说明块
            backToTop.addEventListener('click', function() {
                window.scrollTo({
                    top: 0,
                    behavior: 'smooth'
                });
            });

            // **【代码注释】**见下方说明块
            updateScrollInfo();
            lazyLoad();
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  1. scroll 配合 requestAnimationFrame 节流,更新进度条与导航样式。
  2. scrollTop / (scrollHeight - clientHeight) 计算阅读进度百分比。
  3. getBoundingClientRect() + 视口高度实现懒加载图片。
  4. scrollTo({ behavior: 'smooth' }) 实现回到顶部。

实战场景:文档站阅读进度、吸顶导航、无限列表懒加载、回到顶部按钮。

8.2 视口尺寸改变 resize

resize 在窗口或元素尺寸变化时触发(窗口级监听 window)。

  • 节流:resize 高频,用防抖/节流再重算布局。
  • 典型用途:ECharts/Canvas 重绘、响应式表格列宽、移动端横竖屏切换。
  • 注意 :仅尺寸变化触发,与 scroll 无关;visualViewport 可辅助移动端软键盘场景。
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>
        .resize-demo {
            padding: 20px;
            max-width: 1200px;
            margin: 0 auto;
        }

        .size-info-panel {
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 20px;
            background: rgba(0,0,0,0.8);
            color: white;
            border-radius: 10px;
            font-family: monospace;
            z-index: 1000;
            min-width: 200px;
        }

        .size-info-panel div {
            margin: 10px 0;
        }

        .size-info-panel .label {
            color: #aaa;
        }

        .size-info-panel .value {
            color: #00ff00;
            font-size: 18px;
        }

        .responsive-grid {
            display: grid;
            gap: 20px;
            margin-top: 20px;
        }

        .grid-item {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px;
            border-radius: 10px;
            text-align: center;
            min-height: 150px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .grid-item h3 {
            margin-bottom: 10px;
        }

        .breakpoint-info {
            background: #f5f5f5;
            padding: 20px;
            border-radius: 10px;
            margin-bottom: 20px;
        }

        .breakpoint-bar {
            display: flex;
            height: 30px;
            border-radius: 5px;
            overflow: hidden;
            margin-top: 10px;
        }

        .breakpoint-segment {
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 12px;
            font-weight: bold;
            transition: all 0.3s;
        }
    </style>
</head>
<body>
    <div class="size-info-panel" id="sizeInfo">
        <div><span class="label">????:</span> <span class="value" id="viewportWidth">0</span>px</div>
        <div><span class="label">????:</span> <span class="value" id="viewportHeight">0</span>px</div>
        <div><span class="label">??:</span> <span class="value" id="breakpoint">-</span></div>
        <div><span class="label">????:</span> <span class="value" id="gridColumns">0</span></div>
    </div>

    <div class="resize-demo">
        <h1 style="text-align: center;">示例页面</h1>
        <p style="text-align: center;">请按键盘或点击操作</p>

        <div class="breakpoint-info">
            <h3>?????</h3>
            <div class="breakpoint-bar" id="breakpointBar">
                <div class="breakpoint-segment" style="width: 33.33%; background: #ff6b6b;">XS</div>
                <div class="breakpoint-segment" style="width: 33.33%; background: #feca57;">SM</div>
                <div class="breakpoint-segment" style="width: 33.33%; background: #48dbfb;">LG</div>
            </div>
            <p style="margin-top: 10px;">
                <strong>XS:</strong> &lt;768px | <strong>SM:</strong> 768-1024px | <strong>LG:</strong> &gt;1024px
            </p>
        </div>

        <div class="responsive-grid" id="responsiveGrid">
            <!-- ??????JS???? -->
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const viewportWidth = document.getElementById('viewportWidth');
            const viewportHeight = document.getElementById('viewportHeight');
            const breakpoint = document.getElementById('breakpoint');
            const gridColumns = document.getElementById('gridColumns');
            const responsiveGrid = document.getElementById('responsiveGrid');
            const breakpointBar = document.getElementById('breakpointBar');

            // **【代码注释】**见下方说明块
            const breakpoints = {
                xs: { min: 0, max: 767, columns: 1, name: 'XS (Extra Small)' },
                sm: { min: 768, max: 1023, columns: 2, name: 'SM (Small)' },
                lg: { min: 1024, max: Infinity, columns: 3, name: 'LG (Large)' }
            };

            // **【代码注释】**见下方说明块
            function updateSizeInfo() {
                const width = window.innerWidth;
                const height = window.innerHeight;

                viewportWidth.textContent = width;
                viewportHeight.textContent = height;

                // **【代码注释】**见下方说明块
                let currentBreakpoint;
                if (width < 768) {
                    currentBreakpoint = breakpoints.xs;
                    highlightBreakpoint(0);
                } else if (width < 1024) {
                    currentBreakpoint = breakpoints.sm;
                    highlightBreakpoint(1);
                } else {
                    currentBreakpoint = breakpoints.lg;
                    highlightBreakpoint(2);
                }

                breakpoint.textContent = currentBreakpoint.name;
                gridColumns.textContent = currentBreakpoint.columns;

                // **【代码注释】**见下方说明块
                updateGrid(currentBreakpoint.columns);
            }

            // **【代码注释】**见下方说明块
            function highlightBreakpoint(index) {
                const segments = breakpointBar.querySelectorAll('.breakpoint-segment');
                segments.forEach((seg, i) => {
                    if (i === index) {
                        seg.style.transform = 'scale(1.1)';
                        seg.style.boxShadow = '0 0 10px rgba(0,0,0,0.3)';
                    } else {
                        seg.style.transform = 'scale(1)';
                        seg.style.boxShadow = 'none';
                    }
                });
            }

            // **【代码注释】**见下方说明块
            function updateGrid(columns) {
                responsiveGrid.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;

                // **【代码注释】**见下方说明块
                const itemCount = columns * 2;
                while (responsiveGrid.children.length < itemCount) {
                    const item = document.createElement('div');
                    item.className = 'grid-item';
                    item.innerHTML = `
                        <h3>??? ${responsiveGrid.children.length + 1}</h3>
                        <p>????: ${columns}</p>
                    `;
                    responsiveGrid.appendChild(item);
                }
            }

            // **【代码注释】**见下方说明块
            function debounce(func, wait) {
                let timeout;
                return function() {
                    clearTimeout(timeout);
                    timeout = setTimeout(func, wait);
                };
            }

            // **【代码注释】**见下方说明块
            window.addEventListener('resize', debounce(updateSizeInfo, 100));

            // **【代码注释】**见下方说明块
            updateSizeInfo();
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  • 封装 updateSizeInfo(),读取 innerWidth 更新面板并计算栅格列数。
  • window 监听 resize,用 debounce(updateSizeInfo, 100) 合并 100ms 内的连续触发。
  • highlightBreakpoint() 高亮当前断点;updateGrid(columns) 设置 grid-template-columns

关键 API / 概念

  • resize:窗口尺寸变化;元素级尺寸用 ResizeObserver
  • window.innerWidth / innerHeight:布局视口;与 CSS @media 断点配合。
  • debounce(func, wait):resize 场景必备,避免布局抖动。
  • gridTemplateColumnsrepeat(n, 1fr) 实现响应式列数。

注意点

  • 使用 window.addEventListener('resize', ...),没有 window.onresize 属性链式写法。
  • 单元素尺寸变化(侧边栏折叠)优先 ResizeObserver,勿误用 window resize。
  • 移动端软键盘可能只改变 visual viewport,需 visualViewport API 辅助。

实战场景

  • ECharts、Canvas 在 resize 后调用 chart.resize()
  • 后台管理系统侧栏展开/收起后重算表格列宽。

8.3 触摸与 Pointer 事件(扩展)

移动端常用 touchstart / touchmove / touchend ;现代推荐 Pointer Eventspointerdown / pointermove / pointerup)统一鼠标、触控笔与触摸(MDN Pointer events)。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pointer ????</title>
    <style>
        body { margin: 0; font-family: sans-serif; }
        .pad {
            width: 100%; height: 70vh; background: linear-gradient(135deg, #1e3c72, #2a5298);
            touch-action: none; /* ????????????????? */
            position: relative; overflow: hidden;
        }
        .dot {
            position: absolute; width: 48px; height: 48px; margin: -24px 0 0 -24px;
            border-radius: 50%; background: #ff6b6b; color: #fff;
            display: flex; align-items: center; justify-content: center;
            font-size: 12px; pointer-events: none; user-select: none;
        }
        .info { padding: 16px; background: #f5f5f5; font-family: monospace; font-size: 13px; }
    </style>
</head>
<body>
    <p class="info" id="info">请按键盘或点击操作</p>
    <div class="pad" id="pad"></div>
    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function () {
            const pad = document.getElementById('pad');
            const info = document.getElementById('info');
            let dot = null;

            pad.addEventListener('pointerdown', function (e) {
                pad.setPointerCapture(e.pointerId); // 【见下方代码注释】
                dot = document.createElement('div');
                dot.className = 'dot';
                dot.textContent = e.pointerType; // mouse / touch / pen
                pad.appendChild(dot);
                moveDot(e);
            });

            pad.addEventListener('pointermove', function (e) {
                if (dot) moveDot(e);
            });

            pad.addEventListener('pointerup', function (e) {
                pad.releasePointerCapture(e.pointerId);
                if (dot) { dot.remove(); dot = null; }
                info.textContent = '??: ' + e.pointerType + ', pressure=' + e.pressure;
            });

            function moveDot(e) {
                const rect = pad.getBoundingClientRect();
                dot.style.left = (e.clientX - rect.left) + 'px';
                dot.style.top = (e.clientY - rect.top) + 'px';
                info.textContent = 'type=' + e.pointerType + ', x=' + Math.round(e.clientX - rect.left);
            }
        })();
    </script>
</body>
</html>

【代码注释】

  • touch-action: none 配合 Pointer 事件,避免滚动与自定义手势冲突。
  • setPointerCapture 将后续 pointermove/pointerup 定向到当前元素,类似拖拽时监听 document
  • pointerType 区分 mousetouchpen,便于统计多端行为。
  • 市面应用:签名板、画板 App、移动端地图拖拽。

【本章小结】

事件 绑定对象 典型场景
scroll 元素 / window 进度条、吸顶、懒加载
resize window 图表重绘、响应式布局
pointer* 统一指针 触控+鼠标一体化

九、事件流机制深度解析

9.1 事件传播三阶段回顾

子元素(目标) 父元素 body document window 子元素(目标) 父元素 body document window 捕获阶段(自上而下) 目标阶段 冒泡阶段(自下而上) 1. 捕获 2. 捕获 3. 捕获 4. 捕获 5. 目标 6. 冒泡 7. 冒泡 8. 冒泡 9. 冒泡

9.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 {
            font-family: Arial, sans-serif;
            padding: 20px;
            background: #f5f5f5;
        }

        .event-flow-demo {
            max-width: 800px;
            margin: 0 auto;
        }

        .element-box {
            padding: 40px;
            margin: 20px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        .outer {
            background: #ffeaa7;
        }

        .middle {
            background: #74b9ff;
        }

        .inner {
            background: #ff7675;
            color: white;
            text-align: center;
            padding: 30px;
        }

        .log-panel {
            background: #2d2d2d;
            color: #00ff00;
            padding: 15px;
            border-radius: 10px;
            height: 300px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 12px;
        }

        .log-item {
            padding: 5px;
            margin: 2px 0;
            border-left: 3px solid;
            padding-left: 8px;
        }

        .log-item.capture { border-left-color: #00cec9; }
        .log-item.target { border-left-color: #fdcb6e; }
        .log-item.bubble { border-left-color: #e17055; }

        .controls {
            margin: 20px 0;
            padding: 20px;
            background: white;
            border-radius: 10px;
        }

        .checkbox-group {
            display: flex;
            gap: 20px;
            margin-bottom: 15px;
        }

        .checkbox-group label {
            display: flex;
            align-items: center;
            gap: 5px;
            cursor: pointer;
        }

        .btn {
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            background: #6c5ce7;
            color: white;
            cursor: pointer;
            margin-right: 10px;
        }

        .btn:hover {
            background: #a29bfe;
        }
    </style>
</head>
<body>
    <div class="event-flow-demo">
        <h1 style="text-align: center;">DOM???????</h1>

        <div class="controls">
            <h3>事件时间线</h3>
            <div class="checkbox-group">
                <label><input type="checkbox" checked data-type="capture" data-target="all"> ????</label>
                <label><input type="checkbox" checked data-type="bubble" data-target="all"> ????</label>
            </div>
            <button class="btn" id="clearLog">????</button>
            <button class="btn" id="resetAll">??</button>
        </div>

        <div class="element-box outer" id="outer">
            滚轮演示区
            <div class="element-box middle" id="middle">
                滚轮演示区
                <div class="element-box inner" id="inner">
                    滚动方向:
                </div>
            </div>
        </div>

        <div class="log-panel" id="logPanel">
            <div class="log-item">等待操作...</div>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const elements = {
                outer: document.getElementById('outer'),
                middle: document.getElementById('middle'),
                inner: document.getElementById('inner')
            };
            const logPanel = document.getElementById('logPanel');

            // **【代码注释】**见下方说明块
            const handlers = {
                outer: { capture: null, bubble: null },
                middle: { capture: null, bubble: null },
                inner: { capture: null, bubble: null }
            };

            // **【代码注释】**见下方说明块
            function log(message, type) {
                const time = new Date().toLocaleTimeString();
                const div = document.createElement('div');
                div.className = `log-item ${type}`;
                div.textContent = `[${time}] ${message}`;
                logPanel.appendChild(div);
                logPanel.scrollTop = logPanel.scrollHeight;
            }

            // **【代码注释】**见下方说明块
            function createHandler(elementName, phase) {
                return function(event) {
                    const phaseName = phase === 'capture' ? '??' : '??';
                    const type = event.eventPhase === 2 ? 'target' : phase;
                    log(`${elementName} - ${phaseName}?? (eventPhase: ${event.eventPhase})`, type);
                };
            }

            // **【代码注释】**见下方说明块
            function updateListeners() {
                const enableCapture = document.querySelector('[data-type="capture"][data-target="all"]').checked;
                const enableBubble = document.querySelector('[data-type="bubble"][data-target="all"]').checked;

                Object.keys(elements).forEach(key => {
                    const el = elements[key];

                    // **【代码注释】**见下方说明块
                    if (handlers[key].capture) {
                        el.removeEventListener('click', handlers[key].capture, true);
                        handlers[key].capture = null;
                    }
                    if (handlers[key].bubble) {
                        el.removeEventListener('click', handlers[key].bubble, false);
                        handlers[key].bubble = null;
                    }

                    // **【代码注释】**见下方说明块
                    if (enableCapture) {
                        handlers[key].capture = createHandler(key, 'capture');
                        el.addEventListener('click', handlers[key].capture, true);
                    }
                    if (enableBubble) {
                        handlers[key].bubble = createHandler(key, 'bubble');
                        el.addEventListener('click', handlers[key].bubble, false);
                    }
                });
            }

            // **【代码注释】**见下方说明块
            updateListeners();

            // **【代码注释】**见下方说明块
            document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
                checkbox.addEventListener('change', updateListeners);
            });

            // **【代码注释】**见下方说明块
            document.getElementById('clearLog').addEventListener('click', function() {
                logPanel.innerHTML = '<div class="log-item">等待操作...</div>';
            });

            // **【代码注释】**见下方说明块
            document.getElementById('resetAll').addEventListener('click', function() {
                document.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true);
                updateListeners();
                logPanel.innerHTML = '<div class="log-item">等待操作...</div>';
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  • 见示例代码中的 addEventListener 注册与事件对象常用属性。
  • 在浏览器控制台查看输出,对照 MDN 文档理解触发顺序。

实战场景

  • 将示例复制为独立 .html 文件即可本地运行验证。

十、Event 对象详解

10.1 Event 对象属性总览

Event
+bubbles: boolean
+cancelable: boolean
+currentTarget: Element
+defaultPrevented: boolean
+eventPhase: number
+target: Element
+timeStamp: number
+type: string
+preventDefault()
+stopPropagation()
+stopImmediatePropagation()
MouseEvent
+button: number
+buttons: number
+clientX: number
+clientY: number
+offsetX: number
+offsetY: number
+pageX: number
+pageY: number
+screenX: number
+screenY: number
KeyboardEvent
+key: string
+code: string
+keyCode: number
+ctrlKey: boolean
+shiftKey: boolean
+altKey: boolean
+metaKey: boolean

10.2 target 与 currentTarget 区别

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>target 与 currentTarget 对比</title>
    <style>
        .demo-container {
            width: 600px;
            margin: 50px auto;
            padding: 30px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .click-area {
            padding: 40px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 10px;
            color: white;
        }

        .click-area button {
            padding: 15px 30px;
            background: white;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            margin: 10px;
        }

        .info-panel {
            margin-top: 20px;
            padding: 20px;
            background: #f5f5f5;
            border-radius: 10px;
        }

        .info-item {
            margin: 10px 0;
            padding: 10px;
            background: white;
            border-radius: 5px;
            font-family: monospace;
        }

        .info-item .label {
            color: #666;
            font-weight: bold;
        }

        .info-item .value {
            color: #667eea;
            word-break: break-all;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">target ? currentTarget ???</h1>

    <div class="demo-container">
        <div class="click-area" id="clickArea">
            <h3>对比 target 与 currentTarget</h3>
            <p>请按键盘或点击操作</p>
            <button id="btn1">?? A</button>
            <button id="btn2">?? B</button>
        </div>

        <div class="info-panel">
            <h3>点击测试区</h3>
            <div class="info-item">
                <div class="label">event.target:</div>
                <div class="value" id="targetInfo">-</div>
            </div>
            <div class="info-item">
                <div class="label">event.currentTarget:</div>
                <div class="value" id="currentTargetInfo">-</div>
            </div>
            <div class="info-item">
                <div class="label">this:</div>
                <div class="value" id="thisInfo">-</div>
            </div>
            <div class="info-item">
                <div class="label">??:</div>
                <div class="value">
                    target: 实际被点击的节点<br>
                    currentTarget: 绑定监听器的节点
                </div>
            </div>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const clickArea = document.getElementById('clickArea');
            const targetInfo = document.getElementById('targetInfo');
            const currentTargetInfo = document.getElementById('currentTargetInfo');
            const thisInfo = document.getElementById('thisInfo');

            clickArea.addEventListener('click', function(event) {
                targetInfo.textContent = event.target.tagName + (event.target.id ? '#' + event.target.id : '');
                currentTargetInfo.textContent = event.currentTarget.tagName + (event.currentTarget.id ? '#' + event.currentTarget.id : '');
                thisInfo.textContent = this.tagName + (this.id ? '#' + this.id : '');
            });
        })();
    </script>
</body>
</html>

【代码注释】

  • click:完整的「按下 + 在同一元素内释放」才触发;若按下后拖出元素再释放,可能不触发 click
  • dblclick:两次 click 间隔极短时触发;注意与两次单独 click 的交互设计(如「单击选中、双击打开」需防抖区分)。
  • contextmenu:右键菜单;event.preventDefault() 可阻止系统菜单,用于自定义右键面板(地图标注、表格行操作等)。

市面应用:GitHub 代码行号区右键菜单、Figma 画布右键、电商商品图「右键另存为」拦截。

10.3 阻止默认行为 preventDefault

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>
        .bubble-demo {
            max-width: 600px;
            margin: 50px auto;
        }

        .box {
            padding: 40px;
            margin: 20px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            transition: background 0.3s;
        }

        .box.outer { background: #ffeaa7; }
        .box.middle { background: #74b9ff; }
        .box.inner { background: #ff7675; color: white; }

        .box.clicked {
            transform: scale(0.98);
        }

        .log-panel {
            background: #2d2d2d;
            color: #00ff00;
            padding: 15px;
            border-radius: 10px;
            height: 200px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 12px;
        }

        .controls {
            margin: 20px 0;
            padding: 20px;
            background: white;
            border-radius: 10px;
        }

        .toggle-switch {
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .toggle-switch input {
            width: 20px;
            height: 20px;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>

    <div class="bubble-demo">
        <div class="controls">
            <label class="toggle-switch">
                <input type="checkbox" id="stopBubble">
                <span>事件传播顺序演示</span>
            </label>
            <button class="btn" id="clearLog" style="margin-left: 20px;">清空日志</button>
        </div>

        <div class="box outer" id="outer">
            滚轮演示区
            <div class="box middle" id="middle">
                滚轮演示区
                <div class="box inner" id="inner">
                    滚动方向:
                </div>
            </div>
        </div>

        <div class="log-panel" id="logPanel">
            <div>等待操作...</div>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const elements = {
                outer: document.getElementById('outer'),
                middle: document.getElementById('middle'),
                inner: document.getElementById('inner')
            };
            const stopBubble = document.getElementById('stopBubble');
            const logPanel = document.getElementById('logPanel');

            function log(message) {
                const time = new Date().toLocaleTimeString();
                const div = document.createElement('div');
                div.textContent = `[${time}] ${message}`;
                logPanel.appendChild(div);
                logPanel.scrollTop = logPanel.scrollHeight;
            }

            // **【代码注释】**见下方说明块
            Object.keys(elements).forEach(key => {
                const el = elements[key];

                el.addEventListener('click', function(event) {
                    const name = el.className.split(' ').find(c => ['outer', 'middle', 'inner'].includes(c));

                    // **【代码注释】**见下方说明块
                    el.classList.add('clicked');
                    setTimeout(() => el.classList.remove('clicked'), 200);

                    log(`${name} 阶段`);

                    // **【代码注释】**见下方说明块
                    if (key === 'inner' && stopBubble.checked) {
                        event.stopPropagation();
                        log('已阻止进一步传播');
                    }
                });
            });

            // **【代码注释】**见下方说明块
            document.getElementById('clearLog').addEventListener('click', function() {
                logPanel.innerHTML = '<div>等待操作...</div>';
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  • 见示例代码中的 addEventListener 注册与事件对象常用属性。
  • 在浏览器控制台查看输出,对照 MDN 文档理解触发顺序。

实战场景

  • 将示例复制为独立 .html 文件即可本地运行验证。

10.4 阻止传播 stopPropagation

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>
        .prevent-demo {
            max-width: 800px;
            margin: 50px auto;
            padding: 30px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .demo-section {
            margin: 30px 0;
            padding: 20px;
            background: #f5f5f5;
            border-radius: 10px;
        }

        .demo-section h3 {
            margin-top: 0;
            color: #333;
        }

        .link-demo a {
            color: #667eea;
            text-decoration: none;
            font-weight: bold;
        }

        .form-demo input {
            padding: 10px;
            border: 2px solid #e0e0e0;
            border-radius: 5px;
            width: 200px;
        }

        .form-demo button {
            padding: 10px 20px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }

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

        .controls {
            margin: 20px 0;
            padding: 15px;
            background: #e3f2fd;
            border-radius: 10px;
        }

        .checkbox-group {
            display: flex;
            gap: 20px;
            flex-wrap: wrap;
        }

        .checkbox-group label {
            display: flex;
            align-items: center;
            gap: 5px;
            cursor: pointer;
        }

        .message {
            margin-top: 15px;
            padding: 10px;
            background: #fff3cd;
            border-radius: 5px;
            display: none;
        }

        .message.show {
            display: block;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>

    <div class="prevent-demo">
        <div class="controls">
            <h3>document 级快捷键演示</h3>
            <div class="checkbox-group">
                <label><input type="checkbox" id="preventLink"> 阻止链接跳转</label>
                <label><input type="checkbox" id="preventForm"> 阻止表单提交</label>
                <label><input type="checkbox" id="preventContext"> 阻止右键菜单</label>
            </div>
            <div class="message" id="message">等待操作...</div>
        </div>

        <div class="demo-section link-demo">
            <h3>1. 链接默认行为</h3>
            <p><a href="https://example.com" id="testLink">点击测试(默认会打开 example.com)</a></p>
        </div>

        <div class="demo-section form-demo">
            <h3>2. 表单提交</h3>
            <form id="testForm">
                <input type="text" placeholder="请输入..." required>
                <button type="submit">??</button>
            </form>
        </div>

        <div class="demo-section context-demo" id="contextArea">
            <h3>3. 右键菜单</h3>
            <p>请按键盘或点击操作</p>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const preventLink = document.getElementById('preventLink');
            const preventForm = document.getElementById('preventForm');
            const preventContext = document.getElementById('preventContext');
            const message = document.getElementById('message');

            function showMessage() {
                message.classList.add('show');
                setTimeout(() => message.classList.remove('show'), 2000);
            }

            // **【代码注释】**见下方说明块
            document.getElementById('testLink').addEventListener('click', function(e) {
                if (preventLink.checked) {
                    e.preventDefault();
                    console.log('按钮被点击了!');
                    showMessage();
                }
            });

            // **【代码注释】**见下方说明块
            document.getElementById('testForm').addEventListener('submit', function(e) {
                if (preventForm.checked) {
                    e.preventDefault();
                    console.log('按钮被点击了!');
                    showMessage();
                }
            });

            // **【代码注释】**见下方说明块
            document.getElementById('contextArea').addEventListener('contextmenu', function(e) {
                if (preventContext.checked) {
                    e.preventDefault();
                    console.log('按钮被点击了!');
                    showMessage();
                }
            });
        })();
    </script>
</body>
</html>

【代码注释】

  • click:完整的「按下 + 在同一元素内释放」才触发;若按下后拖出元素再释放,可能不触发 click
  • dblclick:两次 click 间隔极短时触发;注意与两次单独 click 的交互设计(如「单击选中、双击打开」需防抖区分)。
  • contextmenu:右键菜单;event.preventDefault() 可阻止系统菜单,用于自定义右键面板(地图标注、表格行操作等)。

市面应用:GitHub 代码行号区右键菜单、Figma 画布右键、电商商品图「右键另存为」拦截。

十一、事件委托模式

11.1 事件委托原理

事件委托(Event Delegation) 利用冒泡,在父节点统一监听子元素事件,减少监听器数量、支持动态子节点。

说明 :判断 event.target 是否匹配选择器,或用 event.target.closest('.item') 向上查找;勿与 mouseenter(不冒泡)混淆,列表点击常用 click
冒泡
冒泡
冒泡
父容器 ul#list
一个 click 监听
动态 li 1
动态 li 2
动态 li 3
判断 target
执行业务逻辑

11.2 事件委托实战演示

?? ??

11.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>
        .delegation-demo {
            max-width: 800px;
            margin: 50px auto;
            padding: 30px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .todo-list {
            list-style: none;
            padding: 0;
        }

        .todo-item {
            display: flex;
            align-items: center;
            padding: 15px;
            margin-bottom: 10px;
            background: #f5f5f5;
            border-radius: 5px;
            transition: all 0.3s;
        }

        .todo-item:hover {
            background: #e9ecef;
        }

        .todo-item.completed {
            opacity: 0.6;
        }

        .todo-item.completed .todo-text {
            text-decoration: line-through;
        }

        .todo-checkbox {
            width: 20px;
            height: 20px;
            margin-right: 15px;
            cursor: pointer;
        }

        .todo-text {
            flex: 1;
            font-size: 16px;
        }

        .todo-delete {
            padding: 5px 15px;
            background: #ff6b6b;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            opacity: 0.8;
            transition: opacity 0.3s;
        }

        .todo-delete:hover {
            opacity: 1;
        }

        .input-group {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
        }

        .input-group input {
            flex: 1;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 5px;
            font-size: 16px;
        }

        .input-group button {
            padding: 12px 25px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
        }

        .stats {
            display: flex;
            justify-content: space-between;
            margin-top: 20px;
            padding: 15px;
            background: #e3f2fd;
            border-radius: 5px;
        }

        .log-panel {
            margin-top: 20px;
            padding: 15px;
            background: #2d2d2d;
            color: #00ff00;
            border-radius: 5px;
            height: 150px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 12px;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">待办列表 - 事件委托</h1>

    <div class="delegation-demo">
        <div class="input-group">
            <input type="text" id="todoInput" placeholder="输入待办事项...">
            <button id="addBtn">??</button>
        </div>

        <ul class="todo-list" id="todoList">
            <li class="todo-item" data-id="1">
                <input type="checkbox" class="todo-checkbox">
                <span class="todo-text">学习 JavaScript 事件</span>
                <button class="todo-delete">??</button>
            </li>
            <li class="todo-item" data-id="2">
                <input type="checkbox" class="todo-checkbox">
                <span class="todo-text">复习 DOM 委托</span>
                <button class="todo-delete">??</button>
            </li>
            <li class="todo-item" data-id="3">
                <input type="checkbox" class="todo-checkbox">
                <span class="todo-text">完成课后作业</span>
                <button class="todo-delete">??</button>
            </li>
        </ul>

        <div class="stats">
            <span>总计: <strong id="totalCount">3</strong></span>
            <span>已完成: <strong id="completedCount">0</strong></span>
            <span>未完成: <strong id="activeCount">3</strong></span>
        </div>

        <div class="log-panel" id="logPanel">
            <div>等待操作...</div>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const todoList = document.getElementById('todoList');
            const todoInput = document.getElementById('todoInput');
            const addBtn = document.getElementById('addBtn');
            const logPanel = document.getElementById('logPanel');
            let todoId = 4;

            function log(message) {
                const time = new Date().toLocaleTimeString();
                const div = document.createElement('div');
                div.textContent = `[${time}] ${message}`;
                logPanel.appendChild(div);
                logPanel.scrollTop = logPanel.scrollHeight;
            }

            function updateStats() {
                const items = todoList.querySelectorAll('.todo-item');
                const completed = todoList.querySelectorAll('.todo-item.completed');
                document.getElementById('totalCount').textContent = items.length;
                document.getElementById('completedCount').textContent = completed.length;
                document.getElementById('activeCount').textContent = items.length - completed.length;
            }

            // **【代码注释】**见下方说明块
            todoList.addEventListener('click', function(event) {
                const target = event.target;
                const todoItem = target.closest('.todo-item');

                if (!todoItem) return;

                // **【代码注释】**见下方说明块
                if (target.classList.contains('todo-checkbox')) {
                    todoItem.classList.toggle('completed', target.checked);
                    log(`勾选状态: ${todoItem.querySelector('.todo-text').textContent} -> ${target.checked ? '完成' : '未完成'}`);
                    updateStats();
                }

                // **【代码注释】**见下方说明块
                if (target.classList.contains('todo-delete')) {
                    const text = todoItem.querySelector('.todo-text').textContent;
                    todoItem.remove();
                    log(`删除: ${text}`);
                    updateStats();
                }
            });

            // **【代码注释】**见下方说明块
            function addTodo() {
                const text = todoInput.value.trim();
                if (!text) return;

                const li = document.createElement('li');
                li.className = 'todo-item';
                li.dataset.id = todoId++;
                li.innerHTML = `
                    <input type="checkbox" class="todo-checkbox">
                    <span class="todo-text">${text}</span>
                    <button class="todo-delete">??</button>
                `;

                todoList.appendChild(li);
                todoInput.value = '';
                log(`新增: ${text}`);
                updateStats();
            }

            addBtn.addEventListener('click', addTodo);
            todoInput.addEventListener('keypress', function(e) {
                if (e.key === 'Enter') addTodo();
            });

            // **【代码注释】**见下方说明块
            updateStats();
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  • 见示例代码中的 addEventListener 注册与事件对象常用属性。
  • 在浏览器控制台查看输出,对照 MDN 文档理解触发顺序。

实战场景

  • 将示例复制为独立 .html 文件即可本地运行验证。

【本章小结】


?
用户点击子元素
event.target 匹配?
执行业务逻辑
??
父元素监听

【代码注释】

  • 列表/表格:用 event.target + closest() 定位行。
  • 不冒泡事件(如 mouseenterfocus)无法委托,需直接绑定。

十二、DOM 对象原型链分析

12.1 DOM 节点与原型链

实例对象

例: div 元素
HTMLDivElement.prototype
HTMLElement.prototype
Element.prototype
Node.prototype
EventTarget.prototype
Object.prototype

核心逻辑

层级 主要能力
Object.prototype 基础对象方法 toString(), valueOf()
EventTarget.prototype 事件监听 addEventListener(), removeEventListener(), dispatchEvent()
Node.prototype 节点树 appendChild(), cloneNode(), normalize()
Element.prototype 元素 API querySelector(), getAttribute(), classList
HTMLElement.prototype HTML 元素通用 innerHTML, style, title
HTMLDivElement.prototype div 专有(通常为空)

12.2 常用 DOM 接口继承关系

事件对象

例: click
MouseEvent.prototype
UIEvent.prototype
Event.prototype
Object.prototype

12.3 HTMLCollection 与 NodeList 区别

?? HTMLCollection NodeList
集合类型 getElementsByTagName(), getElementsByClassName(), children querySelectorAll(), getElementsByName(), childNodes
forEach ❌ 不支持 ✅ 支持
length?? ? ? ? ?
item()?? ? ? ? ?
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMLCollection?NodeList???</title>
    <style>
        .collection-demo {
            max-width: 800px;
            margin: 50px auto;
            padding: 30px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .demo-box {
            padding: 40px;
            background: #f5f5f5;
            border-radius: 10px;
            margin-bottom: 20px;
        }

        .comparison-table {
            width: 100%;
            border-collapse: collapse;
            margin: 20px 0;
        }

        .comparison-table th,
        .comparison-table td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #e0e0e0;
        }

        .comparison-table th {
            background: #667eea;
            color: white;
        }

        .code-block {
            background: #2d2d2d;
            color: #00ff00;
            padding: 15px;
            border-radius: 5px;
            font-family: monospace;
            margin: 10px 0;
            overflow-x: auto;
        }

        .test-section {
            padding: 20px;
            background: #e3f2fd;
            border-radius: 10px;
            margin-top: 20px;
        }

        .button-group {
            display: flex;
            gap: 10px;
            margin-top: 15px;
        }

        .btn {
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            background: #667eea;
            color: white;
            cursor: pointer;
        }

        .btn:hover {
            background: #764ba2;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">HTMLCollection ? NodeList ???</h1>

    <div class="collection-demo">
        <table class="comparison-table">
            <thead>
                <tr>
                    <th>??</th>
                    <th>HTMLCollection</th>
                    <th>NodeList</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td><strong>????</strong></td>
                    <td>getElementsByTagName()<br>getElementsByClassName()<br>children</td>
                    <td>querySelectorAll()<br>getElementsByName()<br>childNodes</td>
                </tr>
                <tr>
                    <td><strong>????</strong></td>
                    <td>?????</td>
                    <td>??????</td>
                </tr>
                <tr>
                    <td><strong>forEach</strong></td>
                    <td>? ???</td>
                    <td>? ??</td>
                </tr>
                <tr>
                    <td><strong>点我</strong></td>
                    <td>????</td>
                    <td>????</td>
                </tr>
            </tbody>
        </table>

        <div class="demo-box">
            <h3>点击测试区</h3>
            <div id="testArea">
                <p>?? 1</p>
                <p>?? 2</p>
                <p>?? 3</p>
            </div>

            <div class="button-group">
                <button class="btn" id="testHTMLCollection">?? HTMLCollection</button>
                <button class="btn" id="testNodeList">?? NodeList</button>
                <button class="btn" id="addParagraph">????</button>
                <button class="btn" id="reset">??</button>
            </div>
        </div>

        <div class="code-block" id="resultOutput">
            滚动方向????...
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const testArea = document.getElementById('testArea');
            const resultOutput = document.getElementById('resultOutput');

            function output(message) {
                resultOutput.textContent = message;
            }

            // 【见下方代码注释】
            document.getElementById('testHTMLCollection').addEventListener('click', function() {
                const collection = testArea.getElementsByTagName('p');
                let message = 'HTMLCollection (getElementsByTagName):\n';
                message += `????: ${collection.length}\n`;
                message += `??: ${Array.from(collection).map(p => p.textContent).join(', ')}\n\n`;
                message += '?? forEach:\n';
                try {
                    collection.forEach(item => message += item.textContent);
                } catch (e) {
                    message += `??: ${e.message}\n`;
                    message += '????: Array.from(collection).forEach() ? for??';
                }
                output(message);
            });

            // 【见下方代码注释】
            document.getElementById('testNodeList').addEventListener('click', function() {
                const list = testArea.querySelectorAll('p');
                let message = 'NodeList (querySelectorAll):\n';
                message += `????: ${list.length}\n`;
                message += `??: ${Array.from(list).map(p => p.textContent).join(', ')}\n\n`;
                message += '?? forEach:\n';
                try {
                    const items = [];
                    list.forEach(item => items.push(item.textContent));
                    message += `??! ??: ${items.join(', ')}`;
                } catch (e) {
                    message += `??: ${e.message}`;
                }
                output(message);
            });

            // **【代码注释】**见下方说明块
            document.getElementById('addParagraph').addEventListener('click', function() {
                const newPara = document.createElement('p');
                newPara.textContent = `?? ${testArea.children.length + 1}`;
                testArea.appendChild(newPara);
            });

            // **【代码注释】**见下方说明块
            document.getElementById('reset').addEventListener('click', function() {
                testArea.innerHTML = `
                    <p>?? 1</p>
                    <p>?? 2</p>
                    <p>?? 3</p>
                `;
                output('???');
            });
        })();
    </script>
</body>
</html>

【代码注释】

  1. mousedown 记录 offsetX/offsetY,表示鼠标在元素内的点击偏移,避免拖动时元素「跳动」。
  2. mousemove 绑定在 document 上,防止快速拖动时指针离开元素导致中断。
  3. 使用 clientX/clientY 配合偏移计算 left/top,并做视口边界钳制。
  4. 生产环境可改用 HTML5 Drag and Drop APIPointer Events 统一鼠标/触控。

十三、最佳实践与性能优化

13.1 性能优化要点

1. 事件委托 vs 逐项绑定
javascript 复制代码
// ===== 完整可运行示例:复制整段到 .html 文件 =====
// 【见下方代码注释】
const items = document.querySelectorAll('.list-item');
items.forEach(item => {
    item.addEventListener('click', handleClick);
});

// 【见下方代码注释】
const list = document.querySelector('.list');
list.addEventListener('click', function(event) {
    if (event.target.matches('.list-item')) {
        handleClick(event);
    }
});

【代码注释】

  • click:完整的「按下 + 在同一元素内释放」才触发;若按下后拖出元素再释放,可能不触发 click
  • dblclick:两次 click 间隔极短时触发;注意与两次单独 click 的交互设计(如「单击选中、双击打开」需防抖区分)。
  • contextmenu:右键菜单;event.preventDefault() 可阻止系统菜单,用于自定义右键面板(地图标注、表格行操作等)。

市面应用:GitHub 代码行号区右键菜单、Figma 画布右键、电商商品图「右键另存为」拦截。

2. passive 与 preventDefault
javascript 复制代码
// ===== 完整可运行示例:复制整段到 .html 文件 =====
// 【见下方代码注释】
window.addEventListener('scroll', handleScroll, { passive: true });

// 【见下方代码注释】
element.addEventListener('touchmove', handleTouch, { passive: false });

【代码注释】

  • { passive: true } 告诉浏览器不会调用 preventDefault(),滚动更流畅。
  • 需要阻止默认行为(如自定义横向滑动)时使用 { passive: false }
3. 防抖与节流
javascript 复制代码
// ===== 完整可运行示例:复制整段到 .html 文件 =====
// **【代码注释】**见下方说明块
function debounce(func, wait) {
    let timeout;
    return function() {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, arguments), wait);
    };
}

// **【代码注释】**见下方说明块
function throttle(func, wait) {
    let lastTime = 0;
    return function() {
        const now = Date.now();
        if (now - lastTime >= wait) {
            func.apply(this, arguments);
            lastTime = now;
        }
    };
}

// **【代码注释】**见下方说明块
window.addEventListener('resize', debounce(handleResize, 200));
window.addEventListener('scroll', throttle(handleScroll, 100));

【代码注释】

  • 说明resizedebounce,等待用户停止拖拽窗口后再重算布局。
  • 说明scrollthrottlerequestAnimationFrame,避免每帧多次执行。
4. 组件销毁时解绑
javascript 复制代码
// ===== 完整可运行示例:复制整段到 .html 文件 =====
// **【代码注释】**见下方说明块
class MyComponent {
    constructor() {
        this.handleClick = this.handleClick.bind(this);
        this.button.addEventListener('click', this.handleClick);
    }

    destroy() {
        this.button.removeEventListener('click', this.handleClick);
    }

    handleClick(event) {
        // **【代码注释】**见下方说明块
    }
}

【代码注释】

  • click:完整的「按下 + 在同一元素内释放」才触发;若按下后拖出元素再释放,可能不触发 click
  • dblclick:两次 click 间隔极短时触发;注意与两次单独 click 的交互设计(如「单击选中、双击打开」需防抖区分)。
  • contextmenu:右键菜单;event.preventDefault() 可阻止系统菜单,用于自定义右键面板(地图标注、表格行操作等)。

市面应用:GitHub 代码行号区右键菜单、Figma 画布右键、电商商品图「右键另存为」拦截。

13.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>
        .performance-demo {
            max-width: 1000px;
            margin: 50px auto;
            padding: 30px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .grid-container {
            display: grid;
            grid-template-columns: repeat(10, 1fr);
            gap: 5px;
            margin: 20px 0;
        }

        .grid-item {
            aspect-ratio: 1;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 5px;
            cursor: pointer;
            transition: transform 0.1s;
        }

        .grid-item:hover {
            transform: scale(1.05);
        }

        .grid-item.clicked {
            background: #ff6b6b;
        }

        .stats-panel {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 20px;
            margin: 20px 0;
        }

        .stat-card {
            padding: 20px;
            background: #f5f5f5;
            border-radius: 10px;
        }

        .stat-card h3 {
            margin-top: 0;
            color: #333;
        }

        .stat-value {
            font-size: 32px;
            font-weight: bold;
            color: #667eea;
        }

        .controls {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            margin: 20px 0;
        }

        .btn {
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            background: #667eea;
            color: white;
            cursor: pointer;
        }

        .btn:hover {
            background: #764ba2;
        }

        .btn.secondary {
            background: #6c757d;
        }

        .btn.secondary:hover {
            background: #5a6268;
        }

        .log-panel {
            background: #2d2d2d;
            color: #00ff00;
            padding: 15px;
            border-radius: 10px;
            height: 150px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 12px;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>

    <div class="performance-demo">
        <div class="controls">
            <button class="btn" id="createDelegation">??1000?????????</button>
            <button class="btn" id="createIndividual">??1000?????????</button>
            <button class="btn" id="testDelegation">??????</button>
            <button class="btn" id="testIndividual">??????</button>
            <button class="btn secondary" id="clear">??</button>
        </div>

        <div class="stats-panel">
            <div class="stat-card">
                <h3>点击测试区</h3>
                <div class="stat-value" id="elementCount">0</div>
            </div>
            <div class="stat-card">
                <h3>事件时间线</h3>
                <div class="stat-value" id="listenerCount">0</div>
            </div>
            <div class="stat-card">
                <h3>点击测试区</h3>
                <div class="stat-value" id="clickCount">0</div>
            </div>
            <div class="stat-card">
                <h3>点击测试区</h3>
                <div class="stat-value" id="executionTime">0ms</div>
            </div>
        </div>

        <div class="grid-container" id="gridContainer"></div>

        <div class="log-panel" id="logPanel">
            <div>等待操作...</div>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const gridContainer = document.getElementById('gridContainer');
            const logPanel = document.getElementById('logPanel');
            const elementCount = document.getElementById('elementCount');
            const listenerCount = document.getElementById('listenerCount');
            const clickCount = document.getElementById('clickCount');
            const executionTime = document.getElementById('executionTime');

            let clicks = 0;
            let listeners = 0;

            function log(message) {
                const time = new Date().toLocaleTimeString();
                const div = document.createElement('div');
                div.textContent = `[${time}] ${message}`;
                logPanel.appendChild(div);
                logPanel.scrollTop = logPanel.scrollHeight;
            }

            function updateStats() {
                elementCount.textContent = gridContainer.children.length;
                listenerCount.textContent = listeners;
                clickCount.textContent = clicks;
            }

            // **【代码注释】**见下方说明块
            document.getElementById('createDelegation').addEventListener('click', function() {
                clearGrid();

                const start = performance.now();

                for (let i = 0; i < 1000; i++) {
                    const item = document.createElement('div');
                    item.className = 'grid-item';
                    item.textContent = i + 1;
                    gridContainer.appendChild(item);
                }

                // **【代码注释】**见下方说明块
                gridContainer.addEventListener('click', handleDelegationClick);
                listeners = 1;

                const end = performance.now();
                executionTime.textContent = (end - start).toFixed(2) + 'ms';

                updateStats();
                log(`????????1000??????: ${(end - start).toFixed(2)}ms????: 1?`);
            });

            // **【代码注释】**见下方说明块
            document.getElementById('createIndividual').addEventListener('click', function() {
                clearGrid();

                const start = performance.now();

                for (let i = 0; i < 1000; i++) {
                    const item = document.createElement('div');
                    item.className = 'grid-item';
                    item.textContent = i + 1;
                    item.addEventListener('click', handleIndividualClick);
                    gridContainer.appendChild(item);
                }

                listeners = 1000;

                const end = performance.now();
                executionTime.textContent = (end - start).toFixed(2) + 'ms';

                updateStats();
                log(`????????1000??????: ${(end - start).toFixed(2)}ms????: 1000?`);
            });

            // **【代码注释】**见下方说明块
            function handleDelegationClick(event) {
                if (event.target.classList.contains('grid-item')) {
                    clicks++;
                    event.target.classList.toggle('clicked');
                    updateStats();
                }
            }

            // **【代码注释】**见下方说明块
            function handleIndividualClick(event) {
                clicks++;
                this.classList.toggle('clicked');
                updateStats();
            }

            // **【代码注释】**见下方说明块
            document.getElementById('testDelegation').addEventListener('click', function() {
                const start = performance.now();
                for (let i = 0; i < 100; i++) {
                    const randomIndex = Math.floor(Math.random() * gridContainer.children.length);
                    gridContainer.children[randomIndex].click();
                }
                const end = performance.now();
                log(`???? - 100???????: ${(end - start).toFixed(2)}ms`);
            });

            document.getElementById('testIndividual').addEventListener('click', function() {
                const start = performance.now();
                for (let i = 0; i < 100; i++) {
                    const randomIndex = Math.floor(Math.random() * gridContainer.children.length);
                    gridContainer.children[randomIndex].click();
                }
                const end = performance.now();
                log(`???? - 100???????: ${(end - start).toFixed(2)}ms`);
            });

            // **【代码注释】**见下方说明块
            function clearGrid() {
                gridContainer.innerHTML = '';
                clicks = 0;
                listeners = 0;
                updateStats();
            }

            document.getElementById('clear').addEventListener('click', function() {
                clearGrid();
                log('???');
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  • 见示例代码中的 addEventListener 注册与事件对象常用属性。
  • 在浏览器控制台查看输出,对照 MDN 文档理解触发顺序。

实战场景

  • 将示例复制为独立 .html 文件即可本地运行验证。

十四、附录:事件速查与面试要点

14.1 监听 API 速查

javascript 复制代码
// 注册(推荐)
el.addEventListener('click', handler, { capture: false, once: false, passive: true, signal });

// 注销(须同一函数引用)
el.removeEventListener('click', handler);

// 主动派发
el.dispatchEvent(new CustomEvent('my-event', { detail: { id: 1 }, bubbles: true }));
options 字段 含义
capture 在捕获阶段触发
once 触发一次后自动移除
passive 不会调用 preventDefault(滚动优化)
signal 传入 AbortController.signal,调用 abort() 批量解绑

14.2 表单事件:input vs change

用户输入
input
每次值变化即触发
change
input: 失焦后且值变
select: 选项一变即触发

14.3 文档就绪:load vs DOMContentLoaded

DOMContentLoaded load
触发时机 HTML 解析完成 含图片、样式、iframe 等全部资源
典型用途 绑定 DOM、发起首屏请求 依赖尺寸的图表、统计像素
监听对象 document / window window

14.4 高频面试题归纳

  1. 事件委托原理? 冒泡 + 父级统一监听 + event.target 匹配。
  2. targetcurrentTarget 前者是事件源,后者是正在执行监听器的元素。
  3. 如何阻止冒泡与默认行为? stopPropagation() / stopImmediatePropagation()preventDefault()
  4. 为什么滚动监听要 passive? 避免阻塞合成线程滚动;不能阻止默认滚动时不应同步 preventDefault
  5. mouseentermouseover 前者不冒泡,子元素进出不会重复触发父级。

14.5 市面产品事件映射(技术向)

产品类型 典型事件组合
淘宝/京东商品页 scroll 吸顶导航 + load/error 主图 + input 规格筛选
百度搜索框 input/compositionend 联想 + keydown Enter 提交
Notion / 飞书文档 keydown 快捷键 + paste 剪贴板
网易云音乐播放器 timeupdate(媒体)+ click 控件
微信 H5 分享页 DOMContentLoaded 初始化 + touch/click 埋点

总结

知识点归纳(思维导图)

工程化
页面生命周期
设备事件
基础
表单与媒体
submit / input / change
img load / error
transition / animation
三种监听方式
事件流 捕获/目标/冒泡
this / target / currentTarget
鼠标 MouseEvent
键盘 KeyboardEvent
滚轮 wheel 标准
DOMContentLoaded
load
scroll / resize
事件委托
防抖节流
passive / once / AbortSignal

1. 事件监听方式
  • HTML 属性:快速演示,避免生产使用
  • DOM 属性:单监听器,易被覆盖
  • addEventListener :多监听器、捕获/冒泡、options 扩展
2. 事件流三阶段
  • 捕获document → 目标
  • 目标:在目标元素执行
  • 冒泡 :目标 → document(委托依赖此阶段)
3. 常用事件分类(扩展版)
分类 主要事件 经典场景
鼠标 click, dblclick, mousedown/up, mousemove, mouseenter/leave, wheel 拖拽、缩放、悬停菜单
键盘 keydown, keyup 快捷键、游戏、表单校验
文档 load, DOMContentLoaded 入口脚本、性能打点
表单 submit, reset, focus, blur, input, change, select 登录、搜索、联动下拉
图片 load, error 预加载进度、占位图
CSS transition*, animation* 骨架屏、轮播指示器
其他 scroll, resize 吸顶、懒加载、响应式布局
4. Event 对象核心
  • target / currentTarget / eventPhase
  • preventDefault() / stopPropagation() / stopImmediatePropagation()
  • 设备事件扩展:MouseEvent.buttonKeyboardEvent.keyWheelEvent.deltaY
5. 事件委托
  • 动态列表、表格、聊天消息列表的标配模式
  • 注意:不冒泡事件(如 mouseenter)无法委托,需直接绑定或使用 mouseover 并判断 relatedTarget
6. 性能优化清单
  • 委托减少监听器数量
  • scroll/resize/mousemove 使用节流;搜索框 input 使用防抖
  • removeEventListenerAbortController 在组件卸载时清理
  • 滚动类监听优先 { passive: true }

学习路径建议

掌握 addEventListener
理解事件流与 Event
分类练习 UI 事件
事件委托 + 性能
结合框架合成事件对比

  1. 实践优先:将文中每个 HTML 示例保存为单文件,在浏览器中逐段调试。
  2. 阅读规范 :以 MDN 事件索引为纲,遇到新 API 先查是否已废弃(如 keypresskeyCode)。
  3. 性能意识:在 DevTools Performance 中观察滚动、输入是否触发过长任务。
  4. 工程衔接 :理解原生事件后,再对比 React/Vue 中的事件绑定与修饰符(.prevent.stop)。

本篇为 JavaScript DOM 事件体系的完整技术博客:从监听注册、事件传播、各类 UI 事件到委托与性能优化,配套可运行示例与归纳总结,可作为日常开发与面试复习手册。

相关推荐
顾凌陵1 小时前
Python 数据可视化实战
开发语言·python·信息可视化
星恒随风1 小时前
从0开始的操作系统(3)
开发语言·笔记·学习
开发者联盟league1 小时前
pip install出现报错ERROR: Cannot set --home and --prefix together
开发语言·python·pip
_codemonster1 小时前
JSP 、Thymeleaf 、 JavaScript 和Vue
java·javascript·vue.js
杖雍皓1 小时前
Markstream-VUE:构建高性能流式 Markdown 渲染器
前端·javascript·vue.js·markdown·流式输出
FlagOS智算系统软件栈1 小时前
众智FlagOS完成腾讯混元MT2多语翻译模型全系列多芯片适配:英伟达/华为/平头哥三芯开箱即用
开发语言·人工智能·开源
東隅已逝,桑榆非晚1 小时前
C语言内存函数
c语言·开发语言·笔记·算法
lly2024061 小时前
Docker 安装 MySQL
开发语言
techdashen1 小时前
在 Async Rust 中实现请求合并(Request Coalescing)
开发语言·后端·rust