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

已拆分为两篇博客(推荐阅读拆分版):

篇次 文件 内容
上篇 JavaScript_DOM事件完全指南(上篇)------UI事件与监听基础.md §零~§八:监听注册、鼠标/键盘/文档/表单/图片/过渡/scroll/resize
下篇 JavaScript_DOM事件完全指南(下篇)------事件流委托与工程实践.md §九~§十四 + 总结:事件流、Event、委托、原型链、性能、附录

下文为 未拆分的完整合集(约 6300 行),发布博客或分段学习请优先使用上表两个文件。
一篇面向前端工程师的 DOM 事件系统 深度技术博客。内容覆盖事件监听、事件流、各类 UI 事件、Event 对象、事件委托与性能优化,并配有 完整可运行的 HTML 示例Mermaid 流程图知识点归纳 。示例中的本地图片统一使用 images/ 目录(db01.svg ~ db10.svg)。

权威参考(建议配合阅读):


目录


零、导读与核心概念

0.1 知识体系总览

DOM 事件是浏览器将 用户行为页面状态变化 通知给 JavaScript 的桥梁。从工程视角,可拆为四层:
DOM 事件
注册层
HTML 属性
DOM 属性
addEventListener
传播层
捕获 capture
目标 target
冒泡 bubble
数据层
Event 基类
MouseEvent
KeyboardEvent
模式层
事件委托
防抖节流
passive 优化
脚本
浏览器
用户侧
点击/键盘/滚动/输入
合成事件对象 Event
捕获 → 目标 → 冒泡
监听器 listener
业务逻辑 handler

0.2 核心名词解释

名词 英文 解析
事件 Event 在文档或浏览器窗口中发生的、可被脚本感知的交互或状态变化(如点击、加载完成)。
事件类型 event type 字符串标识,如 clickkeydown,区分不同语义。
事件目标 event target 最初派发事件的 DOM 节点,对应 event.target
当前目标 current target 正在执行监听器的元素,对应 event.currentTarget
监听器 listener / handler 事件触发时调用的函数;addEventListener 的第二个参数。
事件流 event flow 事件从 document 到目标再返回的传播路径,含捕获、目标、冒泡三阶段。
捕获 capturing 从外到内传播;addEventListener 第三参数为 true{ capture: true }
冒泡 bubbling 从内到外传播;多数日常监听默认在冒泡阶段。
默认行为 default action 浏览器内置响应,如链接跳转、表单提交;可用 preventDefault() 阻止。
事件委托 event delegation 在祖先元素上统一监听,利用冒泡处理子元素事件,适合动态列表。
合成事件 synthetic event 框架(如 React)封装后的事件对象,接口与原生类似但池化复用。
被动监听 passive listener { passive: true } 告知浏览器不会 preventDefault,利于滚动性能。

EventTarget :实现 addEventListener / removeEventListener / dispatchEvent 的接口;ElementDocumentWindow 等均继承该能力(见 MDN EventTarget)。

0.3 案例覆盖清单

本篇示例与下列实践一一对应,全部保留并扩展为可独立运行的完整 HTML:

分类 涵盖主题
鼠标 单击/双击/右击、按下抬起、button、坐标、拖拽、mouseenter/mouseleave、滚轮兼容、无缝滚动
键盘 按下抬起、keydown/keypress 区别、keyup 实时输入、方向键控制移动
文档 loadDOMContentLoaded 对比
表单 submit/resetfocus/blurinput/change、二级联动选择
图片 load 预加载进度、error 占位
过渡/动画 transitionstart/transitionrun/transitionendanimationstart/animationend/animationiteration
其他 scroll 吸顶/懒加载思路、resize 响应式
进阶 事件流演示、target/currentTargetstopPropagationpreventDefault、事件委托、性能对比

一、事件基础回顾

1.1 什么是事件?

事件(Event) 是用户或浏览器在文档中发生的、可被 JavaScript 感知的行为或状态变化(如点击、键盘输入、资源加载完成)。

从工程角度,处理事件通常包含四步,合称 事件处理模型

  • 监听 :在 DOM 节点或 window 上注册监听器(Event Listener)
  • 触发:用户操作或浏览器状态变化产生事件对象
  • 传播:事件沿捕获 → 目标 → 冒泡路径传递
  • 响应 :回调中更新 UI、发请求或调用 preventDefault / stopPropagation

用户操作
DOM 节点
创建 Event
捕获/目标/冒泡
执行监听器
更新页面状态

【代码注释】

核心逻辑

  • 事件是浏览器与脚本之间的「消息」;没有监听函数时,默认行为仍会执行(如链接跳转)。
  • 同一元素可注册多个 addEventListener,按注册顺序在对应阶段依次执行。

注意点

  • 内联 onclick 属于 HTML 与 JS 混写,不利于 CSP 与维护,生产环境优先 DOM2 API。
  • 理解传播阶段是写好事件委托、弹层关闭的前提。

实战场景

  • 按钮点击、表单提交、图片 load/error、窗口 resize 等均依赖同一套事件模型。

1.2 事件监听的三种方式

方式一:HTML 属性方式
html 复制代码
<button onclick="handleClick()">点我</button>

<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
function handleClick() {
    console.log('按钮被点击了!'); // 输出点击日志
}
</script>

【代码注释】

核心逻辑

  • 用户点击 <button> 时执行内联 onclick,调用全局函数 handleClick
  • handleClick() 无参;若需事件对象应写 onclick="handleClick(event)"

关键 API / 概念

  • onclick:HTML 属性绑定,处理函数须在全局作用域(如 window.handleClick)。
  • 与 DOM0 / DOM2 相比,无法同一事件绑定多个独立监听器。

注意点

  • HTML 与 JS 耦合,不利于 CSP 与组件化;仅适合 Demo。
  • 函数未挂到 window 时会报 handleClick is not defined

实战场景

  • 课堂演示、极简单页;生产环境应使用 addEventListener
方式二:DOM 属性方式
html 复制代码
<button id="myBtn">点我</button>

<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
const btn = document.getElementById('myBtn'); // 获取按钮元素
btn.onclick = function() { // DOM0:为 onclick 属性赋值处理函数
    console.log('按钮被点击了!');
};
</script>

【代码注释】

核心逻辑

  • getElementById('myBtn') 获取按钮 DOM 引用。
  • btn.onclick 赋值函数;后赋值会覆盖先前的处理函数(仅保留一个)。

关键 API / 概念

  • document.getElementById(id):找不到时返回 null
  • element.onclick:DOM0 属性,类型为 Function | null,赋 null 即解绑。

注意点

  • 无法为同一事件注册多个独立处理函数;多监听器请用 addEventListener
  • 普通函数作处理器时,回调内 this 指向该元素。

实战场景

  • 简单按钮交互;解绑:btn.onclick = null
方式三:addEventListener 方式
html 复制代码
<button id="myBtn">点我</button>

<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
const btn = document.getElementById('myBtn');
btn.addEventListener('click', function(event) { // 监听 click 事件
    console.log('按钮被点击了!', event); // event 为事件对象
}, false); // false:冒泡阶段(默认)
</script>

【代码注释】

核心逻辑

  • addEventListener('click', fn, false)冒泡阶段 注册;第三参数 false 为默认值。
  • 回调收到 event 对象,可调用 preventDefaultstopPropagation 等。

关键 API / 概念

  • 同一元素可注册多个监听器,互不覆盖。
  • 解绑须传入与注册时同一函数引用removeEventListener('click', fn)

注意点

  • 箭头函数作监听器时,this 为词法作用域,不是绑定元素(与 DOM0 不同)。
  • 匿名函数无法被 removeEventListener 移除,需保存函数引用。

实战场景

  • 生产环境首选;React/Vue 底层合成事件仍建立在此 API 之上。

1.3 三种方式对比

对比项 HTML 属性 DOM 属性 addEventListener
多监听器
捕获/冒泡
解绑方式 置 null 置 null removeEventListener
代码分离 ⚠️
生产推荐 ⚠️

1.4 解除事件监听

javascript 复制代码
// ===== 完整可运行示例:复制整段到 .html 文件 =====
// DOM0 / HTML 属性:置 null
element.onclick = null;

// DOM2:须传入与注册时相同的函数引用
function handleClick(event) {
    console.log('点击');
}
element.addEventListener('click', handleClick);
element.removeEventListener('click', handleClick);

// ❌ 错误:匿名函数无法移除(引用不同)
element.addEventListener('click', function() {
    console.log('匿名');
});
element.removeEventListener('click', function() {
    console.log('匿名');
}); // 无效

【代码注释】

核心逻辑

  • DOM0:element.onclick = null 解除绑定。
  • DOM2:removeEventListener(type, listener)listener 必须与 addEventListener 时为同一引用

注意点

  • 每次 function(){} 都是新对象,匿名函数解绑无效;类组件中常在构造器 bind 保存引用。
  • 现代项目可用 AbortController + { signal } 批量解绑(见 §16.2)。

实战场景

  • 路由切换、组件卸载时清理监听,避免内存泄漏与重复触发。

1.5 事件流(Event Flow)

DOM2 规定事件传播分三阶段:捕获(Capture)→ 目标(Target)→ 冒泡(Bubble)
从 document 向下
在目标元素执行
向 document 向上
捕获阶段
目标阶段
冒泡阶段
传播结束

html 复制代码
<div id="outer">
    <div id="middle">
        <div id="inner">点我</div>
    </div>
</div>

<script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
const elements = ['outer', 'middle', 'inner'];
elements.forEach(id => {
    const el = document.getElementById(id);
    el.addEventListener('click', function(e) {
        console.log(`${id} - 阶段`, e.eventPhase);
    }, true); // true:捕获阶段

    el.addEventListener('click', function(e) {
        console.log(`${id} - 阶段`, e.eventPhase);
    }, false); // false = 冒泡阶段
});
</script>

【代码注释】

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

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

1.6 事件回调函数中的 this

javascript 复制代码
// ===== 完整可运行示例:复制整段到 .html 文件 =====
const button = document.querySelector('button');

button.onclick = function() {
    console.log(this); // 指向 button 元素
};

button.addEventListener('click', function() {
    console.log(this); // 指向 button 元素
});

// 箭头函数不绑定 this
button.addEventListener('click', () => {
    console.log(this); // 继承外层词法 this,此处通常不是 button
});

// 推荐:用 event 获取元素
button.addEventListener('click', (event) => {
    console.log(event.target); // 事件源(可能为子节点)
    console.log(event.currentTarget); // 当前绑定监听的元素
});

【代码注释】

核心逻辑

  • 普通函数作监听器时,thisevent.currentTarget 均指向绑定事件的元素。
  • 箭头函数没有自己的 this,回调内应使用 event.currentTargetevent.target

关键 API / 概念

  • event.target:最初触发事件的节点(如点击 <span> 时可能是子元素)。
  • event.currentTarget:正在执行监听器的元素(绑定 addEventListener 的节点)。

注意点

  • React 合成事件中 this 行为由框架封装,勿与原生混用。

实战场景

  • 列表项点击、表单控件内嵌图标点击时,用 target + closest() 定位业务行。

二、鼠标事件详解

名词MouseEvent 继承自 UIEventEvent,提供 clientXbuttonbuttons 等鼠标专用字段(MDN MouseEvent)。

2.1 鼠标事件类型总览

事件名 说明 典型场景
click 单击 按钮、链接、卡片点击
dblclick 双击 桌面图标、文件重命名
contextmenu 右键菜单 自定义菜单、禁用默认菜单
mousedown 鼠标按下 拖拽起点、绘图落笔
mouseup 鼠标抬起 拖拽终点、释放选中
mousemove 鼠标移动 画板轨迹、悬停提示
mouseover 鼠标进入(冒泡) 带子元素的进入检测
mouseout 鼠标离开(冒泡) 带子元素的离开检测
mouseenter 鼠标进入(不冒泡) 导航菜单、Tooltip
mouseleave 鼠标离开(不冒泡) 关闭下拉、隐藏提示
mousewheel 滚轮(非标准) Chrome/Safari/Edge 旧 API
DOMMouseScroll 滚轮(Firefox) 仅能通过 addEventListener 监听

2.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>
        .click-demo {
            width: 400px;
            height: 200px;
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 10px;
            cursor: pointer;
            user-select: none;
            transition: transform 0.1s;
        }

        .click-demo:active {
            transform: scale(0.98);
        }

        .log-panel {
            margin-top: 20px;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 5px;
            font-family: monospace;
            max-height: 200px;
            overflow-y: auto;
        }
    </style>
</head>
<body>
    <h2>单击、双击与右键菜单</h2>
    <div class="click-demo" id="clickBox">
        <p>在此区域单击、双击或右键</p>
    </div>
    <div class="log-panel" id="logPanel">
        <div>等待事件...</div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const clickBox = document.getElementById('clickBox');
            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;
            }

            // 单击
            clickBox.addEventListener('click', function(event) {
                log('单击事件触发');
                console.log('Click event:', event);
            });

            // **【代码注释】**见下方说明块
            clickBox.addEventListener('dblclick', function(event) {
                log('双击事件触发');
                console.log('Double click event:', event);
            });

            // **【代码注释】**见下方说明块
            clickBox.addEventListener('contextmenu', function(event) {
                event.preventDefault(); // 阻止默认右键菜单
                log('右键菜单事件');
                console.log('Context menu event:', event);
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  • click:在同一元素上完成「按下 + 释放」才触发;拖出元素外释放可能不触发。
  • dblclick:两次 click 间隔极短时触发;需与单击业务区分(如防抖)。
  • contextmenupreventDefault() 可阻止系统右键菜单,用于自定义面板。

注意点

  • 演示区使用 addEventListener 注册;log() 将事件写入下方日志面板。

实战场景

  • GitHub 行号右键、Figma 画布菜单、商品图「禁止另存为」拦截。

2.3 鼠标按下与抬起(button 属性)

MouseEvent.button 取值规则:

按键
0 左键
1 中键(滚轮)
2 右键
3 侧键(较少见)
4 侧键(较少见)
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>
        .mouse-button-demo {
            width: 400px;
            height: 200px;
            background: #099;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 18px;
            border-radius: 10px;
            cursor: crosshair;
        }

        .mouse-button-demo.pressed {
            background: #900;
        }

        .status-panel {
            margin-top: 20px;
            padding: 15px;
            background: #f0f0f0;
            border-radius: 5px;
        }

        .status-item {
            margin: 5px 0;
            padding: 8px;
            background: white;
            border-radius: 3px;
        }
    </style>
</head>
<body>
    <h2>事件演示</h2>
    <div class="mouse-button-demo" id="buttonBox">
        鼠标按键演示
    </div>
    <div class="status-panel">
        <div class="status-item" id="buttonStatus">等待操作...</div>
        <div class="status-item" id="buttonType">按键:-</div>
    </div>

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

            const buttonNames = {
                0: '??',
                1: '???',
                2: '??',
                3: '???',
                4: '???'
            };

            // **【代码注释】**见下方说明块
            buttonBox.addEventListener('mousedown', function(event) {
                this.classList.add('pressed');
                buttonStatus.textContent = '按钮被点击了!';
                buttonType.textContent = `按键:${buttonNames[event.button] || '未知'} (${event.button})`;
            });

            // **【代码注释】**见下方说明块
            buttonBox.addEventListener('mouseup', function(event) {
                this.classList.remove('pressed');
                buttonStatus.textContent = '按钮被点击了!';
                buttonType.textContent = `按键:${buttonNames[event.button] || '未知'} (${event.button})`;
            });
        })();
    </script>
</body>
</html>

【代码注释】

  • mousedown / mouseup 在按下/抬起瞬间触发;event.button 区分左键(0)、中键(1)、右键(2)。
  • 自定义拖拽常在 mousedown 记录起点,再在 document 上监听 mousemove/mouseup,避免指针移出元素后丢失事件。

2.4 鼠标移动与坐标

相对
相对
相对
相对
鼠标事件坐标
offsetX/offsetY
clientX/clientY
pageX/pageY
screenX/screenY
相对目标元素 padding 边
相对视口
相对文档含滚动
相对屏幕

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;
            min-height: 200vh;
        }

        .position-demo {
            width: 400px;
            height: 300px;
            margin: 50px;
            padding: 20px;
            background: linear-gradient(45deg, #ff6b6b, #feca57);
            border-radius: 10px;
            cursor: crosshair;
        }

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

        .position-info div {
            margin: 5px 0;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>
    <p>在此区域单击、双击或右键</p>

    <div class="position-demo" id="demoArea">
        在此区域内移动鼠标查看坐标
    </div>

    <div class="position-info">
        <div>offsetX: <span id="offsetX">0</span></div>
        <div>offsetY: <span id="offsetY">0</span></div>
        <div>clientX: <span id="clientX">0</span></div>
        <div>clientY: <span id="clientY">0</span></div>
        <div>pageX: <span id="pageX">0</span></div>
        <div>pageY: <span id="pageY">0</span></div>
        <div>screenX: <span id="screenX">0</span></div>
        <div>screenY: <span id="screenY">0</span></div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const demoArea = document.getElementById('demoArea');
            const elements = {
                offsetX: document.getElementById('offsetX'),
                offsetY: document.getElementById('offsetY'),
                clientX: document.getElementById('clientX'),
                clientY: document.getElementById('clientY'),
                pageX: document.getElementById('pageX'),
                pageY: document.getElementById('pageY'),
                screenX: document.getElementById('screenX'),
                screenY: document.getElementById('screenY')
            };

            demoArea.addEventListener('mousemove', function(event) {
                // **【代码注释】**见下方说明块
                Object.keys(elements).forEach(key => {
                    elements[key].textContent = event[key];
                });
            });
        })();
    </script>
</body>
</html>

【代码注释】

  • mousemove 高频触发,适合做画板轨迹、悬停高亮;演示中用 event.offsetX/Y 相对盒子绘制。
  • offset* 相对事件目标;client* 相对视口;page* 含页面滚动;screen* 相对物理屏幕。

核心逻辑

属性 参照系 典型用途
offsetX/Y 相对目标元素 padding 边 画板、局部热区
clientX/Y 视口 拖拽定位,配合 getBoundingClientRect
pageX/Y 文档 长页面滚动场景
screenX/Y 屏幕 多显示器场景

实战场景:画板涂鸦、拖拽定位、地图标注、多屏协作光标。

2.5 鼠标拖拽

坐标系选择直接影响拖拽与画板实现是否「跟手」。

  • 说明 :列表/画布内优先 offset*client* + getBoundingClientRect
  • 说明 :全页滚动、无限列表用 page*client* + scrollX/Y
  • UI 层 :固定层、弹窗内拖拽多用 client*
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>
        .drag-container {
            width: 100%;
            height: 100vh;
            background: #f0f0f0;
            position: relative;
            overflow: hidden;
        }

        .draggable-box {
            position: absolute;
            left: 100px;
            top: 60px;
            width: 150px;
            height: 150px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 10px;
            cursor: move;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 16px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
            user-select: none;
            transition: box-shadow 0.2s, background 0.2s;
        }

        .draggable-box:active {
            background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
            box-shadow: 0 8px 25px rgba(0,0,0,0.3);
        }

        .position-display {
            position: fixed;
            bottom: 20px;
            left: 20px;
            padding: 15px;
            background: rgba(0,0,0,0.8);
            color: white;
            border-radius: 5px;
            font-family: monospace;
        }
    </style>
</head>
<body>
    <div class="drag-container">
        <div class="draggable-box" id="dragBox">
            ???
        </div>
        <div class="position-display" id="positionDisplay">
            X: 100px, Y: 60px
        </div>
    </div>

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

            let isDragging = false;
            let startX = 0;  // 【见下方代码注释】
            let startY = 0;  // 【见下方代码注释】

            // **【代码注释】**见下方说明块
            dragBox.addEventListener('mousedown', function(event) {
                isDragging = true;

                // **【代码注释】**见下方说明块
                startX = event.offsetX;
                startY = event.offsetY;
            });

            // 【见下方代码注释】
            document.addEventListener('mousemove', function(event) {
                if (!isDragging) return;

                // **【代码注释】**见下方说明块
                let left = event.clientX - startX;
                let top = event.clientY - startY;

                // **【代码注释】**见下方说明块
                const maxX = window.innerWidth - dragBox.offsetWidth;
                const maxY = window.innerHeight - dragBox.offsetHeight;

                left = Math.max(0, Math.min(left, maxX));
                top = Math.max(0, Math.min(top, maxY));

                // **【代码注释】**见下方说明块
                dragBox.style.left = left + 'px';
                dragBox.style.top = top + 'px';

                // **【代码注释】**见下方说明块
                positionDisplay.textContent = `X: ${Math.round(left)}px, Y: ${Math.round(top)}px`;
            });

            // **【代码注释】**见下方说明块
            document.addEventListener('mouseup', function() {
                isDragging = false;
            });
        })();
    </script>
</body>
</html>

【代码注释】

  1. mousedown 记录 offsetX/offsetY 或元素矩形,作为拖拽起点。
  2. mousemove 挂在 document 上更新幽灵节点位置,松开时在 mouseup 里解绑。
  3. clientX/clientY 减容器 getBoundingClientRect() 得到 left/top,避免滚动误差。
  4. 移动端与无障碍场景优先 HTML5 Drag and Drop APIPointer Events 统一指针模型。

核心逻辑

2.6 mouseenter/mouseleave vs mouseover/mouseout

说明mouseenter/mouseleave 不冒泡 ,进入子元素不会在父级重复触发;mouseover/mouseout 会冒泡,适合需要感知子节点进出的场景。
进入子元素
进入子元素
mouseover 会冒泡
父元素再次触发
mouseenter 不冒泡
父元素不重复触发

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>
        .compare-container {
            display: flex;
            gap: 50px;
            margin: 50px;
        }

        .box-pair {
            flex: 1;
        }

        .outer-box {
            width: 300px;
            height: 200px;
            padding: 20px;
            background: #e0e0e0;
            border-radius: 10px;
        }

        .inner-box {
            width: 150px;
            height: 100px;
            background: #667eea;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 5px;
        }

        .log-area {
            margin-top: 20px;
            padding: 10px;
            background: #f5f5f5;
            height: 150px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 12px;
        }
    </style>
</head>
<body>
    <h1>mouseenter/mouseleave vs mouseover/mouseout</h1>

    <div class="compare-container">
        <div class="box-pair">
            <h3>mouseover/mouseout(会冒泡)</h3>
            <div class="outer-box" id="outer1">
                <div class="inner-box">点我</div>
            </div>
            <div class="log-area" id="log1">
                <div>等待操作...</div>
            </div>
        </div>

        <div class="box-pair">
            <h3>mouseenter/mouseleave(不冒泡)</h3>
            <div class="outer-box" id="outer2">
                <div class="inner-box">点我</div>
            </div>
            <div class="log-area" id="log2">
                <div>等待操作...</div>
            </div>
        </div>
    </div>

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

            function log(panel, message) {
                const div = document.createElement('div');
                div.textContent = message;
                panel.appendChild(div);
                panel.scrollTop = panel.scrollHeight;
            }

            // mouseover/mouseout
            outer1.addEventListener('mouseover', function(e) {
                log(log1, `mouseover: ${e.target.className || e.target.tagName}`);
            });
            outer1.addEventListener('mouseout', function(e) {
                log(log1, `mouseout: ${e.target.className || e.target.tagName}`);
            });

            // mouseenter/mouseleave
            outer2.addEventListener('mouseenter', function(e) {
                log(log2, `mouseenter: ${e.target.className || e.target.tagName}`);
            });
            outer2.addEventListener('mouseleave', function(e) {
                log(log2, `mouseleave: ${e.target.className || e.target.tagName}`);
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

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

实战场景

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

2.7 滚轮事件兼容处理

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

| Chrome/Safari/Edge | mousewheel | wheelDelta | ??(120) | ??(-120) |

| Firefox | DOMMouseScroll | detail | ??(-3) | ??(3) |

| 标准事件 | wheel | deltaY | 向下为正 | 推荐 |

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>
        .wheel-demo {
            width: 400px;
            height: 300px;
            background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 24px;
            border-radius: 10px;
            transition: transform 0.3s;
        }

        .wheel-info {
            margin-top: 20px;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 5px;
            font-family: monospace;
        }

        .wheel-info div {
            margin: 5px 0;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>
    <p>请按键盘或点击操作</p>

    <div class="wheel-demo" id="wheelBox">
        滚轮演示区
    </div>

    <div class="wheel-info">
        <div>方向:<span id="direction">-</span></div>
        <div>delta:<span id="deltaValue">0</span></div>
        <div>浏览器:<span id="browserInfo">-</span></div>
    </div>

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

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

            // **【代码注释】**见下方说明块
            const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
            browserInfo.textContent = isFirefox ? 'Firefox' : 'Chrome/Safari/Edge';

            let scale = 1;

            // **【代码注释】**见下方说明块
            function handleWheel(event) {
                let delta = 0;
                let dir = '';

                // **【代码注释】**见下方说明块
                if (event.wheelDelta) {
                    // Chrome, Safari, Edge, IE
                    delta = event.wheelDelta;
                    dir = event.wheelDelta > 0 ? '??' : '??';
                } else if (event.detail) {
                    // Firefox
                    delta = -event.detail * 40; // 【见下方代码注释】
                    dir = event.detail < 0 ? '??' : '??';
                }

                // **【代码注释】**见下方说明块
                direction.textContent = dir;
                deltaValue.textContent = delta;

                // **【代码注释】**见下方说明块
                scale += dir === '??' ? 0.1 : -0.1;
                scale = Math.max(0.5, Math.min(scale, 2));
                wheelBox.style.transform = `scale(${scale})`;

                console.log('滚轮事件:', {
                    direction: dir,
                    delta: delta,
                    wheelDelta: event.wheelDelta,
                    detail: event.detail
                });
            }

            // Chrome, Safari, Edge
            window.addEventListener('mousewheel', handleWheel);

            // Firefox
            window.addEventListener('DOMMouseScroll', handleWheel);

            // **【代码注释】**见下方说明块
            wheelBox.addEventListener('wheel', function(event) {
                // 【见下方代码注释】
                const deltaY = event.deltaY;
                const dir = deltaY < 0 ? '??' : '??';
                console.log('?? wheel ??:', { deltaY, direction: dir });
            });
        })();
    </script>
</body>
</html>

【代码注释】

  • 历史:mousewheel/wheelDelta;Firefox 旧版 DOMMouseScroll/detail;现代统一 wheel
  • 说明 :读 event.deltaY;阻止滚动需 { passive: false }preventDefault()
  • 地图缩放、横向滚动、无限列表加载更多。

2.8 无缝滚动案例

说明

  • 演示区监听 wheel 并输出 deltaY、方向与浏览器信息。
  • 旧 API 与 wheel 对照,便于维护遗留代码。
  • 生产环境统一使用标准 wheel 事件。
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>
        .scroll-container {
            width: 100%;
            max-width: 800px;
            margin: 50px auto;
            overflow: hidden;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .scroll-wrapper {
            display: flex;
            overflow: hidden;
        }

        .scroll-item {
            flex: 0 0 auto;
            width: 200px;
            height: 200px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24px;
            color: white;
            font-weight: bold;
        }

        .scroll-item:nth-child(odd) {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        }

        .scroll-item:nth-child(even) {
            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
        }

        .control-panel {
            max-width: 800px;
            margin: 20px auto;
            text-align: center;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 10px;
        }

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

        .btn:hover {
            background: #764ba2;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>

    <div class="scroll-container">
        <div class="scroll-wrapper" id="scrollWrapper">
            <div class="scroll-item">1</div>
            <div class="scroll-item">2</div>
            <div class="scroll-item">3</div>
            <div class="scroll-item">4</div>
            <div class="scroll-item">5</div>
            <div class="scroll-item">6</div>
            <div class="scroll-item">7</div>
            <div class="scroll-item">8</div>
            <div class="scroll-item">9</div>
            <div class="scroll-item">10</div>
        </div>
    </div>

    <div class="control-panel">
        <p>请按键盘或点击操作</p>
        <button class="btn" id="pauseBtn">暂停</button>
        <button class="btn" id="resumeBtn">继续</button>
        <button class="btn" id="speedUpBtn">加速</button>
        <button class="btn" id="slowDownBtn">减速</button>
    </div>

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

            let scrollSpeed = 2; // 【见下方代码注释】
            let isPaused = false;
            let animationId = null;

            // **【代码注释】**见下方说明块
            const itemWidth = 200;
            const itemCount = scrollWrapper.children.length;

            // **【代码注释】**见下方说明块
            scrollWrapper.innerHTML += scrollWrapper.innerHTML;

            // **【代码注释】**见下方说明块
            function scroll() {
                if (!isPaused) {
                    scrollContainer.scrollLeft += scrollSpeed;

                    // **【代码注释】**见下方说明块
                    if (scrollContainer.scrollLeft >= itemWidth * itemCount) {
                        scrollContainer.scrollLeft = 0;
                    }
                }
                animationId = requestAnimationFrame(scroll);
            }

            // **【代码注释】**见下方说明块
            animationId = requestAnimationFrame(scroll);

            // **【代码注释】**见下方说明块
            scrollWrapper.addEventListener('mouseenter', function() {
                isPaused = true;
            });

            // **【代码注释】**见下方说明块
            scrollWrapper.addEventListener('mouseleave', function() {
                isPaused = false;
            });

            // **【代码注释】**见下方说明块
            document.getElementById('pauseBtn').addEventListener('click', function() {
                isPaused = true;
            });

            document.getElementById('resumeBtn').addEventListener('click', function() {
                isPaused = false;
            });

            document.getElementById('speedUpBtn').addEventListener('click', function() {
                scrollSpeed = Math.min(scrollSpeed + 0.5, 10);
            });

            document.getElementById('slowDownBtn').addEventListener('click', function() {
                scrollSpeed = Math.max(scrollSpeed - 0.5, 0.5);
            });
        })();
    </script>
</body>
</html>

【代码注释】

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

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

【本章小结】鼠标事件

要点 记忆
点击族 click / dblclick / contextmenu
过程族 mousedownmousemovemouseup
进入离开 菜单用 mouseenter/mouseleave,需冒泡用 mouseover/mouseout
坐标 元素内 offset*,视口 client*,文档 page*
滚轮 新标准 wheel + deltaY;历史需兼容 mousewheel / DOMMouseScroll

三、键盘事件详解

名词KeyboardEvent 提供 keycoderepeat 等;key 表逻辑键位,code 表物理键位(MDN KeyboardEvent)。keypress 已废弃,请使用 keydown + event.key

3.1 键盘事件类型总览

事件名 说明 备注
keydown 按键按下 所有键均可触发,推荐统一使用
keyup 按键抬起 常与 keydown 配对
keypress 按键按下(已废弃) 仅部分可打印字符,请改用 keydown

提示keypress 已不推荐,请使用 keydown 配合 event.key 判断。
按键按下
keydown 触发
keyCode/key/code
业务处理快捷键
按键抬起
keyup 触发
keyCode/key/code

3.2 键盘事件常用属性

属性 说明 示例
keyCode 已废弃,ASCII 码 65(A)
which 同 keyCode,已废弃 65
key 逻辑键名 'a', 'Enter', 'ArrowUp'
code 物理键位 'KeyA', 'Enter', 'ArrowUp'
ctrlKey 是否按下 Ctrl true/false
shiftKey 是否按下 Shift true/false
altKey 是否按下 Alt true/false
metaKey Win / Cmd 键 true/false

3.3 keydown 与 keypress 区别

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>
        .keyboard-demo {
            width: 600px;
            margin: 50px auto;
            padding: 30px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 15px;
            color: white;
        }

        .input-box {
            width: 100%;
            padding: 15px;
            font-size: 16px;
            border: none;
            border-radius: 5px;
            margin-bottom: 15px;
        }

        .event-log {
            background: rgba(255,255,255,0.1);
            padding: 15px;
            border-radius: 5px;
            height: 200px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 12px;
        }

        .event-log div {
            margin: 5px 0;
            padding: 5px;
            background: rgba(0,0,0,0.2);
            border-radius: 3px;
        }

        .keydown { color: #ffd700; }
        .keypress { color: #90ee90; }
        .keyup { color: #87ceeb; }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>

    <div class="keyboard-demo">
        <h3>键盘事件对比演示(keydown / keypress / keyup)</h3>
        <input type="text" class="input-box" id="inputBox" placeholder="在此输入测试...">
        <p>请按键盘或点击操作</p>

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

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

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

            // keydown ??
            inputBox.addEventListener('keydown', function(event) {
                log(`keydown - key: ${event.key}, keyCode: ${event.keyCode}, code: ${event.code}`, 'keydown');
            });

            // keypress 已废弃,仅作对比
            inputBox.addEventListener('keypress', function(event) {
                log(`keypress - key: ${event.key}, keyCode: ${event.keyCode}, charCode: ${event.charCode}`, 'keypress');
            });

            // keyup ??
            inputBox.addEventListener('keyup', function(event) {
                log(`keyup - key: ${event.key}, keyCode: ${event.keyCode}, code: ${event.code}`, 'keyup');
            });

            // 【见下方代码注释】
            document.addEventListener('keydown', function(event) {
                if (document.activeElement !== inputBox) {
                    console.log('按键按下:', event.key, event.code);
                }
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

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

实战场景

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

3.4 实时获取输入框内容

说明

  • keydown/keyup 可识别功能键与组合键;keypress 已废弃。
  • 使用 event.key 判断字符,勿依赖 keyCode
  • 输入框内演示三种事件的触发顺序差异。
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>
        .input-demo {
            width: 600px;
            margin: 50px auto;
            padding: 30px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .form-group {
            margin-bottom: 20px;
        }

        .form-group label {
            display: block;
            margin-bottom: 8px;
            font-weight: bold;
            color: #333;
        }

        .form-group input {
            width: 100%;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 5px;
            font-size: 16px;
            transition: border-color 0.3s;
        }

        .form-group input:focus {
            outline: none;
            border-color: #667eea;
        }

        .card-display {
            margin-top: 15px;
            padding: 15px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 10px;
            font-family: monospace;
            font-size: 20px;
            letter-spacing: 2px;
            min-height: 50px;
        }

        .password-strength {
            margin-top: 10px;
        }

        .strength-bar {
            height: 5px;
            background: #e0e0e0;
            border-radius: 3px;
            overflow: hidden;
        }

        .strength-fill {
            height: 100%;
            width: 0%;
            transition: width 0.3s, background 0.3s;
        }

        .strength-text {
            margin-top: 5px;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>

    <div class="input-demo">
        <div class="form-group">
            <label>密码强度(至少 4 个字符)</label>
            <input type="text" id="bankCard" maxlength="19" placeholder="请输入...">
            <div class="card-display" id="cardDisplay">#### #### #### ####</div>
        </div>

        <div class="form-group">
            <label>城市</label>
            <input type="password" id="password" placeholder="请输入...">
            <div class="password-strength">
                <div class="strength-bar">
                    <div class="strength-fill" id="strengthFill"></div>
                </div>
                <div class="strength-text" id="strengthText">等待操作...</div>
            </div>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            // **【代码注释】**见下方说明块
            const bankCardInput = document.getElementById('bankCard');
            const cardDisplay = document.getElementById('cardDisplay');

            bankCardInput.addEventListener('keyup', function() {
                let value = this.value.replace(/\D/g, ''); // 【见下方代码注释】

                // 至少 4 个字符
                let formattedValue = value.replace(/(\d{4})(?=\d)/g, '$1 ');

                this.value = formattedValue;
                cardDisplay.textContent = formattedValue || '#### #### #### ####';
            });

            // **【代码注释】**见下方说明块
            const passwordInput = document.getElementById('password');
            const strengthFill = document.getElementById('strengthFill');
            const strengthText = document.getElementById('strengthText');

            passwordInput.addEventListener('input', function() {
                const password = this.value;
                let strength = 0;
                let color = '#e0e0e0';
                let text = '???';

                if (password) {
                    // **【代码注释】**见下方说明块
                    if (password.length >= 6) strength++;
                    if (password.length >= 10) strength++;

                    // **【代码注释】**见下方说明块
                    if (/[a-z]/.test(password)) strength++;

                    // **【代码注释】**见下方说明块
                    if (/[A-Z]/.test(password)) strength++;

                    // **【代码注释】**见下方说明块
                    if (/\d/.test(password)) strength++;

                    // **【代码注释】**见下方说明块
                    if (/[^a-zA-Z0-9]/.test(password)) strength++;

                    // **【代码注释】**见下方说明块
                    switch (Math.min(strength, 5)) {
                        case 1:
                            color = '#ff4444';
                            text = '?';
                            break;
                        case 2:
                            color = '#ff8800';
                            text = '??';
                            break;
                        case 3:
                            color = '#ffcc00';
                            text = '??';
                            break;
                        case 4:
                            color = '#88cc00';
                            text = '??';
                            break;
                        case 5:
                            color = '#00cc44';
                            text = '?';
                            break;
                    }
                }

                strengthFill.style.width = `${(Math.min(strength, 5) / 5) * 100}%`;
                strengthFill.style.background = color;
                strengthText.textContent = `强度:${text}`;
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

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

实战场景

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

3.5 方向键控制元素移动

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>
        .game-container {
            width: 800px;
            height: 600px;
            margin: 50px auto;
            position: relative;
            background: linear-gradient(to bottom, #87CEEB 0%, #E0F7FA 100%);
            border: 3px solid #333;
            border-radius: 10px;
            overflow: hidden;
        }

        .player {
            position: absolute;
            width: 50px;
            height: 50px;
            background: #ff6b6b;
            border-radius: 50%;
            left: 375px;
            top: 275px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            transition: transform 0.1s;
        }

        .player::before {
            content: '';
            position: absolute;
            width: 20px;
            height: 20px;
            background: rgba(255,255,255,0.8);
            border-radius: 50%;
            top: 10px;
            left: 10px;
        }

        .player::after {
            content: '';
            position: absolute;
            width: 20px;
            height: 20px;
            background: rgba(255,255,255,0.8);
            border-radius: 50%;
            top: 10px;
            right: 10px;
        }

        .info-panel {
            position: absolute;
            top: 10px;
            left: 10px;
            background: rgba(0,0,0,0.7);
            color: white;
            padding: 10px 15px;
            border-radius: 5px;
            font-family: monospace;
        }

        .instructions {
            width: 800px;
            margin: 20px auto;
            text-align: center;
            background: #f5f5f5;
            padding: 15px;
            border-radius: 10px;
        }

        .key-hint {
            display: inline-block;
            padding: 5px 10px;
            background: #333;
            color: white;
            border-radius: 3px;
            margin: 0 3px;
            font-family: monospace;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>

    <div class="game-container" id="gameContainer">
        <div class="player" id="player">等待操作...</div>
        <div class="info-panel">
            <div>??: X:<span id="posX">375</span> Y:<span id="posY">275</span></div>
            <div>??: <span id="speed">10</span> px</div>
        </div>
    </div>

    <div class="instructions">
        <p>按 <span class="key-hint">↑</span> <span class="key-hint">↓</span> <span class="key-hint">←</span> <span class="key-hint">→</span> 或 <span class="key-hint">W</span> <span class="key-hint">A</span> <span class="key-hint">S</span> <span class="key-hint">D</span> 移动</p>
        <p>按 <span class="key-hint">Shift</span> 加速 | 按 <span class="key-hint">Ctrl</span> 减速</p>
        <p>请按键盘或点击操作</p>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const player = document.getElementById('player');
            const gameContainer = document.getElementById('gameContainer');
            const posXDisplay = document.getElementById('posX');
            const posYDisplay = document.getElementById('posY');
            const speedDisplay = document.getElementById('speed');

            let x = 375;
            let y = 275;
            let baseSpeed = 10;
            let currentSpeed = 10;
            const playerSize = 50;
            const containerWidth = 800;
            const containerHeight = 600;

            // **【代码注释】**见下方说明块
            const keys = {
                up: false,
                down: false,
                left: false,
                right: false,
                shift: false,
                ctrl: false
            };

            // **【代码注释】**见下方说明块
            function updateDisplay() {
                posXDisplay.textContent = Math.round(x);
                posYDisplay.textContent = Math.round(y);
                speedDisplay.textContent = currentSpeed;

                player.style.left = x + 'px';
                player.style.top = y + 'px';
            }

            // **【代码注释】**见下方说明块
            function gameLoop() {
                // **【代码注释】**见下方说明块
                currentSpeed = keys.shift ? baseSpeed * 2 : (keys.ctrl ? baseSpeed / 2 : baseSpeed);

                // **【代码注释】**见下方说明块
                if (keys.up) y = Math.max(0, y - currentSpeed);
                if (keys.down) y = Math.min(containerHeight - playerSize, y + currentSpeed);
                if (keys.left) x = Math.max(0, x - currentSpeed);
                if (keys.right) x = Math.min(containerWidth - playerSize, x + currentSpeed);

                updateDisplay();
                requestAnimationFrame(gameLoop);
            }

            // **【代码注释】**见下方说明块
            document.addEventListener('keydown', function(event) {
                // **【代码注释】**见下方说明块
                if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(event.key)) {
                    event.preventDefault();
                }

                switch(event.key) {
                    case 'ArrowUp':
                    case 'w':
                    case 'W':
                        keys.up = true;
                        break;
                    case 'ArrowDown':
                    case 's':
                    case 'S':
                        keys.down = true;
                        break;
                    case 'ArrowLeft':
                    case 'a':
                    case 'A':
                        keys.left = true;
                        break;
                    case 'ArrowRight':
                    case 'd':
                    case 'D':
                        keys.right = true;
                        break;
                    case 'Shift':
                        keys.shift = true;
                        break;
                    case 'Control':
                        keys.ctrl = true;
                        break;
                }
            });

            document.addEventListener('keyup', function(event) {
                switch(event.key) {
                    case 'ArrowUp':
                    case 'w':
                    case 'W':
                        keys.up = false;
                        break;
                    case 'ArrowDown':
                    case 's':
                    case 'S':
                        keys.down = false;
                        break;
                    case 'ArrowLeft':
                    case 'a':
                    case 'A':
                        keys.left = false;
                        break;
                    case 'ArrowRight':
                    case 'd':
                    case 'D':
                        keys.right = false;
                        break;
                    case 'Shift':
                        keys.shift = false;
                        break;
                    case 'Control':
                        keys.ctrl = false;
                        break;
                }
            });

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

【代码注释】

  • document 监听 keydown,用 event.key(如 ArrowUp)更新方块位置。
  • 对方向键调用 event.preventDefault(),避免页面滚动。

3.6 哪些元素可监听键盘事件

全局快捷键与输入框焦点冲突是常见坑,需区分监听目标。

  1. 表单控件 :焦点在 <input>, <textarea>, <select>, <button> 时,通常不触发全局快捷键。
  2. document 监听:适合游戏画布、无输入框的全屏应用。
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>
        .demo-section {
            width: 600px;
            margin: 20px auto;
            padding: 20px;
            background: #f5f5f5;
            border-radius: 10px;
        }

        .demo-section input,
        .demo-section textarea,
        .demo-section button {
            display: block;
            width: 100%;
            margin-bottom: 15px;
            padding: 10px;
            border: 2px solid #e0e0e0;
            border-radius: 5px;
            font-size: 16px;
        }

        .demo-section button {
            background: #667eea;
            color: white;
            border: none;
            cursor: pointer;
            transition: background 0.3s;
        }

        .demo-section button:hover {
            background: #764ba2;
        }

        .log-area {
            width: 600px;
            margin: 20px auto;
            padding: 15px;
            background: #2d2d2d;
            color: #00ff00;
            border-radius: 10px;
            font-family: monospace;
            height: 200px;
            overflow-y: auto;
        }

        .log-item {
            margin: 5px 0;
            padding: 5px;
            border-left: 3px solid #667eea;
            padding-left: 10px;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>
    <p style="text-align: center;">请按键盘或点击操作</p>

    <div class="demo-section">
        <h3>document 级快捷键演示</h3>
        <input type="text" placeholder="请输入..." id="input1">
        <textarea placeholder="请输入..." id="textarea1"></textarea>
        <button id="button1">点击下方按钮后按 Enter 测试</button>
    </div>

    <div class="demo-section">
        <h3>可聚焦 div(需 tabindex)</h3>
        <div id="div1" style="padding: 20px; background: white; border: 2px dashed #ccc; cursor: pointer;" tabindex="0">
            div 默认不可聚焦,设置 tabindex 后可接收键盘事件
        </div>
    </div>

    <div class="log-area" id="logArea">
        <div class="log-item">提示:在页面空白处按键可触发 document 级监听</div>
    </div>

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

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

            // **【代码注释】**见下方说明块
            const elements = {
                input1: '???',
                textarea1: '???',
                button1: '??',
                div1: 'div??'
            };

            Object.keys(elements).forEach(id => {
                const el = document.getElementById(id);
                el.addEventListener('keydown', function(event) {
                    log(`${elements[id]} ?? keydown: ${event.key}`);
                });
            });

            // **【代码注释】**见下方说明块
            document.addEventListener('keydown', function(event) {
                const activeElement = document.activeElement;
                const elementName = activeElement.id ?
                    elements[activeElement.id] || activeElement.tagName :
                    activeElement.tagName;

                log(`document 级 keydown: ${event.key},焦点元素: ${elementName}`);
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

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

实战场景

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

【本章小结】键盘事件

  • 优先使用 keydown / keyup ,配合 event.key 判断按键;勿再依赖 keypresskeyCode
  • 全局快捷键监听 document,注意与输入框焦点冲突(event.target 是否为 INPUT)。
  • 游戏/画布移动:在 keydown 中改坐标,用 preventDefault() 避免方向键滚动页面。

四、文档事件详解

名词DOMContentLoaded 在 HTML 解析完成后触发,不等待图片与样式;load 在全部资源加载后触发(MDN DOMContentLoaded)。

4.1 load 事件与 DOMContentLoaded 事件对比



HTML 开始解析
构建 DOM 树
DOMContentLoaded 触发
并行加载资源
还有资源未完成?
load 触发

特性 load DOMContentLoaded
触发对象 window/body window/document
是否等待图片/CSS

4.2 DOMContentLoaded 实战演示

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>
        .loading-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);
        }

        .event-timeline {
            margin-top: 30px;
            padding: 20px;
            background: #f5f5f5;
            border-radius: 10px;
        }

        .timeline-item {
            padding: 15px;
            margin-bottom: 10px;
            background: white;
            border-radius: 5px;
            border-left: 4px solid #667eea;
            font-family: monospace;
        }

        .timeline-item .time {
            color: #666;
            font-size: 12px;
        }

        .timeline-item .event-name {
            font-size: 16px;
            font-weight: bold;
            color: #333;
            margin-top: 5px;
        }

        .timeline-item .delta {
            color: #764ba2;
            font-size: 14px;
            margin-top: 5px;
        }

        .image-grid {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 10px;
            margin-top: 20px;
        }

        .image-grid img {
            width: 100%;
            height: 100px;
            object-fit: cover;
            border-radius: 5px;
            background: #f0f0f0;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>

    <div class="loading-demo">
        <h2>事件演示</h2>
        <div class="image-grid" id="imageGrid">
            <!-- 阻塞脚本会延迟 DOMContentLoaded -->
        </div>

        <div class="event-timeline">
            <h3>事件时间线</h3>
            <div id="timeline">
                <div class="timeline-item">
                    <div class="time">-</div>
                    <div class="event-name">等待触发...</div>
                </div>
            </div>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const timeline = document.getElementById('timeline');
            const imageGrid = document.getElementById('imageGrid');
            const startTime = performance.now();
            let domReadyTime = 0;

            function addTimelineItem(eventName, color) {
                const currentTime = performance.now();
                const delta = currentTime - startTime;
                const item = document.createElement('div');
                item.className = 'timeline-item';
                item.style.borderLeftColor = color;
                item.innerHTML = `
                    <div class="time">${new Date().toLocaleTimeString()}</div>
                    <div class="event-name" style="color: ${color}">${eventName}</div>
                    <div class="delta">距上一事件: ${delta.toFixed(2)}ms</div>
                `;
                timeline.appendChild(item);
            }

            // DOMContentLoaded ??
            document.addEventListener('DOMContentLoaded', function() {
                domReadyTime = performance.now();
                addTimelineItem('DOMContentLoaded ??', '#28a745');
                console.log('DOMContentLoaded:', domReadyTime - startTime);

                // 【见下方代码注释】
                loadImages();
            });

            // load ??
            window.addEventListener('load', function() {
                const loadTime = performance.now();
                addTimelineItem('load ??', '#dc3545');
                console.log('load:', loadTime - startTime);
                console.log('???:', loadTime - domReadyTime, 'ms');
            });

            // **【代码注释】**见下方说明块
            function loadImages() {
                const imageUrls = [
                    'images/db01.svg', 'images/db02.svg', 'images/db03.svg',
                    'images/db04.svg', 'images/db05.svg', 'images/db06.svg',
                    'images/db07.svg', 'images/db08.svg'
                ];

                imageUrls.forEach(url => {
                    const img = document.createElement('img');
                    img.src = url;
                    img.alt = '示例图片';
                    imageGrid.appendChild(img);
                });
            }
        })();
    </script>
</body>
</html>

【代码注释】

  • DOMContentLoaded 通常早于 load 触发,适合「DOM 就绪即可运行」的脚本。
  • DOMContentLoaded 时 DOM 已可查询与绑定事件;load 需等待图片、iframe、样式表等全部加载完成。
  • 动态插入带 srcimg 会单独触发该元素的 loadwindowload 表示页面级资源已全部就绪。

4.3 何时使用哪个事件

适合 DOMContentLoaded 的场景

  • 初始化菜单、Tab、轮播等 DOM 结构
  • 绑定事件委托、请求首屏接口(不依赖图片尺寸)
  • 统计首屏可交互时间(TTI 相关指标)

适合 load 的场景

  • 需要读取图片 naturalWidth / 画布尺寸
  • 全屏背景图、地图瓦片加载完成后再渲染
  • 旧式「等所有资源再显示页面」的 loading 遮罩

【本章小结】文档事件

  • DOMContentLoaded:DOM 可操作时尽早执行脚本(推荐作为业务入口)。
  • load:全部资源就绪后再做依赖尺寸/图片的逻辑。
  • 性能优化:关键脚本放底部或使用 defer,避免阻塞解析。

五、表单事件详解

名词 :表单相关事件多属于 EventInputEventinput 在每次值变化时触发,change 在「提交型」控件上于失焦且值变化时触发(MDN input 事件)。

5.1 表单事件类型总览

事件名 说明 典型场景
submit 表单提交 form
reset 表单重置 form
focus 获得焦点 表单控件
blur 失去焦点 表单控件
input 内容实时变化 input/textarea
change 值改变且失焦(或 select 立即) input/select/textarea
select 文本被选中 input/textarea

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

        .form-group {
            margin-bottom: 20px;
        }

        .form-group label {
            display: block;
            margin-bottom: 8px;
            font-weight: bold;
            color: #333;
        }

        .form-group input,
        .form-group textarea {
            width: 100%;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 5px;
            font-size: 16px;
            box-sizing: border-box;
            transition: border-color 0.3s;
        }

        .form-group input:focus,
        .form-group textarea:focus {
            outline: none;
            border-color: #667eea;
        }

        .form-group textarea {
            resize: vertical;
            min-height: 100px;
        }

        .form-group .error {
            color: #ff4444;
            font-size: 14px;
            margin-top: 5px;
            display: none;
        }

        .form-group.error input {
            border-color: #ff4444;
        }

        .form-group.error .error {
            display: block;
        }

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

        .btn {
            flex: 1;
            padding: 12px;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            transition: all 0.3s;
        }

        .btn-submit {
            background: #667eea;
            color: white;
        }

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

        .btn-reset {
            background: #e0e0e0;
            color: #333;
        }

        .btn-reset:hover {
            background: #d0d0d0;
        }

        .form-data {
            margin-top: 20px;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 5px;
            display: none;
        }

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

    <div class="form-container">
        <form id="myForm">
            <div class="form-group" id="emailGroup">
                <label>城市</label>
                <input type="email" id="email" name="email" placeholder="请输入..." required>
                <div class="error">等待操作...</div>
            </div>

            <div class="form-group" id="passwordGroup">
                <label>城市</label>
                <input type="password" id="password" name="password" placeholder="请输入..." required>
                <div class="error">密码至少 6 位</div>
            </div>

            <div class="form-group">
                <label>城市</label>
                <textarea id="message" name="message" placeholder="请输入..."></textarea>
            </div>

            <div class="button-group">
                <button type="submit" class="btn btn-submit">??</button>
                <button type="reset" class="btn btn-reset">??</button>
            </div>
        </form>

        <div class="form-data" id="formData">
            <h3>input:实时触发</h3>
            <pre id="formContent"></pre>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const form = document.getElementById('myForm');
            const formData = document.getElementById('formData');
            const formContent = document.getElementById('formContent');
            const emailGroup = document.getElementById('emailGroup');
            const passwordGroup = document.getElementById('passwordGroup');

            // **【代码注释】**见下方说明块
            form.addEventListener('submit', function(event) {
                event.preventDefault(); // 阻止默认右键菜单

                // **【代码注释】**见下方说明块
                const formDataObj = new FormData(form);
                const data = Object.fromEntries(formDataObj.entries());

                // **【代码注释】**见下方说明块
                let isValid = true;

                // **【代码注释】**见下方说明块
                const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                if (!emailRegex.test(data.email)) {
                    emailGroup.classList.add('error');
                    isValid = false;
                } else {
                    emailGroup.classList.remove('error');
                }

                // **【代码注释】**见下方说明块
                if (data.password.length < 6) {
                    passwordGroup.classList.add('error');
                    isValid = false;
                } else {
                    passwordGroup.classList.remove('error');
                }

                if (isValid) {
                    // **【代码注释】**见下方说明块
                    formContent.textContent = JSON.stringify(data, null, 2);
                    formData.classList.add('show');
                    console.log('表单提交')??:', data);
                }
            });

            // **【代码注释】**见下方说明块
            form.addEventListener('reset', function(event) {
                // setTimeout 模拟异步校验
                setTimeout(function() {
                    formData.classList.remove('show');
                    emailGroup.classList.remove('error');
                    passwordGroup.classList.remove('error');
                    console.log('表单提交')?');
                }, 0);
            });

            // **【代码注释】**见下方说明块
            document.getElementById('email').addEventListener('input', function() {
                emailGroup.classList.remove('error');
            });

            document.getElementById('password').addEventListener('input', function() {
                passwordGroup.classList.remove('error');
            });
        })();
    </script>
</body>
</html>

【代码注释】

  • window.resize 在视口尺寸变化时触发,应用防抖避免布局计算过于频繁。
  • 可读取 innerWidth/innerHeight 切换移动端与桌面端布局。
  • 示例中 debounce(updateSizeInfo, 100) 在窗口停止拖动约 100ms 后再重算网格列数。
  • 市面应用 :响应式后台布局、图表 resize 重绘(ECharts)、移动端横竖屏切换。

5.3 焦点事件 focus 与 blur

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

        .input-wrapper {
            position: relative;
            margin-bottom: 25px;
        }

        .input-wrapper input {
            width: 100%;
            padding: 15px;
            border: 2px solid #e0e0e0;
            border-radius: 5px;
            font-size: 16px;
            transition: all 0.3s;
            box-sizing: border-box;
        }

        .input-wrapper input:focus {
            outline: none;
            border-color: #667eea;
            box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
        }

        .input-wrapper label {
            position: absolute;
            left: 15px;
            top: 50%;
            transform: translateY(-50%);
            color: #999;
            pointer-events: none;
            transition: all 0.3s;
        }

        .input-wrapper input:focus + label,
        .input-wrapper input:not(:placeholder-shown) + label {
            top: 0;
            transform: translateY(-50%);
            background: white;
            padding: 0 5px;
            font-size: 12px;
            color: #667eea;
        }

        .status-badge {
            position: absolute;
            right: 15px;
            top: 50%;
            transform: translateY(-50%);
            padding: 5px 10px;
            border-radius: 3px;
            font-size: 12px;
            opacity: 0;
            transition: opacity 0.3s;
        }

        .status-badge.show {
            opacity: 1;
        }

        .status-badge.focused {
            background: #e3f2fd;
            color: #1976d2;
        }

        .status-badge.blurred {
            background: #fff3e0;
            color: #f57c00;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>

    <div class="focus-demo">
        <div class="input-wrapper">
            <input type="text" id="username" placeholder=" " class="focus-input">
            <label for="username">点我</label>
            <span class="status-badge">待输入</span>
        </div>

        <div class="input-wrapper">
            <input type="email" id="email" placeholder=" " class="focus-input">
            <label>城市</label>
            <span class="status-badge">待输入</span>
        </div>

        <div class="input-wrapper">
            <input type="password" id="password" placeholder=" " class="focus-input">
            <label>城市</label>
            <span class="status-badge">待输入</span>
        </div>

        <div class="input-wrapper">
            <input type="text" id="phone" placeholder=" " class="focus-input">
            <label for="phone">点我</label>
            <span class="status-badge">待输入</span>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            const inputs = document.querySelectorAll('.focus-input');

            inputs.forEach(input => {
                const badge = input.nextElementSibling.nextElementSibling;

                // **【代码注释】**见下方说明块
                input.addEventListener('focus', function() {
                    badge.textContent = '已聚焦';
                    badge.className = 'status-badge show focused';
                    console.log(`${input.previousElementSibling.textContent} 获焦`);
                });

                // **【代码注释】**见下方说明块
                input.addEventListener('blur', function() {
                    if (this.value) {
                        badge.textContent = '已填写';
                        badge.className = 'status-badge show blurred';
                    } else {
                        badge.className = 'status-badge';
                    }
                    console.log(`${input.previousElementSibling.textContent} 失焦值: ${this.value}`);
                });

                // **【代码注释】**见下方说明块
                input.addEventListener('select', function() {
                    const start = this.selectionStart;
                    const end = this.selectionEnd;
                    const selectedText = this.value.substring(start, end);
                    console.log(`选中文本: "${selectedText}"`);
                });
            });
        })();
    </script>
</body>
</html>

【代码注释】

  • focus 在元素获得焦点时触发;blur 在失去焦点时触发(不冒泡)。
  • 表单校验、显示/隐藏提示常用这对事件;需要冒泡时用 focusin/focusout

5.4 input 与 change 区别

对比项 input change
触发时机 每次输入 失焦且值变化(select 选中即触发)
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>input 与 change 对比演示</title>
    <style>
        .comparison-demo {
            width: 700px;
            margin: 50px auto;
            padding: 30px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .demo-row {
            margin-bottom: 30px;
            padding-bottom: 20px;
            border-bottom: 1px solid #e0e0e0;
        }

        .demo-row:last-child {
            border-bottom: none;
        }

        .input-field {
            width: 100%;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 5px;
            font-size: 16px;
            margin-bottom: 10px;
            box-sizing: border-box;
        }

        .event-log {
            background: #f5f5f5;
            padding: 10px;
            border-radius: 5px;
            font-family: monospace;
            font-size: 12px;
            height: 100px;
            overflow-y: auto;
        }

        .event-log .log-item {
            margin: 3px 0;
            padding: 3px 5px;
            background: white;
            border-radius: 3px;
        }

        .event-log .input-event {
            border-left: 3px solid #28a745;
        }

        .event-log .change-event {
            border-left: 3px solid #dc3545;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">input 与 change 对比</h1>

    <div class="comparison-demo">
        <div class="demo-row">
            <h3>input:每次键入触发</h3>
            <input type="text" class="input-field" id="inputDemo" placeholder="输入测试...">
            <div class="event-log" id="inputLog">
                <div class="log-item">input 事件日志</div>
            </div>
        </div>

        <div class="demo-row">
            <h3>change:失焦或选中时触发</h3>
            <input type="text" class="input-field" id="changeDemo" placeholder="输入后点击外部失焦...">
            <div class="event-log" id="changeLog">
                <div class="log-item">change 事件日志</div>
            </div>
        </div>

        <div class="demo-row">
            <h3>input:实时触发</h3>
            <input type="text" class="input-field" id="bothDemo" placeholder="对比两者...">
            <div class="event-log" id="bothLog">
                <div class="log-item">等待操作...</div>
            </div>
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            function addLog(containerId, message, className) {
                const container = document.getElementById(containerId);
                const time = new Date().toLocaleTimeString();
                const div = document.createElement('div');
                div.className = `log-item ${className}`;
                div.textContent = `[${time}] ${message}`;
                container.appendChild(div);
                container.scrollTop = container.scrollHeight;
            }

            // input ??
            const inputDemo = document.getElementById('inputDemo');
            let inputCount = 0;
            inputDemo.addEventListener('input', function() {
                inputCount++;
                addLog('inputLog', `input ?? #${inputCount}, ?: "${this.value}"`, 'input-event');
            });

            // change ??
            const changeDemo = document.getElementById('changeDemo');
            changeDemo.addEventListener('change', function() {
                addLog('changeLog', `change ??, ?: "${this.value}"`, 'change-event');
            });

            // **【代码注释】**见下方说明块
            const bothDemo = document.getElementById('bothDemo');
            bothDemo.addEventListener('input', function() {
                addLog('bothLog', `input ??, ?: "${this.value}"`, 'input-event');
            });
            bothDemo.addEventListener('change', function() {
                addLog('bothLog', `change ??, ?: "${this.value}"`, 'change-event');
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

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

实战场景

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

5.5 二级地址联动选择

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

        .selector-row {
            display: flex;
            gap: 15px;
            margin-bottom: 20px;
        }

        .selector-group {
            flex: 1;
        }

        .selector-group label {
            display: block;
            margin-bottom: 8px;
            font-weight: bold;
            color: #333;
        }

        .selector-group select {
            width: 100%;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 5px;
            font-size: 16px;
            background: white;
            cursor: pointer;
            transition: border-color 0.3s;
        }

        .selector-group select:focus {
            outline: none;
            border-color: #667eea;
        }

        .result-display {
            padding: 15px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 5px;
            text-align: center;
            font-size: 18px;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>

    <div class="city-selector">
        <div class="selector-row">
            <div class="selector-group">
                <label for="province">省/直辖市</label>
                <select id="province">
                    <option value="">请选择</option>
                </select>
            </div>
            <div class="selector-group">
                <label>城市</label>
                <select id="city" disabled>
                    <option value="">请选择</option>
                </select>
            </div>
        </div>

        <div class="result-display" id="result">
            请选择省和市
        </div>
    </div>

    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function() {
            // **【代码注释】**见下方说明块
            const cityData = {
                '北京': ['东城区', '西城区', '朝阳区', '海淀区', '丰台区'],
                '上海': ['黄浦区', '徐汇区', '长宁区', '静安区', '浦东新区'],
                '广东': ['广州', '深圳', '珠海', '汕头', '佛山'],
                '浙江': ['杭州', '宁波', '温州', '嘉兴', '湖州'],
                '江苏': ['南京', '苏州', '无锡', '常州', '南通'],
                '四川': ['成都', '绵阳', '德阳', '南充', '宜宾'],
                '湖北': ['武汉', '宜昌', '襄阳', '荆州', '黄冈']
            };

            const provinceSelect = document.getElementById('province');
            const citySelect = document.getElementById('city');
            const resultDisplay = document.getElementById('result');

            // **【代码注释】**见下方说明块
            Object.keys(cityData).forEach(function(province) {
                const option = document.createElement('option');
                option.value = province;
                option.textContent = province;
                provinceSelect.appendChild(option);
            });

            // **【代码注释】**见下方说明块
            provinceSelect.addEventListener('change', function() {
                const selectedProvince = this.value;

                // **【代码注释】**见下方说明块
                citySelect.innerHTML = '<option value="">请选择</option>';

                if (selectedProvince) {
                    // **【代码注释】**见下方说明块
                    citySelect.disabled = false;
                    cityData[selectedProvince].forEach(function(city) {
                        const option = document.createElement('option');
                        option.value = city;
                        option.textContent = city;
                        citySelect.appendChild(option);
                    });
                } else {
                    // **【代码注释】**见下方说明块
                    citySelect.disabled = true;
                }

                updateResult();
            });

            // **【代码注释】**见下方说明块
            citySelect.addEventListener('change', updateResult);

            // **【代码注释】**见下方说明块
            function updateResult() {
                const province = provinceSelect.value;
                const city = citySelect.value;

                if (province && city) {
                    resultDisplay.textContent = `已选:${province} - ${city}`;
                } else if (province) {
                    resultDisplay.textContent = `已选省:${province},请选市`;
                } else {
                    resultDisplay.textContent = '请选择省和市';
                }
            }
        })();
    </script>
</body>
</html>

【代码注释】

  • 省市区联动用 change 而非 clickselect 选中选项会立即触发 change
  • 切换省时清空 citySelect.innerHTML 再填充城市,避免残留选项。
  • 动态生成的 <select> 同样用 addEventListener('change'),避免内联 onchange 难维护。
  • 说明 :结果区用 textContent 展示选中省、市,防止 XSS。

5.6 中文输入法与 composition 事件

中文输入法组词期间会触发 compositionstartcompositionupdatecompositionendMDN CompositionEvent)。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>composition 输入法演示</title>
    <style>
        body { font-family: sans-serif; max-width: 480px; margin: 40px auto; padding: 20px; }
        input { width: 100%; padding: 12px; font-size: 16px; box-sizing: border-box; }
        .log { margin-top: 16px; padding: 12px; background: #f5f5f5; border-radius: 8px; font-size: 13px; min-height: 120px; }
        .log div { margin: 4px 0; }
    </style>
</head>
<body>
    <h2>事件演示</h2>
    <input type="text" id="search" placeholder="请输入...">
    <div class="log" id="log"></div>
    <script>
        // ===== 完整可运行示例:复制整段到 .html 文件 =====
        (function () {
            const input = document.getElementById('search');
            const logEl = document.getElementById('log');
            let composing = false;

            function append(msg) {
                const d = document.createElement('div');
                d.textContent = msg;
                logEl.appendChild(d);
            }

            input.addEventListener('compositionstart', function () {
                composing = true;
                append('[compositionstart] 开始组词');
            });
            input.addEventListener('compositionupdate', function (e) {
                append('[compositionupdate] 更新: ' + e.data);
            });
            input.addEventListener('compositionend', function (e) {
                composing = false;
                append('[compositionend] 结束: ' + e.data);
                doSearch(input.value);
            });
            input.addEventListener('input', function () {
                if (!composing) doSearch(input.value);
            });

            function doSearch(keyword) {
                append('→ 搜索: 「' + keyword + '」');
            }
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

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

实战场景

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

【本章小结】表单事件

事件 触发时机 典型用途
submit / reset 提交 / 重置 AJAX 登录、清空校验
focus / blur 获焦 / 失焦 边框高亮、失焦校验
input 值实时变化 搜索联想、字数统计
change select 立即;input 失焦且变 下拉联动、选项保存
composition* 输入法组词 中文搜索防抖

六、图片事件详解

6.1 图片加载完成 load

事件名 说明 典型场景
load 图片加载成功 占位图替换、进度条
error 加载失败 默认图、重试

6.2 图片加载失败 error

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;
        }

        .loading-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 9999;
            transition: opacity 0.5s, visibility 0.5s;
        }

        .loading-overlay.hidden {
            opacity: 0;
            visibility: hidden;
        }

        .loading-content {
            width: 80%;
            max-width: 500px;
            text-align: center;
        }

        .loading-title {
            color: white;
            font-size: 24px;
            margin-bottom: 30px;
        }

        .progress-container {
            height: 8px;
            background: rgba(255,255,255,0.2);
            border-radius: 4px;
            overflow: hidden;
            margin-bottom: 15px;
        }

        .progress-bar {
            height: 100%;
            background: linear-gradient(90deg, #00d2ff 0%, #3a7bd5 100%);
            width: 0%;
            transition: width 0.3s;
            border-radius: 4px;
        }

        .progress-text {
            color: white;
            font-size: 18px;
            margin-bottom: 10px;
        }

        .loading-detail {
            color: rgba(255,255,255,0.7);
            font-size: 14px;
        }

        .gallery {
            max-width: 1200px;
            margin: 50px auto;
            padding: 20px;
        }

        .gallery-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
            gap: 20px;
        }

        .gallery-item {
            aspect-ratio: 4/3;
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 4px 15px rgba(0,0,0,0.1);
            background: #f0f0f0;
        }

        .gallery-item img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            transition: transform 0.3s;
        }

        .gallery-item:hover img {
            transform: scale(1.05);
        }
    </style>
</head>
<body>
    <!-- 全屏加载层 -->
    <div class="loading-overlay" id="loadingOverlay">
        <div class="loading-content">
            <h1 class="loading-title">图片加载中...</h1>
            <div class="progress-container">
                <div class="progress-bar" id="progressBar"></div>
            </div>
            <div class="progress-text" id="progressText">0%</div>
            <div class="loading-detail" id="loadingDetail">准备中...</div>
        </div>
    </div>

    <!-- 全屏加载层 -->
    <div class="gallery">
        <h1 style="text-align: center;">示例页面</h1>
        <div class="gallery-grid" id="galleryGrid">
            <!-- 阻塞脚本会延迟 DOMContentLoaded -->
        </div>
    </div>

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

            // 【见下方代码注释】
            const images = [
                'images/db01.svg', 'images/db02.svg', 'images/db03.svg',
                'images/db04.svg', 'images/db05.svg', 'images/db06.svg',
                'images/db07.svg', 'images/db08.svg', 'images/db09.svg',
                'images/db10.svg'
            ];

            let loadedCount = 0;
            const totalCount = images.length;

            // **【代码注释】**见下方说明块
            function updateProgress() {
                const progress = (loadedCount / totalCount) * 100;
                progressBar.style.width = progress + '%';
                progressText.textContent = Math.round(progress) + '%';
                loadingDetail.textContent = `??? ${loadedCount}/${totalCount} ???`;

                if (loadedCount === totalCount) {
                    setTimeout(function() {
                        loadingOverlay.classList.add('hidden');
                        setTimeout(function() {
                            loadingOverlay.style.display = 'none';
                        }, 500);
                    }, 500);
                }
            }

            // **【代码注释】**见下方说明块
            function preloadImages() {
                images.forEach(function(url, index) {
                    const img = new Image();
                    img.onload = function() {
                        loadedCount++;
                        updateProgress();

                        // **【代码注释】**见下方说明块
                        const galleryItem = document.createElement('div');
                        galleryItem.className = 'gallery-item';
                        const galleryImg = document.createElement('img');
                        galleryImg.src = url;
                        galleryImg.alt = 'Gallery Image ' + (index + 1);
                        galleryItem.appendChild(galleryImg);
                        galleryGrid.appendChild(galleryItem);
                    };

                    img.onerror = function() {
                        loadedCount++;
                        updateProgress();
                        console.error('图片加载失败:', url);
                    };

                    img.src = url;
                });
            }

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

【代码注释】

核心逻辑

  1. new Image() 预加载,不插入 DOM 也能监听 load/error,全部完成后再展示图库。
  2. loadedCount / totalCount 驱动进度条;遮罩层用 transitionopacity 淡出。
  3. onerror 计数仍可增加,避免单张失败阻塞整体进度(可按业务选择)。

注意点

  • 演示使用本地 images/*.svg;线上可换 CDN 或懒加载 + loading="lazy"

实战场景

  • 电商详情多图、H5 活动页预加载、相册首屏占位。

6.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>
        .image-grid {
            max-width: 1000px;
            margin: 50px auto;
            padding: 20px;
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: 20px;
        }

        .image-card {
            position: relative;
            aspect-ratio: 1;
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 4px 10px rgba(0,0,0,0.1);
            background: #f5f5f5;
        }

        .image-card img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            transition: transform 0.3s;
        }

        .image-card:hover img {
            transform: scale(1.05);
        }

        .image-card.error {
            display: flex;
            align-items: center;
            justify-content: center;
            background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
        }

        .image-card.error::before {
            content: '📷';
            font-size: 48px;
        }

        .image-card .error-text {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            padding: 10px;
            background: rgba(0,0,0,0.7);
            color: white;
            text-align: center;
            font-size: 12px;
        }

        .retry-btn {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            padding: 8px 16px;
            background: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            display: none;
        }

        .image-card.error .retry-btn {
            display: block;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">示例页面</h1>
    <p style="text-align: center;">请按键盘或点击操作</p>

    <div class="image-grid" id="imageGrid">
        <!-- 预加载脚本 -->
    </div>

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

            // **【代码注释】**见下方说明块
            const imageList = [
                'images/db01.svg',
                'images/not-exist-01.svg', // 【见下方代码注释】
                'images/db02.svg',
                'images/not-exist-02.svg',
                'images/db03.svg',
                'images/not-exist-03.svg',
                'images/db04.svg',
                'images/not-exist-04.svg'
            ];

            // 【见下方代码注释】
            const placeholderImage = 'data:image/svg+xml,' + encodeURIComponent(`
                <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
                    <rect fill="#e0e0e0" width="200" height="200"/>
                    <text x="100" y="100" font-family="Arial" font-size="16" fill="#999" text-anchor="middle">加载失败</text>
                </svg>
            `);

            // **【代码注释】**见下方说明块
            imageList.forEach(function(url, index) {
                const card = document.createElement('div');
                card.className = 'image-card';

                const img = document.createElement('img');
                img.alt = 'Image ' + (index + 1);
                img.dataset.originalUrl = url;

                // **【代码注释】**见下方说明块
                img.addEventListener('error', function() {
                    card.classList.add('error');
                    this.src = placeholderImage;

                    // **【代码注释】**见下方说明块
                    const errorText = document.createElement('div');
                    errorText.className = 'error-text';
                    errorText.textContent = '加载失败';
                    card.appendChild(errorText);
                });

                // **【代码注释】**见下方说明块
                img.addEventListener('load', function() {
                    console.log('图片加载成功:', url);
                });

                img.src = url;
                card.appendChild(img);
                imageGrid.appendChild(card);
            });
        })();
    </script>
</body>
</html>

【代码注释】

  • img.onerror 在 404 等失败时触发,可换 Base64 占位或内联 SVG。
  • retry 按钮重新赋值 src 触发再次加载。

【本章小结】图片事件

  • loadnew Image() 预加载或 <img>load 表示该资源可读。
  • error:404、跨域、格式错误时触发,应替换占位图或重试。
  • 区分元素级 loadwindowload;首屏进度条常用预加载 + 计数。

相关推荐
JAVA社区1 小时前
Java进阶全套教程(八)—— Docker超详细实战详解
java·运维·开发语言·docker·容器·面试·职场和发展
todaycode1 小时前
Vue + CPP项目
javascript·c++·vue.js
水木流年追梦1 小时前
大模型入门-RL基础
开发语言·python·算法·leetcode·正则表达式
.千余1 小时前
【Linux】Socket编程UDP
linux·运维·服务器·开发语言·网络协议·学习·udp
枕星而眠1 小时前
C++ String类精讲:从基础用法到进阶底层原理
开发语言·c++·后端·学习方法
江屿风1 小时前
【C++笔记】模板初阶流食般投喂
开发语言·c++·笔记
Shadow(⊙o⊙)1 小时前
qt信号和槽链接的接入与断开
开发语言·前端·c++·qt·学习
慕斯fuafua1 小时前
JS——DOM操作
前端·javascript·html
AI玫瑰助手1 小时前
Python运算符:逻辑运算符(and/or/not)的短路特性
开发语言·python·信息可视化