已拆分为两篇博客(推荐阅读拆分版):
篇次 文件 内容 上篇 JavaScript_DOM事件完全指南(上篇)------UI事件与监听基础.md §零~§八:监听注册、鼠标/键盘/文档/表单/图片/过渡/scroll/resize 下篇 JavaScript_DOM事件完全指南(下篇)------事件流委托与工程实践.md §九~§十四 + 总结:事件流、Event、委托、原型链、性能、附录 下文为 未拆分的完整合集(约 6300 行),发布博客或分段学习请优先使用上表两个文件。
一篇面向前端工程师的 DOM 事件系统 深度技术博客。内容覆盖事件监听、事件流、各类 UI 事件、Event对象、事件委托与性能优化,并配有 完整可运行的 HTML 示例 、Mermaid 流程图 与 知识点归纳 。示例中的本地图片统一使用images/目录(db01.svg~db10.svg)。
权威参考(建议配合阅读):
目录
-
- [0.1 知识体系总览](#0.1 知识体系总览)
- [0.2 核心名词解释](#0.2 核心名词解释)
- [0.3 案例覆盖清单](#0.3 案例覆盖清单)
-
- [1.1 什么是事件?](#1.1 什么是事件?)
- [1.2 事件监听的三种方式](#1.2 事件监听的三种方式)
- [1.3 三种方式对比](#1.3 三种方式对比)
- [1.4 解除事件监听](#1.4 解除事件监听)
- [1.5 事件流(Event Flow)](#1.5 事件流(Event Flow))
- [1.6 事件回调函数中的 this](#1.6 事件回调函数中的 this)
-
- [2.1 ~ 2.8 点击、按键、位置、拖拽、enter/leave、滚轮、无缝滚动](#2.1 ~ 2.8 点击、按键、位置、拖拽、enter/leave、滚轮、无缝滚动)
-
- [3.1 ~ 3.6 类型、属性、keydown/keypress、实时输入、方向键移动、监听范围](#3.1 ~ 3.6 类型、属性、keydown/keypress、实时输入、方向键移动、监听范围)
-
[十、Event 对象详解](#十、Event 对象详解)
-
[十二、DOM 对象原型链分析](#十二、DOM 对象原型链分析)
-
[十六、进阶 API 与可访问性](#十六、进阶 API 与可访问性)
零、导读与核心概念
0.1 知识体系总览
DOM 事件是浏览器将 用户行为 与 页面状态变化 通知给 JavaScript 的桥梁。从工程视角,可拆为四层:
DOM 事件
注册层
HTML 属性
DOM 属性
addEventListener
传播层
捕获 capture
目标 target
冒泡 bubble
数据层
Event 基类
MouseEvent
KeyboardEvent
模式层
事件委托
防抖节流
passive 优化
脚本
浏览器
用户侧
点击/键盘/滚动/输入
合成事件对象 Event
捕获 → 目标 → 冒泡
监听器 listener
业务逻辑 handler
0.2 核心名词解释
| 名词 | 英文 | 解析 |
|---|---|---|
| 事件 | Event | 在文档或浏览器窗口中发生的、可被脚本感知的交互或状态变化(如点击、加载完成)。 |
| 事件类型 | event type | 字符串标识,如 click、keydown,区分不同语义。 |
| 事件目标 | 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 的接口;Element、Document、Window 等均继承该能力(见 MDN EventTarget)。
0.3 案例覆盖清单
本篇示例与下列实践一一对应,全部保留并扩展为可独立运行的完整 HTML:
| 分类 | 涵盖主题 |
|---|---|
| 鼠标 | 单击/双击/右击、按下抬起、button、坐标、拖拽、mouseenter/mouseleave、滚轮兼容、无缝滚动 |
| 键盘 | 按下抬起、keydown/keypress 区别、keyup 实时输入、方向键控制移动 |
| 文档 | load、DOMContentLoaded 对比 |
| 表单 | submit/reset、focus/blur、input/change、二级联动选择 |
| 图片 | load 预加载进度、error 占位 |
| 过渡/动画 | transitionstart/transitionrun/transitionend、animationstart/animationend/animationiteration |
| 其他 | scroll 吸顶/懒加载思路、resize 响应式 |
| 进阶 | 事件流演示、target/currentTarget、stopPropagation、preventDefault、事件委托、性能对比 |
一、事件基础回顾
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对象,可调用preventDefault、stopPropagation等。
关键 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>
【代码注释】
mousedown记录offsetX/offsetY,表示鼠标在元素内的点击偏移,避免拖动时元素「跳动」。mousemove绑定在document上,防止快速拖动时指针离开元素导致中断。- 使用
clientX/clientY配合偏移计算left/top,并做视口边界钳制。 - 生产环境可改用 HTML5 Drag and Drop API 或 Pointer 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); // 当前绑定监听的元素
});
【代码注释】
核心逻辑
- 普通函数作监听器时,
this与event.currentTarget均指向绑定事件的元素。 - 箭头函数没有自己的
this,回调内应使用event.currentTarget或event.target。
关键 API / 概念
event.target:最初触发事件的节点(如点击<span>时可能是子元素)。event.currentTarget:正在执行监听器的元素(绑定addEventListener的节点)。
注意点
- React 合成事件中
this行为由框架封装,勿与原生混用。
实战场景
- 列表项点击、表单控件内嵌图标点击时,用
target+closest()定位业务行。
二、鼠标事件详解
名词 :
MouseEvent继承自UIEvent→Event,提供clientX、button、buttons等鼠标专用字段(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间隔极短时触发;需与单击业务区分(如防抖)。contextmenu:preventDefault()可阻止系统右键菜单,用于自定义面板。
注意点
- 演示区使用
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>
【代码注释】
mousedown记录offsetX/offsetY或元素矩形,作为拖拽起点。mousemove挂在document上更新幽灵节点位置,松开时在mouseup里解绑。- 用
clientX/clientY减容器getBoundingClientRect()得到left/top,避免滚动误差。 - 移动端与无障碍场景优先 HTML5 Drag and Drop API 或 Pointer 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>
【代码注释】
mousedown记录offsetX/offsetY,表示鼠标在元素内的点击偏移,避免拖动时元素「跳动」。mousemove绑定在document上,防止快速拖动时指针离开元素导致中断。- 使用
clientX/clientY配合偏移计算left/top,并做视口边界钳制。 - 生产环境可改用 HTML5 Drag and Drop API 或 Pointer Events 统一鼠标/触控。
市面应用:Trello 看板卡片拖拽、网盘文件拖入上传区、可视化搭建工具组件拖放。
【本章小结】鼠标事件
| 要点 | 记忆 |
|---|---|
| 点击族 | click / dblclick / contextmenu |
| 过程族 | mousedown → mousemove → mouseup |
| 进入离开 | 菜单用 mouseenter/mouseleave,需冒泡用 mouseover/mouseout |
| 坐标 | 元素内 offset*,视口 client*,文档 page* |
| 滚轮 | 新标准 wheel + deltaY;历史需兼容 mousewheel / DOMMouseScroll |
三、键盘事件详解
名词 :
KeyboardEvent提供key、code、repeat等;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 哪些元素可监听键盘事件
全局快捷键与输入框焦点冲突是常见坑,需区分监听目标。
- 表单控件 :焦点在
<input>,<textarea>,<select>,<button>时,通常不触发全局快捷键。 - 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判断按键;勿再依赖keypress、keyCode。 - 全局快捷键监听
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、样式表等全部加载完成。 - 动态插入带
src的img会单独触发该元素的load;window的load表示页面级资源已全部就绪。
4.3 何时使用哪个事件
适合 DOMContentLoaded 的场景
- 初始化菜单、Tab、轮播等 DOM 结构
- 绑定事件委托、请求首屏接口(不依赖图片尺寸)
- 统计首屏可交互时间(TTI 相关指标)
适合 load 的场景
- 需要读取图片
naturalWidth/ 画布尺寸 - 全屏背景图、地图瓦片加载完成后再渲染
- 旧式「等所有资源再显示页面」的 loading 遮罩
【本章小结】文档事件
DOMContentLoaded:DOM 可操作时尽早执行脚本(推荐作为业务入口)。load:全部资源就绪后再做依赖尺寸/图片的逻辑。- 性能优化:关键脚本放底部或使用
defer,避免阻塞解析。
五、表单事件详解
名词 :表单相关事件多属于
Event或InputEvent;input在每次值变化时触发,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而非click;select选中选项会立即触发change。 - 切换省时清空
citySelect.innerHTML再填充城市,避免残留选项。 - 动态生成的
<select>同样用addEventListener('change'),避免内联onchange难维护。 - 说明 :结果区用
textContent展示选中省、市,防止 XSS。
5.6 中文输入法与 composition 事件
中文输入法组词期间会触发 compositionstart 、compositionupdate 、compositionend (MDN 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>
【代码注释】
核心逻辑
- 用
new Image()预加载,不插入 DOM 也能监听load/error,全部完成后再展示图库。 loadedCount / totalCount驱动进度条;遮罩层用transition或opacity淡出。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触发再次加载。
【本章小结】图片事件
load:new Image()预加载或<img>的load表示该资源可读。error:404、跨域、格式错误时触发,应替换占位图或重试。- 区分元素级
load与window的load;首屏进度条常用预加载 + 计数。