七、过渡与动画事件
7.1 过渡事件 transition
| 事件名 | 说明 | 典型场景 |
|---|---|---|
transitionstart |
过渡开始 | 记录动画起点 |
transitionrun |
过渡进行 | 与 start 成对出现 |
transitionend |
过渡结束 | 收起面板、切换步骤 |
transitioncancel |
过渡取消 | 中途改样式时触发 |
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件演示</title>
<style>
.transition-demo {
width: 600px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.transition-box {
width: 200px;
height: 200px;
margin: 50px auto;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 18px;
cursor: pointer;
transition: all 2s ease-in-out 1s;
}
.transition-box.active {
transform: rotate(360deg) scale(1.2);
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border-radius: 50%;
}
.event-log {
margin-top: 30px;
padding: 15px;
background: #f5f5f5;
border-radius: 5px;
max-height: 250px;
overflow-y: auto;
}
.event-item {
padding: 8px;
margin: 5px 0;
background: white;
border-radius: 3px;
font-family: monospace;
font-size: 14px;
border-left: 4px solid #667eea;
}
.event-item.start { border-left-color: #28a745; }
.event-item.run { border-left-color: #ffc107; }
.event-item.end { border-left-color: #dc3545; }
.controls {
text-align: center;
margin-top: 20px;
}
.btn {
padding: 10px 20px;
margin: 0 5px;
border: none;
border-radius: 5px;
background: #667eea;
color: white;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.btn:hover {
background: #764ba2;
}
</style>
</head>
<body>
<h1 style="text-align: center;">CSS 过渡事件演示</h1>
<div class="transition-demo">
<div class="transition-box" id="transitionBox">
???
</div>
<div class="controls">
<button class="btn" id="toggleBtn">切换样式</button>
<button class="btn" id="clearBtn">清空日志</button>
</div>
<div class="event-log" id="eventLog">
<div class="event-item">等待操作...</div>
</div>
</div>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function() {
const transitionBox = document.getElementById('transitionBox');
const eventLog = document.getElementById('eventLog');
const toggleBtn = document.getElementById('toggleBtn');
const clearBtn = document.getElementById('clearBtn');
function logEvent(eventName, className) {
const time = new Date().toLocaleTimeString();
const item = document.createElement('div');
item.className = `event-item ${className}`;
item.textContent = `[${time}] ${eventName}`;
eventLog.appendChild(item);
eventLog.scrollTop = eventLog.scrollHeight;
}
// **【代码注释】**见下方说明块
transitionBox.addEventListener('transitionstart', function(e) {
console.log('transitionstart:', e);
logEvent(`transitionstart - ??: ${e.propertyName}`, 'start');
});
// **【代码注释】**见下方说明块
transitionBox.addEventListener('transitionrun', function(e) {
console.log('transitionrun:', e);
logEvent(`transitionrun - ??: ${e.propertyName}`, 'run');
});
// **【代码注释】**见下方说明块
transitionBox.addEventListener('transitionend', function(e) {
console.log('transitionend:', e);
logEvent(`transitionend - 属性: ${e.propertyName}, 耗时: ${e.elapsedTime.toFixed(2)}ms`, 'end');
});
// **【代码注释】**见下方说明块
transitionBox.addEventListener('click', function() {
this.classList.toggle('active');
});
toggleBtn.addEventListener('click', function() {
transitionBox.classList.toggle('active');
});
clearBtn.addEventListener('click', function() {
eventLog.innerHTML = '<div class="event-item">等待操作...</div>';
});
})();
</script>
</body>
</html>
【代码注释】
mousedown记录offsetX/offsetY,表示鼠标在元素内的点击偏移,避免拖动时元素「跳动」。mousemove绑定在document上,防止快速拖动时指针离开元素导致中断。- 使用
clientX/clientY配合偏移计算left/top,并做视口边界钳制。 - 生产环境可改用 HTML5 Drag and Drop API 或 Pointer Events 统一鼠标/触控。
市面应用:Trello 看板卡片拖拽、网盘文件拖入上传区、可视化搭建工具组件拖放。
7.2 动画事件 animation
| 事件名 | 说明 | 典型场景 |
|---|---|---|
animationstart |
动画开始 | 入场动效 |
animationend |
动画结束 | 移除 loading |
animationiteration |
每次循环 | 无限旋转指示器 |
animationcancel |
动画取消 | 切换 class 时 |
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件演示</title>
<style>
.animation-demo {
width: 600px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.animation-stage {
height: 200px;
background: linear-gradient(to bottom, #87CEEB 0%, #E0F7FA 100%);
border-radius: 10px;
position: relative;
overflow: hidden;
margin-bottom: 20px;
}
.animated-element {
position: absolute;
width: 60px;
height: 60px;
background: #ff6b6b;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%, 100% {
left: 20px;
top: 50%;
transform: translateY(-50%);
}
50% {
left: calc(100% - 80px);
top: 30%;
transform: translateY(-50%) scale(1.2);
}
}
.stats-panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
text-align: center;
}
.stat-card .label {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.stat-card .value {
font-size: 24px;
font-weight: bold;
color: #667eea;
}
.event-log {
padding: 15px;
background: #2d2d2d;
color: #00ff00;
border-radius: 5px;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.event-log .log-item {
margin: 3px 0;
padding: 3px 5px;
border-left: 3px solid;
padding-left: 8px;
}
.event-log .start { border-left-color: #28a745; }
.event-log .iteration { border-left-color: #ffc107; }
.event-log .end { border-left-color: #dc3545; }
.controls {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 5px;
background: #667eea;
color: white;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.btn:hover {
background: #764ba2;
}
.btn.secondary {
background: #6c757d;
}
.btn.secondary:hover {
background: #5a6268;
}
</style>
</head>
<body>
<h1 style="text-align: center;">CSS 过渡事件演示</h1>
<div class="animation-demo">
<div class="animation-stage">
<div class="animated-element" id="animElement">等待操作...</div>
</div>
<div class="stats-panel">
<div class="stat-card">
<div class="label">等待操作...</div>
<div class="value" id="startCount">0</div>
</div>
<div class="stat-card">
<div class="label">等待操作...</div>
<div class="value" id="iterationCount">0</div>
</div>
<div class="stat-card">
<div class="label">等待操作...</div>
<div class="value" id="endCount">0</div>
</div>
</div>
<div class="event-log" id="eventLog">
<div class="log-item">等待操作...</div>
</div>
<div class="controls">
<button class="btn" id="pauseBtn">暂停动画</button>
<button class="btn" id="resumeBtn">继续动画</button>
<button class="btn secondary" id="clearBtn">清空日志</button>
</div>
</div>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function() {
const animElement = document.getElementById('animElement');
const eventLog = document.getElementById('eventLog');
const startCount = document.getElementById('startCount');
const iterationCount = document.getElementById('iterationCount');
const endCount = document.getElementById('endCount');
const pauseBtn = document.getElementById('pauseBtn');
const resumeBtn = document.getElementById('resumeBtn');
const clearBtn = document.getElementById('clearBtn');
let counts = { start: 0, iteration: 0, end: 0 };
function logEvent(message, className) {
const time = new Date().toLocaleTimeString();
const item = document.createElement('div');
item.className = `log-item ${className}`;
item.textContent = `[${time}] ${message}`;
eventLog.appendChild(item);
eventLog.scrollTop = eventLog.scrollHeight;
}
function updateCounts() {
startCount.textContent = counts.start;
iterationCount.textContent = counts.iteration;
endCount.textContent = counts.end;
}
// **【代码注释】**见下方说明块
animElement.addEventListener('animationstart', function(e) {
counts.start++;
updateCounts();
console.log('animationstart:', e);
logEvent(`animationstart - ??: ${e.animationName}`, 'start');
});
// **【代码注释】**见下方说明块
animElement.addEventListener('animationiteration', function(e) {
counts.iteration++;
updateCounts();
console.log('animationiteration:', e);
logEvent(`animationiteration - ${e.animationName} 新一轮`, 'iteration');
});
// **【代码注释】**见下方说明块
animElement.addEventListener('animationend', function(e) {
counts.end++;
updateCounts();
console.log('animationend:', e);
logEvent(`animationend - 动画: ${e.animationName}, 耗时: ${e.elapsedTime.toFixed(2)}ms`, 'end');
});
// **【代码注释】**见下方说明块
pauseBtn.addEventListener('click', function() {
animElement.style.animationPlayState = 'paused';
});
resumeBtn.addEventListener('click', function() {
animElement.style.animationPlayState = 'running';
});
clearBtn.addEventListener('click', function() {
eventLog.innerHTML = '<div class="log-item">等待操作...</div>';
counts = { start: 0, iteration: 0, end: 0 };
updateCounts();
});
})();
</script>
</body>
</html>
【代码注释】
mousedown记录offsetX/offsetY,表示鼠标在元素内的点击偏移,避免拖动时元素「跳动」。mousemove绑定在document上,防止快速拖动时指针离开元素导致中断。- 使用
clientX/clientY配合偏移计算left/top,并做视口边界钳制。 - 生产环境可改用 HTML5 Drag and Drop API 或 Pointer Events 统一鼠标/触控。
市面应用:Trello 看板卡片拖拽、网盘文件拖入上传区、可视化搭建工具组件拖放。
【本章小结】
|------|----------|----------|
| CSS transition | transitionrun / transitionstart / transitionend | 属性过渡结束再改 DOM |
| CSS animation | animationstart / animationiteration / animationend | @keyframes 多阶段动效 |
八、其他重要事件
8.1 滚动事件 scroll
scroll 在元素滚动时触发(冒泡),常用于吸顶导航、阅读进度、回到顶部。监听容器用 element.addEventListener('scroll', ...);window 滚动监听 document 或 window。
- 说明 :高频触发,应用
requestAnimationFrame或节流合并计算。 - 说明 :
scrollTop/scrollLeft读位置;scrollHeight算进度百分比。 - 说明 :
position: fixed导航常配合classList.toggle('scrolled')。 - 说明 :移动端注意
-webkit-overflow-scrolling: touch与滚动穿透。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件演示</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
}
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 20px;
background: transparent;
transition: all 0.3s;
z-index: 1000;
}
.navbar.scrolled {
background: rgba(102, 126, 234, 0.95);
padding: 15px 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.navbar ul {
list-style: none;
display: flex;
justify-content: center;
gap: 30px;
}
.navbar a {
color: white;
text-decoration: none;
font-weight: bold;
}
.hero {
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 48px;
}
.scroll-indicator {
position: absolute;
bottom: 30px;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-20px); }
60% { transform: translateY(-10px); }
}
.content-section {
padding: 100px 20px;
max-width: 1000px;
margin: 0 auto;
}
.content-section h2 {
font-size: 32px;
margin-bottom: 20px;
color: #333;
}
.content-section p {
font-size: 16px;
line-height: 1.8;
color: #666;
margin-bottom: 15px;
}
.scroll-progress {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: linear-gradient(90deg, #00d2ff 0%, #3a7bd5 100%);
width: 0%;
z-index: 1001;
transition: width 0.1s;
}
.scroll-info {
position: fixed;
bottom: 20px;
right: 20px;
padding: 15px;
background: rgba(0,0,0,0.8);
color: white;
border-radius: 5px;
font-family: monospace;
z-index: 1000;
}
.back-to-top {
position: fixed;
bottom: 80px;
right: 20px;
width: 50px;
height: 50px;
background: #667eea;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
z-index: 1000;
font-size: 24px;
}
.back-to-top.show {
opacity: 1;
visibility: visible;
}
.back-to-top:hover {
background: #764ba2;
transform: translateY(-5px);
}
.lazy-image {
width: 100%;
height: 300px;
margin: 20px 0;
background: #f0f0f0;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 18px;
transition: opacity 0.5s;
}
.lazy-image img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 10px;
opacity: 0;
}
.lazy-image img.loaded {
opacity: 1;
}
</style>
</head>
<body>
<div class="scroll-progress" id="scrollProgress"></div>
<nav class="navbar" id="navbar">
<ul>
<li><a href="#home">??</a></li>
<li><a href="#about">??</a></li>
<li><a href="#services">??</a></li>
<li><a href="#contact">??</a></li>
</ul>
</nav>
<div class="hero" id="home">
<div>
<h1 style="text-align: center;">示例页面</h1>
<p>请按键盘或点击操作</p>
</div>
<div class="scroll-indicator">等待操作...</div>
</div>
<div class="content-section" id="about">
<h2>事件演示</h2>
<p>请按键盘或点击操作</p>
<p>请按键盘或点击操作</p>
<div class="lazy-image" data-src="images/db01.svg">
<span>懒加载占位...</span>
</div>
</div>
<div class="content-section" id="services">
<h2>事件演示</h2>
<p>请按键盘或点击操作</p>
<p>请按键盘或点击操作</p>
<div class="lazy-image" data-src="images/db02.svg">
<span>懒加载占位...</span>
</div>
</div>
<div class="content-section" id="contact">
<h2>事件演示</h2>
<p>请按键盘或点击操作</p>
<p>请按键盘或点击操作</p>
<div class="lazy-image" data-src="images/db03.svg">
<span>懒加载占位...</span>
</div>
</div>
<div style="height: 500px;"></div>
<div class="scroll-info" id="scrollInfo">
<div>????: <span id="scrollPos">0</span>px</div>
<div>????: <span id="docHeight">0</span>px</div>
<div>????: <span id="viewHeight">0</span>px</div>
</div>
<button class="back-to-top" id="backToTop">?</button>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function() {
const navbar = document.getElementById('navbar');
const scrollProgress = document.getElementById('scrollProgress');
const scrollPos = document.getElementById('scrollPos');
const docHeight = document.getElementById('docHeight');
const viewHeight = document.getElementById('viewHeight');
const backToTop = document.getElementById('backToTop');
const lazyImages = document.querySelectorAll('.lazy-image');
// **【代码注释】**见下方说明块
function updateScrollInfo() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const documentHeight = document.documentElement.scrollHeight;
const viewportHeight = window.innerHeight;
scrollPos.textContent = Math.round(scrollTop);
docHeight.textContent = documentHeight;
viewHeight.textContent = viewportHeight;
// **【代码注释】**见下方说明块
const progress = (scrollTop / (documentHeight - viewportHeight)) * 100;
scrollProgress.style.width = progress + '%';
// **【代码注释】**见下方说明块
if (scrollTop > 50) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
// **【代码注释】**见下方说明块
if (scrollTop > 500) {
backToTop.classList.add('show');
} else {
backToTop.classList.remove('show');
}
}
// **【代码注释】**见下方说明块
function lazyLoad() {
lazyImages.forEach(function(container) {
const rect = container.getBoundingClientRect();
const img = container.querySelector('img');
if (rect.top < window.innerHeight && rect.bottom > 0 && !img) {
const src = container.dataset.src;
const image = document.createElement('img');
image.alt = 'Lazy loaded image';
image.addEventListener('load', function() {
this.classList.add('loaded');
container.querySelector('span').style.display = 'none';
});
image.src = src;
container.appendChild(image);
}
});
}
// **【代码注释】**见下方说明块
let ticking = false;
window.addEventListener('scroll', function() {
if (!ticking) {
window.requestAnimationFrame(function() {
updateScrollInfo();
lazyLoad();
ticking = false;
});
ticking = true;
}
});
// **【代码注释】**见下方说明块
backToTop.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
// **【代码注释】**见下方说明块
updateScrollInfo();
lazyLoad();
})();
</script>
</body>
</html>
【代码注释】
核心逻辑
scroll配合requestAnimationFrame节流,更新进度条与导航样式。scrollTop / (scrollHeight - clientHeight)计算阅读进度百分比。getBoundingClientRect()+ 视口高度实现懒加载图片。scrollTo({ behavior: 'smooth' })实现回到顶部。
实战场景:文档站阅读进度、吸顶导航、无限列表懒加载、回到顶部按钮。
8.2 视口尺寸改变 resize
resize 在窗口或元素尺寸变化时触发(窗口级监听 window)。
- 节流:resize 高频,用防抖/节流再重算布局。
- 典型用途:ECharts/Canvas 重绘、响应式表格列宽、移动端横竖屏切换。
- 注意 :仅尺寸变化触发,与
scroll无关;visualViewport可辅助移动端软键盘场景。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件演示</title>
<style>
.resize-demo {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.size-info-panel {
position: fixed;
top: 20px;
right: 20px;
padding: 20px;
background: rgba(0,0,0,0.8);
color: white;
border-radius: 10px;
font-family: monospace;
z-index: 1000;
min-width: 200px;
}
.size-info-panel div {
margin: 10px 0;
}
.size-info-panel .label {
color: #aaa;
}
.size-info-panel .value {
color: #00ff00;
font-size: 18px;
}
.responsive-grid {
display: grid;
gap: 20px;
margin-top: 20px;
}
.grid-item {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 10px;
text-align: center;
min-height: 150px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.grid-item h3 {
margin-bottom: 10px;
}
.breakpoint-info {
background: #f5f5f5;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.breakpoint-bar {
display: flex;
height: 30px;
border-radius: 5px;
overflow: hidden;
margin-top: 10px;
}
.breakpoint-segment {
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: bold;
transition: all 0.3s;
}
</style>
</head>
<body>
<div class="size-info-panel" id="sizeInfo">
<div><span class="label">????:</span> <span class="value" id="viewportWidth">0</span>px</div>
<div><span class="label">????:</span> <span class="value" id="viewportHeight">0</span>px</div>
<div><span class="label">??:</span> <span class="value" id="breakpoint">-</span></div>
<div><span class="label">????:</span> <span class="value" id="gridColumns">0</span></div>
</div>
<div class="resize-demo">
<h1 style="text-align: center;">示例页面</h1>
<p style="text-align: center;">请按键盘或点击操作</p>
<div class="breakpoint-info">
<h3>?????</h3>
<div class="breakpoint-bar" id="breakpointBar">
<div class="breakpoint-segment" style="width: 33.33%; background: #ff6b6b;">XS</div>
<div class="breakpoint-segment" style="width: 33.33%; background: #feca57;">SM</div>
<div class="breakpoint-segment" style="width: 33.33%; background: #48dbfb;">LG</div>
</div>
<p style="margin-top: 10px;">
<strong>XS:</strong> <768px | <strong>SM:</strong> 768-1024px | <strong>LG:</strong> >1024px
</p>
</div>
<div class="responsive-grid" id="responsiveGrid">
<!-- ??????JS???? -->
</div>
</div>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function() {
const viewportWidth = document.getElementById('viewportWidth');
const viewportHeight = document.getElementById('viewportHeight');
const breakpoint = document.getElementById('breakpoint');
const gridColumns = document.getElementById('gridColumns');
const responsiveGrid = document.getElementById('responsiveGrid');
const breakpointBar = document.getElementById('breakpointBar');
// **【代码注释】**见下方说明块
const breakpoints = {
xs: { min: 0, max: 767, columns: 1, name: 'XS (Extra Small)' },
sm: { min: 768, max: 1023, columns: 2, name: 'SM (Small)' },
lg: { min: 1024, max: Infinity, columns: 3, name: 'LG (Large)' }
};
// **【代码注释】**见下方说明块
function updateSizeInfo() {
const width = window.innerWidth;
const height = window.innerHeight;
viewportWidth.textContent = width;
viewportHeight.textContent = height;
// **【代码注释】**见下方说明块
let currentBreakpoint;
if (width < 768) {
currentBreakpoint = breakpoints.xs;
highlightBreakpoint(0);
} else if (width < 1024) {
currentBreakpoint = breakpoints.sm;
highlightBreakpoint(1);
} else {
currentBreakpoint = breakpoints.lg;
highlightBreakpoint(2);
}
breakpoint.textContent = currentBreakpoint.name;
gridColumns.textContent = currentBreakpoint.columns;
// **【代码注释】**见下方说明块
updateGrid(currentBreakpoint.columns);
}
// **【代码注释】**见下方说明块
function highlightBreakpoint(index) {
const segments = breakpointBar.querySelectorAll('.breakpoint-segment');
segments.forEach((seg, i) => {
if (i === index) {
seg.style.transform = 'scale(1.1)';
seg.style.boxShadow = '0 0 10px rgba(0,0,0,0.3)';
} else {
seg.style.transform = 'scale(1)';
seg.style.boxShadow = 'none';
}
});
}
// **【代码注释】**见下方说明块
function updateGrid(columns) {
responsiveGrid.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
// **【代码注释】**见下方说明块
const itemCount = columns * 2;
while (responsiveGrid.children.length < itemCount) {
const item = document.createElement('div');
item.className = 'grid-item';
item.innerHTML = `
<h3>??? ${responsiveGrid.children.length + 1}</h3>
<p>????: ${columns}</p>
`;
responsiveGrid.appendChild(item);
}
}
// **【代码注释】**见下方说明块
function debounce(func, wait) {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
};
}
// **【代码注释】**见下方说明块
window.addEventListener('resize', debounce(updateSizeInfo, 100));
// **【代码注释】**见下方说明块
updateSizeInfo();
})();
</script>
</body>
</html>
【代码注释】
核心逻辑
- 封装
updateSizeInfo(),读取innerWidth更新面板并计算栅格列数。 window监听resize,用debounce(updateSizeInfo, 100)合并 100ms 内的连续触发。highlightBreakpoint()高亮当前断点;updateGrid(columns)设置grid-template-columns。
关键 API / 概念
resize:窗口尺寸变化;元素级尺寸用ResizeObserver。window.innerWidth/innerHeight:布局视口;与 CSS@media断点配合。debounce(func, wait):resize 场景必备,避免布局抖动。gridTemplateColumns:repeat(n, 1fr)实现响应式列数。
注意点
- 使用
window.addEventListener('resize', ...),没有window.onresize属性链式写法。 - 单元素尺寸变化(侧边栏折叠)优先
ResizeObserver,勿误用 window resize。 - 移动端软键盘可能只改变 visual viewport,需
visualViewportAPI 辅助。
实战场景
- ECharts、Canvas 在
resize后调用chart.resize()。 - 后台管理系统侧栏展开/收起后重算表格列宽。
8.3 触摸与 Pointer 事件(扩展)
移动端常用 touchstart / touchmove / touchend ;现代推荐 Pointer Events (pointerdown / pointermove / pointerup)统一鼠标、触控笔与触摸(MDN Pointer events)。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pointer ????</title>
<style>
body { margin: 0; font-family: sans-serif; }
.pad {
width: 100%; height: 70vh; background: linear-gradient(135deg, #1e3c72, #2a5298);
touch-action: none; /* ????????????????? */
position: relative; overflow: hidden;
}
.dot {
position: absolute; width: 48px; height: 48px; margin: -24px 0 0 -24px;
border-radius: 50%; background: #ff6b6b; color: #fff;
display: flex; align-items: center; justify-content: center;
font-size: 12px; pointer-events: none; user-select: none;
}
.info { padding: 16px; background: #f5f5f5; font-family: monospace; font-size: 13px; }
</style>
</head>
<body>
<p class="info" id="info">请按键盘或点击操作</p>
<div class="pad" id="pad"></div>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function () {
const pad = document.getElementById('pad');
const info = document.getElementById('info');
let dot = null;
pad.addEventListener('pointerdown', function (e) {
pad.setPointerCapture(e.pointerId); // 【见下方代码注释】
dot = document.createElement('div');
dot.className = 'dot';
dot.textContent = e.pointerType; // mouse / touch / pen
pad.appendChild(dot);
moveDot(e);
});
pad.addEventListener('pointermove', function (e) {
if (dot) moveDot(e);
});
pad.addEventListener('pointerup', function (e) {
pad.releasePointerCapture(e.pointerId);
if (dot) { dot.remove(); dot = null; }
info.textContent = '??: ' + e.pointerType + ', pressure=' + e.pressure;
});
function moveDot(e) {
const rect = pad.getBoundingClientRect();
dot.style.left = (e.clientX - rect.left) + 'px';
dot.style.top = (e.clientY - rect.top) + 'px';
info.textContent = 'type=' + e.pointerType + ', x=' + Math.round(e.clientX - rect.left);
}
})();
</script>
</body>
</html>
【代码注释】
touch-action: none配合 Pointer 事件,避免滚动与自定义手势冲突。setPointerCapture将后续pointermove/pointerup定向到当前元素,类似拖拽时监听document。pointerType区分mouse、touch、pen,便于统计多端行为。- 市面应用:签名板、画板 App、移动端地图拖拽。
【本章小结】
| 事件 | 绑定对象 | 典型场景 |
|---|---|---|
scroll |
元素 / window |
进度条、吸顶、懒加载 |
resize |
window |
图表重绘、响应式布局 |
pointer* |
统一指针 | 触控+鼠标一体化 |
九、事件流机制深度解析
9.1 事件传播三阶段回顾
子元素(目标) 父元素 body document window 子元素(目标) 父元素 body document window 捕获阶段(自上而下) 目标阶段 冒泡阶段(自下而上) 1. 捕获 2. 捕获 3. 捕获 4. 捕获 5. 目标 6. 冒泡 7. 冒泡 8. 冒泡 9. 冒泡
9.2 捕获与冒泡实战演示
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件演示</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: #f5f5f5;
}
.event-flow-demo {
max-width: 800px;
margin: 0 auto;
}
.element-box {
padding: 40px;
margin: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.outer {
background: #ffeaa7;
}
.middle {
background: #74b9ff;
}
.inner {
background: #ff7675;
color: white;
text-align: center;
padding: 30px;
}
.log-panel {
background: #2d2d2d;
color: #00ff00;
padding: 15px;
border-radius: 10px;
height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.log-item {
padding: 5px;
margin: 2px 0;
border-left: 3px solid;
padding-left: 8px;
}
.log-item.capture { border-left-color: #00cec9; }
.log-item.target { border-left-color: #fdcb6e; }
.log-item.bubble { border-left-color: #e17055; }
.controls {
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 10px;
}
.checkbox-group {
display: flex;
gap: 20px;
margin-bottom: 15px;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
background: #6c5ce7;
color: white;
cursor: pointer;
margin-right: 10px;
}
.btn:hover {
background: #a29bfe;
}
</style>
</head>
<body>
<div class="event-flow-demo">
<h1 style="text-align: center;">DOM???????</h1>
<div class="controls">
<h3>事件时间线</h3>
<div class="checkbox-group">
<label><input type="checkbox" checked data-type="capture" data-target="all"> ????</label>
<label><input type="checkbox" checked data-type="bubble" data-target="all"> ????</label>
</div>
<button class="btn" id="clearLog">????</button>
<button class="btn" id="resetAll">??</button>
</div>
<div class="element-box outer" id="outer">
滚轮演示区
<div class="element-box middle" id="middle">
滚轮演示区
<div class="element-box inner" id="inner">
滚动方向:
</div>
</div>
</div>
<div class="log-panel" id="logPanel">
<div class="log-item">等待操作...</div>
</div>
</div>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function() {
const elements = {
outer: document.getElementById('outer'),
middle: document.getElementById('middle'),
inner: document.getElementById('inner')
};
const logPanel = document.getElementById('logPanel');
// **【代码注释】**见下方说明块
const handlers = {
outer: { capture: null, bubble: null },
middle: { capture: null, bubble: null },
inner: { capture: null, bubble: null }
};
// **【代码注释】**见下方说明块
function log(message, type) {
const time = new Date().toLocaleTimeString();
const div = document.createElement('div');
div.className = `log-item ${type}`;
div.textContent = `[${time}] ${message}`;
logPanel.appendChild(div);
logPanel.scrollTop = logPanel.scrollHeight;
}
// **【代码注释】**见下方说明块
function createHandler(elementName, phase) {
return function(event) {
const phaseName = phase === 'capture' ? '??' : '??';
const type = event.eventPhase === 2 ? 'target' : phase;
log(`${elementName} - ${phaseName}?? (eventPhase: ${event.eventPhase})`, type);
};
}
// **【代码注释】**见下方说明块
function updateListeners() {
const enableCapture = document.querySelector('[data-type="capture"][data-target="all"]').checked;
const enableBubble = document.querySelector('[data-type="bubble"][data-target="all"]').checked;
Object.keys(elements).forEach(key => {
const el = elements[key];
// **【代码注释】**见下方说明块
if (handlers[key].capture) {
el.removeEventListener('click', handlers[key].capture, true);
handlers[key].capture = null;
}
if (handlers[key].bubble) {
el.removeEventListener('click', handlers[key].bubble, false);
handlers[key].bubble = null;
}
// **【代码注释】**见下方说明块
if (enableCapture) {
handlers[key].capture = createHandler(key, 'capture');
el.addEventListener('click', handlers[key].capture, true);
}
if (enableBubble) {
handlers[key].bubble = createHandler(key, 'bubble');
el.addEventListener('click', handlers[key].bubble, false);
}
});
}
// **【代码注释】**见下方说明块
updateListeners();
// **【代码注释】**见下方说明块
document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', updateListeners);
});
// **【代码注释】**见下方说明块
document.getElementById('clearLog').addEventListener('click', function() {
logPanel.innerHTML = '<div class="log-item">等待操作...</div>';
});
// **【代码注释】**见下方说明块
document.getElementById('resetAll').addEventListener('click', function() {
document.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true);
updateListeners();
logPanel.innerHTML = '<div class="log-item">等待操作...</div>';
});
})();
</script>
</body>
</html>
【代码注释】
核心逻辑
- 见示例代码中的
addEventListener注册与事件对象常用属性。 - 在浏览器控制台查看输出,对照 MDN 文档理解触发顺序。
实战场景
- 将示例复制为独立
.html文件即可本地运行验证。
十、Event 对象详解
10.1 Event 对象属性总览
Event
+bubbles: boolean
+cancelable: boolean
+currentTarget: Element
+defaultPrevented: boolean
+eventPhase: number
+target: Element
+timeStamp: number
+type: string
+preventDefault()
+stopPropagation()
+stopImmediatePropagation()
MouseEvent
+button: number
+buttons: number
+clientX: number
+clientY: number
+offsetX: number
+offsetY: number
+pageX: number
+pageY: number
+screenX: number
+screenY: number
KeyboardEvent
+key: string
+code: string
+keyCode: number
+ctrlKey: boolean
+shiftKey: boolean
+altKey: boolean
+metaKey: boolean
10.2 target 与 currentTarget 区别
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>target 与 currentTarget 对比</title>
<style>
.demo-container {
width: 600px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.click-area {
padding: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
color: white;
}
.click-area button {
padding: 15px 30px;
background: white;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
margin: 10px;
}
.info-panel {
margin-top: 20px;
padding: 20px;
background: #f5f5f5;
border-radius: 10px;
}
.info-item {
margin: 10px 0;
padding: 10px;
background: white;
border-radius: 5px;
font-family: monospace;
}
.info-item .label {
color: #666;
font-weight: bold;
}
.info-item .value {
color: #667eea;
word-break: break-all;
}
</style>
</head>
<body>
<h1 style="text-align: center;">target ? currentTarget ???</h1>
<div class="demo-container">
<div class="click-area" id="clickArea">
<h3>对比 target 与 currentTarget</h3>
<p>请按键盘或点击操作</p>
<button id="btn1">?? A</button>
<button id="btn2">?? B</button>
</div>
<div class="info-panel">
<h3>点击测试区</h3>
<div class="info-item">
<div class="label">event.target:</div>
<div class="value" id="targetInfo">-</div>
</div>
<div class="info-item">
<div class="label">event.currentTarget:</div>
<div class="value" id="currentTargetInfo">-</div>
</div>
<div class="info-item">
<div class="label">this:</div>
<div class="value" id="thisInfo">-</div>
</div>
<div class="info-item">
<div class="label">??:</div>
<div class="value">
target: 实际被点击的节点<br>
currentTarget: 绑定监听器的节点
</div>
</div>
</div>
</div>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function() {
const clickArea = document.getElementById('clickArea');
const targetInfo = document.getElementById('targetInfo');
const currentTargetInfo = document.getElementById('currentTargetInfo');
const thisInfo = document.getElementById('thisInfo');
clickArea.addEventListener('click', function(event) {
targetInfo.textContent = event.target.tagName + (event.target.id ? '#' + event.target.id : '');
currentTargetInfo.textContent = event.currentTarget.tagName + (event.currentTarget.id ? '#' + event.currentTarget.id : '');
thisInfo.textContent = this.tagName + (this.id ? '#' + this.id : '');
});
})();
</script>
</body>
</html>
【代码注释】
click:完整的「按下 + 在同一元素内释放」才触发;若按下后拖出元素再释放,可能不触发click。dblclick:两次click间隔极短时触发;注意与两次单独click的交互设计(如「单击选中、双击打开」需防抖区分)。contextmenu:右键菜单;event.preventDefault()可阻止系统菜单,用于自定义右键面板(地图标注、表格行操作等)。
市面应用:GitHub 代码行号区右键菜单、Figma 画布右键、电商商品图「右键另存为」拦截。
10.3 阻止默认行为 preventDefault
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件演示</title>
<style>
.bubble-demo {
max-width: 600px;
margin: 50px auto;
}
.box {
padding: 40px;
margin: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
transition: background 0.3s;
}
.box.outer { background: #ffeaa7; }
.box.middle { background: #74b9ff; }
.box.inner { background: #ff7675; color: white; }
.box.clicked {
transform: scale(0.98);
}
.log-panel {
background: #2d2d2d;
color: #00ff00;
padding: 15px;
border-radius: 10px;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.controls {
margin: 20px 0;
padding: 20px;
background: white;
border-radius: 10px;
}
.toggle-switch {
display: flex;
align-items: center;
gap: 10px;
}
.toggle-switch input {
width: 20px;
height: 20px;
}
</style>
</head>
<body>
<h1 style="text-align: center;">示例页面</h1>
<div class="bubble-demo">
<div class="controls">
<label class="toggle-switch">
<input type="checkbox" id="stopBubble">
<span>事件传播顺序演示</span>
</label>
<button class="btn" id="clearLog" style="margin-left: 20px;">清空日志</button>
</div>
<div class="box outer" id="outer">
滚轮演示区
<div class="box middle" id="middle">
滚轮演示区
<div class="box inner" id="inner">
滚动方向:
</div>
</div>
</div>
<div class="log-panel" id="logPanel">
<div>等待操作...</div>
</div>
</div>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function() {
const elements = {
outer: document.getElementById('outer'),
middle: document.getElementById('middle'),
inner: document.getElementById('inner')
};
const stopBubble = document.getElementById('stopBubble');
const logPanel = document.getElementById('logPanel');
function log(message) {
const time = new Date().toLocaleTimeString();
const div = document.createElement('div');
div.textContent = `[${time}] ${message}`;
logPanel.appendChild(div);
logPanel.scrollTop = logPanel.scrollHeight;
}
// **【代码注释】**见下方说明块
Object.keys(elements).forEach(key => {
const el = elements[key];
el.addEventListener('click', function(event) {
const name = el.className.split(' ').find(c => ['outer', 'middle', 'inner'].includes(c));
// **【代码注释】**见下方说明块
el.classList.add('clicked');
setTimeout(() => el.classList.remove('clicked'), 200);
log(`${name} 阶段`);
// **【代码注释】**见下方说明块
if (key === 'inner' && stopBubble.checked) {
event.stopPropagation();
log('已阻止进一步传播');
}
});
});
// **【代码注释】**见下方说明块
document.getElementById('clearLog').addEventListener('click', function() {
logPanel.innerHTML = '<div>等待操作...</div>';
});
})();
</script>
</body>
</html>
【代码注释】
核心逻辑
- 见示例代码中的
addEventListener注册与事件对象常用属性。 - 在浏览器控制台查看输出,对照 MDN 文档理解触发顺序。
实战场景
- 将示例复制为独立
.html文件即可本地运行验证。
10.4 阻止传播 stopPropagation
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件演示</title>
<style>
.prevent-demo {
max-width: 800px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.demo-section {
margin: 30px 0;
padding: 20px;
background: #f5f5f5;
border-radius: 10px;
}
.demo-section h3 {
margin-top: 0;
color: #333;
}
.link-demo a {
color: #667eea;
text-decoration: none;
font-weight: bold;
}
.form-demo input {
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 5px;
width: 200px;
}
.form-demo button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.context-demo {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
text-align: center;
}
.controls {
margin: 20px 0;
padding: 15px;
background: #e3f2fd;
border-radius: 10px;
}
.checkbox-group {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.message {
margin-top: 15px;
padding: 10px;
background: #fff3cd;
border-radius: 5px;
display: none;
}
.message.show {
display: block;
}
</style>
</head>
<body>
<h1 style="text-align: center;">示例页面</h1>
<div class="prevent-demo">
<div class="controls">
<h3>document 级快捷键演示</h3>
<div class="checkbox-group">
<label><input type="checkbox" id="preventLink"> 阻止链接跳转</label>
<label><input type="checkbox" id="preventForm"> 阻止表单提交</label>
<label><input type="checkbox" id="preventContext"> 阻止右键菜单</label>
</div>
<div class="message" id="message">等待操作...</div>
</div>
<div class="demo-section link-demo">
<h3>1. 链接默认行为</h3>
<p><a href="https://example.com" id="testLink">点击测试(默认会打开 example.com)</a></p>
</div>
<div class="demo-section form-demo">
<h3>2. 表单提交</h3>
<form id="testForm">
<input type="text" placeholder="请输入..." required>
<button type="submit">??</button>
</form>
</div>
<div class="demo-section context-demo" id="contextArea">
<h3>3. 右键菜单</h3>
<p>请按键盘或点击操作</p>
</div>
</div>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function() {
const preventLink = document.getElementById('preventLink');
const preventForm = document.getElementById('preventForm');
const preventContext = document.getElementById('preventContext');
const message = document.getElementById('message');
function showMessage() {
message.classList.add('show');
setTimeout(() => message.classList.remove('show'), 2000);
}
// **【代码注释】**见下方说明块
document.getElementById('testLink').addEventListener('click', function(e) {
if (preventLink.checked) {
e.preventDefault();
console.log('按钮被点击了!');
showMessage();
}
});
// **【代码注释】**见下方说明块
document.getElementById('testForm').addEventListener('submit', function(e) {
if (preventForm.checked) {
e.preventDefault();
console.log('按钮被点击了!');
showMessage();
}
});
// **【代码注释】**见下方说明块
document.getElementById('contextArea').addEventListener('contextmenu', function(e) {
if (preventContext.checked) {
e.preventDefault();
console.log('按钮被点击了!');
showMessage();
}
});
})();
</script>
</body>
</html>
【代码注释】
click:完整的「按下 + 在同一元素内释放」才触发;若按下后拖出元素再释放,可能不触发click。dblclick:两次click间隔极短时触发;注意与两次单独click的交互设计(如「单击选中、双击打开」需防抖区分)。contextmenu:右键菜单;event.preventDefault()可阻止系统菜单,用于自定义右键面板(地图标注、表格行操作等)。
市面应用:GitHub 代码行号区右键菜单、Figma 画布右键、电商商品图「右键另存为」拦截。
十一、事件委托模式
11.1 事件委托原理
事件委托(Event Delegation) 利用冒泡,在父节点统一监听子元素事件,减少监听器数量、支持动态子节点。
说明 :判断 event.target 是否匹配选择器,或用 event.target.closest('.item') 向上查找;勿与 mouseenter(不冒泡)混淆,列表点击常用 click。
冒泡
冒泡
冒泡
父容器 ul#list
一个 click 监听
动态 li 1
动态 li 2
动态 li 3
判断 target
执行业务逻辑
11.2 事件委托实战演示
| ?? | ?? |
|---|
11.3 事件委托性能对比
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件演示</title>
<style>
.delegation-demo {
max-width: 800px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 15px;
margin-bottom: 10px;
background: #f5f5f5;
border-radius: 5px;
transition: all 0.3s;
}
.todo-item:hover {
background: #e9ecef;
}
.todo-item.completed {
opacity: 0.6;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
}
.todo-checkbox {
width: 20px;
height: 20px;
margin-right: 15px;
cursor: pointer;
}
.todo-text {
flex: 1;
font-size: 16px;
}
.todo-delete {
padding: 5px 15px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
opacity: 0.8;
transition: opacity 0.3s;
}
.todo-delete:hover {
opacity: 1;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.input-group input {
flex: 1;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 16px;
}
.input-group button {
padding: 12px 25px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.stats {
display: flex;
justify-content: space-between;
margin-top: 20px;
padding: 15px;
background: #e3f2fd;
border-radius: 5px;
}
.log-panel {
margin-top: 20px;
padding: 15px;
background: #2d2d2d;
color: #00ff00;
border-radius: 5px;
height: 150px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<h1 style="text-align: center;">待办列表 - 事件委托</h1>
<div class="delegation-demo">
<div class="input-group">
<input type="text" id="todoInput" placeholder="输入待办事项...">
<button id="addBtn">??</button>
</div>
<ul class="todo-list" id="todoList">
<li class="todo-item" data-id="1">
<input type="checkbox" class="todo-checkbox">
<span class="todo-text">学习 JavaScript 事件</span>
<button class="todo-delete">??</button>
</li>
<li class="todo-item" data-id="2">
<input type="checkbox" class="todo-checkbox">
<span class="todo-text">复习 DOM 委托</span>
<button class="todo-delete">??</button>
</li>
<li class="todo-item" data-id="3">
<input type="checkbox" class="todo-checkbox">
<span class="todo-text">完成课后作业</span>
<button class="todo-delete">??</button>
</li>
</ul>
<div class="stats">
<span>总计: <strong id="totalCount">3</strong></span>
<span>已完成: <strong id="completedCount">0</strong></span>
<span>未完成: <strong id="activeCount">3</strong></span>
</div>
<div class="log-panel" id="logPanel">
<div>等待操作...</div>
</div>
</div>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function() {
const todoList = document.getElementById('todoList');
const todoInput = document.getElementById('todoInput');
const addBtn = document.getElementById('addBtn');
const logPanel = document.getElementById('logPanel');
let todoId = 4;
function log(message) {
const time = new Date().toLocaleTimeString();
const div = document.createElement('div');
div.textContent = `[${time}] ${message}`;
logPanel.appendChild(div);
logPanel.scrollTop = logPanel.scrollHeight;
}
function updateStats() {
const items = todoList.querySelectorAll('.todo-item');
const completed = todoList.querySelectorAll('.todo-item.completed');
document.getElementById('totalCount').textContent = items.length;
document.getElementById('completedCount').textContent = completed.length;
document.getElementById('activeCount').textContent = items.length - completed.length;
}
// **【代码注释】**见下方说明块
todoList.addEventListener('click', function(event) {
const target = event.target;
const todoItem = target.closest('.todo-item');
if (!todoItem) return;
// **【代码注释】**见下方说明块
if (target.classList.contains('todo-checkbox')) {
todoItem.classList.toggle('completed', target.checked);
log(`勾选状态: ${todoItem.querySelector('.todo-text').textContent} -> ${target.checked ? '完成' : '未完成'}`);
updateStats();
}
// **【代码注释】**见下方说明块
if (target.classList.contains('todo-delete')) {
const text = todoItem.querySelector('.todo-text').textContent;
todoItem.remove();
log(`删除: ${text}`);
updateStats();
}
});
// **【代码注释】**见下方说明块
function addTodo() {
const text = todoInput.value.trim();
if (!text) return;
const li = document.createElement('li');
li.className = 'todo-item';
li.dataset.id = todoId++;
li.innerHTML = `
<input type="checkbox" class="todo-checkbox">
<span class="todo-text">${text}</span>
<button class="todo-delete">??</button>
`;
todoList.appendChild(li);
todoInput.value = '';
log(`新增: ${text}`);
updateStats();
}
addBtn.addEventListener('click', addTodo);
todoInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') addTodo();
});
// **【代码注释】**见下方说明块
updateStats();
})();
</script>
</body>
</html>
【代码注释】
核心逻辑
- 见示例代码中的
addEventListener注册与事件对象常用属性。 - 在浏览器控制台查看输出,对照 MDN 文档理解触发顺序。
实战场景
- 将示例复制为独立
.html文件即可本地运行验证。
【本章小结】
是
?
用户点击子元素
event.target 匹配?
执行业务逻辑
??
父元素监听
【代码注释】
- 列表/表格:用
event.target+closest()定位行。 - 不冒泡事件(如
mouseenter、focus)无法委托,需直接绑定。
十二、DOM 对象原型链分析
12.1 DOM 节点与原型链
实例对象
例: div 元素
HTMLDivElement.prototype
HTMLElement.prototype
Element.prototype
Node.prototype
EventTarget.prototype
Object.prototype
核心逻辑
| 层级 | 主要能力 |
|---|---|
Object.prototype |
基础对象方法 toString(), valueOf() |
EventTarget.prototype |
事件监听 addEventListener(), removeEventListener(), dispatchEvent() |
Node.prototype |
节点树 appendChild(), cloneNode(), normalize() |
Element.prototype |
元素 API querySelector(), getAttribute(), classList |
HTMLElement.prototype |
HTML 元素通用 innerHTML, style, title |
HTMLDivElement.prototype |
div 专有(通常为空) |
12.2 常用 DOM 接口继承关系
事件对象
例: click
MouseEvent.prototype
UIEvent.prototype
Event.prototype
Object.prototype
12.3 HTMLCollection 与 NodeList 区别
| ?? | HTMLCollection | NodeList |
|---|---|---|
| 集合类型 | getElementsByTagName(), getElementsByClassName(), children | querySelectorAll(), getElementsByName(), childNodes |
| forEach | ❌ 不支持 | ✅ 支持 |
| length?? | ? ? | ? ? |
| item()?? | ? ? | ? ? |
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTMLCollection?NodeList???</title>
<style>
.collection-demo {
max-width: 800px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.demo-box {
padding: 40px;
background: #f5f5f5;
border-radius: 10px;
margin-bottom: 20px;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.comparison-table th,
.comparison-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.comparison-table th {
background: #667eea;
color: white;
}
.code-block {
background: #2d2d2d;
color: #00ff00;
padding: 15px;
border-radius: 5px;
font-family: monospace;
margin: 10px 0;
overflow-x: auto;
}
.test-section {
padding: 20px;
background: #e3f2fd;
border-radius: 10px;
margin-top: 20px;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
background: #667eea;
color: white;
cursor: pointer;
}
.btn:hover {
background: #764ba2;
}
</style>
</head>
<body>
<h1 style="text-align: center;">HTMLCollection ? NodeList ???</h1>
<div class="collection-demo">
<table class="comparison-table">
<thead>
<tr>
<th>??</th>
<th>HTMLCollection</th>
<th>NodeList</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>????</strong></td>
<td>getElementsByTagName()<br>getElementsByClassName()<br>children</td>
<td>querySelectorAll()<br>getElementsByName()<br>childNodes</td>
</tr>
<tr>
<td><strong>????</strong></td>
<td>?????</td>
<td>??????</td>
</tr>
<tr>
<td><strong>forEach</strong></td>
<td>? ???</td>
<td>? ??</td>
</tr>
<tr>
<td><strong>点我</strong></td>
<td>????</td>
<td>????</td>
</tr>
</tbody>
</table>
<div class="demo-box">
<h3>点击测试区</h3>
<div id="testArea">
<p>?? 1</p>
<p>?? 2</p>
<p>?? 3</p>
</div>
<div class="button-group">
<button class="btn" id="testHTMLCollection">?? HTMLCollection</button>
<button class="btn" id="testNodeList">?? NodeList</button>
<button class="btn" id="addParagraph">????</button>
<button class="btn" id="reset">??</button>
</div>
</div>
<div class="code-block" id="resultOutput">
滚动方向????...
</div>
</div>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function() {
const testArea = document.getElementById('testArea');
const resultOutput = document.getElementById('resultOutput');
function output(message) {
resultOutput.textContent = message;
}
// 【见下方代码注释】
document.getElementById('testHTMLCollection').addEventListener('click', function() {
const collection = testArea.getElementsByTagName('p');
let message = 'HTMLCollection (getElementsByTagName):\n';
message += `????: ${collection.length}\n`;
message += `??: ${Array.from(collection).map(p => p.textContent).join(', ')}\n\n`;
message += '?? forEach:\n';
try {
collection.forEach(item => message += item.textContent);
} catch (e) {
message += `??: ${e.message}\n`;
message += '????: Array.from(collection).forEach() ? for??';
}
output(message);
});
// 【见下方代码注释】
document.getElementById('testNodeList').addEventListener('click', function() {
const list = testArea.querySelectorAll('p');
let message = 'NodeList (querySelectorAll):\n';
message += `????: ${list.length}\n`;
message += `??: ${Array.from(list).map(p => p.textContent).join(', ')}\n\n`;
message += '?? forEach:\n';
try {
const items = [];
list.forEach(item => items.push(item.textContent));
message += `??! ??: ${items.join(', ')}`;
} catch (e) {
message += `??: ${e.message}`;
}
output(message);
});
// **【代码注释】**见下方说明块
document.getElementById('addParagraph').addEventListener('click', function() {
const newPara = document.createElement('p');
newPara.textContent = `?? ${testArea.children.length + 1}`;
testArea.appendChild(newPara);
});
// **【代码注释】**见下方说明块
document.getElementById('reset').addEventListener('click', function() {
testArea.innerHTML = `
<p>?? 1</p>
<p>?? 2</p>
<p>?? 3</p>
`;
output('???');
});
})();
</script>
</body>
</html>
【代码注释】
mousedown记录offsetX/offsetY,表示鼠标在元素内的点击偏移,避免拖动时元素「跳动」。mousemove绑定在document上,防止快速拖动时指针离开元素导致中断。- 使用
clientX/clientY配合偏移计算left/top,并做视口边界钳制。 - 生产环境可改用 HTML5 Drag and Drop API 或 Pointer Events 统一鼠标/触控。
十三、最佳实践与性能优化
13.1 性能优化要点
1. 事件委托 vs 逐项绑定
javascript
// ===== 完整可运行示例:复制整段到 .html 文件 =====
// 【见下方代码注释】
const items = document.querySelectorAll('.list-item');
items.forEach(item => {
item.addEventListener('click', handleClick);
});
// 【见下方代码注释】
const list = document.querySelector('.list');
list.addEventListener('click', function(event) {
if (event.target.matches('.list-item')) {
handleClick(event);
}
});
【代码注释】
click:完整的「按下 + 在同一元素内释放」才触发;若按下后拖出元素再释放,可能不触发click。dblclick:两次click间隔极短时触发;注意与两次单独click的交互设计(如「单击选中、双击打开」需防抖区分)。contextmenu:右键菜单;event.preventDefault()可阻止系统菜单,用于自定义右键面板(地图标注、表格行操作等)。
市面应用:GitHub 代码行号区右键菜单、Figma 画布右键、电商商品图「右键另存为」拦截。
2. passive 与 preventDefault
javascript
// ===== 完整可运行示例:复制整段到 .html 文件 =====
// 【见下方代码注释】
window.addEventListener('scroll', handleScroll, { passive: true });
// 【见下方代码注释】
element.addEventListener('touchmove', handleTouch, { passive: false });
【代码注释】
{ passive: true }告诉浏览器不会调用preventDefault(),滚动更流畅。- 需要阻止默认行为(如自定义横向滑动)时使用
{ passive: false }。
3. 防抖与节流
javascript
// ===== 完整可运行示例:复制整段到 .html 文件 =====
// **【代码注释】**见下方说明块
function debounce(func, wait) {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, arguments), wait);
};
}
// **【代码注释】**见下方说明块
function throttle(func, wait) {
let lastTime = 0;
return function() {
const now = Date.now();
if (now - lastTime >= wait) {
func.apply(this, arguments);
lastTime = now;
}
};
}
// **【代码注释】**见下方说明块
window.addEventListener('resize', debounce(handleResize, 200));
window.addEventListener('scroll', throttle(handleScroll, 100));
【代码注释】
- 说明 :
resize用 debounce,等待用户停止拖拽窗口后再重算布局。 - 说明 :
scroll用 throttle 或requestAnimationFrame,避免每帧多次执行。
4. 组件销毁时解绑
javascript
// ===== 完整可运行示例:复制整段到 .html 文件 =====
// **【代码注释】**见下方说明块
class MyComponent {
constructor() {
this.handleClick = this.handleClick.bind(this);
this.button.addEventListener('click', this.handleClick);
}
destroy() {
this.button.removeEventListener('click', this.handleClick);
}
handleClick(event) {
// **【代码注释】**见下方说明块
}
}
【代码注释】
click:完整的「按下 + 在同一元素内释放」才触发;若按下后拖出元素再释放,可能不触发click。dblclick:两次click间隔极短时触发;注意与两次单独click的交互设计(如「单击选中、双击打开」需防抖区分)。contextmenu:右键菜单;event.preventDefault()可阻止系统菜单,用于自定义右键面板(地图标注、表格行操作等)。
市面应用:GitHub 代码行号区右键菜单、Figma 画布右键、电商商品图「右键另存为」拦截。
13.2 防抖与节流
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件演示</title>
<style>
.performance-demo {
max-width: 1000px;
margin: 50px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.grid-container {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 5px;
margin: 20px 0;
}
.grid-item {
aspect-ratio: 1;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 5px;
cursor: pointer;
transition: transform 0.1s;
}
.grid-item:hover {
transform: scale(1.05);
}
.grid-item.clicked {
background: #ff6b6b;
}
.stats-panel {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin: 20px 0;
}
.stat-card {
padding: 20px;
background: #f5f5f5;
border-radius: 10px;
}
.stat-card h3 {
margin-top: 0;
color: #333;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #667eea;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin: 20px 0;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
background: #667eea;
color: white;
cursor: pointer;
}
.btn:hover {
background: #764ba2;
}
.btn.secondary {
background: #6c757d;
}
.btn.secondary:hover {
background: #5a6268;
}
.log-panel {
background: #2d2d2d;
color: #00ff00;
padding: 15px;
border-radius: 10px;
height: 150px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<h1 style="text-align: center;">示例页面</h1>
<div class="performance-demo">
<div class="controls">
<button class="btn" id="createDelegation">??1000?????????</button>
<button class="btn" id="createIndividual">??1000?????????</button>
<button class="btn" id="testDelegation">??????</button>
<button class="btn" id="testIndividual">??????</button>
<button class="btn secondary" id="clear">??</button>
</div>
<div class="stats-panel">
<div class="stat-card">
<h3>点击测试区</h3>
<div class="stat-value" id="elementCount">0</div>
</div>
<div class="stat-card">
<h3>事件时间线</h3>
<div class="stat-value" id="listenerCount">0</div>
</div>
<div class="stat-card">
<h3>点击测试区</h3>
<div class="stat-value" id="clickCount">0</div>
</div>
<div class="stat-card">
<h3>点击测试区</h3>
<div class="stat-value" id="executionTime">0ms</div>
</div>
</div>
<div class="grid-container" id="gridContainer"></div>
<div class="log-panel" id="logPanel">
<div>等待操作...</div>
</div>
</div>
<script>
// ===== 完整可运行示例:复制整段到 .html 文件 =====
(function() {
const gridContainer = document.getElementById('gridContainer');
const logPanel = document.getElementById('logPanel');
const elementCount = document.getElementById('elementCount');
const listenerCount = document.getElementById('listenerCount');
const clickCount = document.getElementById('clickCount');
const executionTime = document.getElementById('executionTime');
let clicks = 0;
let listeners = 0;
function log(message) {
const time = new Date().toLocaleTimeString();
const div = document.createElement('div');
div.textContent = `[${time}] ${message}`;
logPanel.appendChild(div);
logPanel.scrollTop = logPanel.scrollHeight;
}
function updateStats() {
elementCount.textContent = gridContainer.children.length;
listenerCount.textContent = listeners;
clickCount.textContent = clicks;
}
// **【代码注释】**见下方说明块
document.getElementById('createDelegation').addEventListener('click', function() {
clearGrid();
const start = performance.now();
for (let i = 0; i < 1000; i++) {
const item = document.createElement('div');
item.className = 'grid-item';
item.textContent = i + 1;
gridContainer.appendChild(item);
}
// **【代码注释】**见下方说明块
gridContainer.addEventListener('click', handleDelegationClick);
listeners = 1;
const end = performance.now();
executionTime.textContent = (end - start).toFixed(2) + 'ms';
updateStats();
log(`????????1000??????: ${(end - start).toFixed(2)}ms????: 1?`);
});
// **【代码注释】**见下方说明块
document.getElementById('createIndividual').addEventListener('click', function() {
clearGrid();
const start = performance.now();
for (let i = 0; i < 1000; i++) {
const item = document.createElement('div');
item.className = 'grid-item';
item.textContent = i + 1;
item.addEventListener('click', handleIndividualClick);
gridContainer.appendChild(item);
}
listeners = 1000;
const end = performance.now();
executionTime.textContent = (end - start).toFixed(2) + 'ms';
updateStats();
log(`????????1000??????: ${(end - start).toFixed(2)}ms????: 1000?`);
});
// **【代码注释】**见下方说明块
function handleDelegationClick(event) {
if (event.target.classList.contains('grid-item')) {
clicks++;
event.target.classList.toggle('clicked');
updateStats();
}
}
// **【代码注释】**见下方说明块
function handleIndividualClick(event) {
clicks++;
this.classList.toggle('clicked');
updateStats();
}
// **【代码注释】**见下方说明块
document.getElementById('testDelegation').addEventListener('click', function() {
const start = performance.now();
for (let i = 0; i < 100; i++) {
const randomIndex = Math.floor(Math.random() * gridContainer.children.length);
gridContainer.children[randomIndex].click();
}
const end = performance.now();
log(`???? - 100???????: ${(end - start).toFixed(2)}ms`);
});
document.getElementById('testIndividual').addEventListener('click', function() {
const start = performance.now();
for (let i = 0; i < 100; i++) {
const randomIndex = Math.floor(Math.random() * gridContainer.children.length);
gridContainer.children[randomIndex].click();
}
const end = performance.now();
log(`???? - 100???????: ${(end - start).toFixed(2)}ms`);
});
// **【代码注释】**见下方说明块
function clearGrid() {
gridContainer.innerHTML = '';
clicks = 0;
listeners = 0;
updateStats();
}
document.getElementById('clear').addEventListener('click', function() {
clearGrid();
log('???');
});
})();
</script>
</body>
</html>
【代码注释】
核心逻辑
- 见示例代码中的
addEventListener注册与事件对象常用属性。 - 在浏览器控制台查看输出,对照 MDN 文档理解触发顺序。
实战场景
- 将示例复制为独立
.html文件即可本地运行验证。
十四、附录:事件速查与面试要点
14.1 监听 API 速查
javascript
// 注册(推荐)
el.addEventListener('click', handler, { capture: false, once: false, passive: true, signal });
// 注销(须同一函数引用)
el.removeEventListener('click', handler);
// 主动派发
el.dispatchEvent(new CustomEvent('my-event', { detail: { id: 1 }, bubbles: true }));
options 字段 |
含义 |
|---|---|
capture |
在捕获阶段触发 |
once |
触发一次后自动移除 |
passive |
不会调用 preventDefault(滚动优化) |
signal |
传入 AbortController.signal,调用 abort() 批量解绑 |
14.2 表单事件:input vs change
用户输入
input
每次值变化即触发
change
input: 失焦后且值变
select: 选项一变即触发
14.3 文档就绪:load vs DOMContentLoaded
| DOMContentLoaded | load | |
|---|---|---|
| 触发时机 | HTML 解析完成 | 含图片、样式、iframe 等全部资源 |
| 典型用途 | 绑定 DOM、发起首屏请求 | 依赖尺寸的图表、统计像素 |
| 监听对象 | document / window |
window |
14.4 高频面试题归纳
- 事件委托原理? 冒泡 + 父级统一监听 +
event.target匹配。 target与currentTarget? 前者是事件源,后者是正在执行监听器的元素。- 如何阻止冒泡与默认行为?
stopPropagation()/stopImmediatePropagation();preventDefault()。 - 为什么滚动监听要 passive? 避免阻塞合成线程滚动;不能阻止默认滚动时不应同步
preventDefault。 mouseenter与mouseover? 前者不冒泡,子元素进出不会重复触发父级。
14.5 市面产品事件映射(技术向)
| 产品类型 | 典型事件组合 |
|---|---|
| 淘宝/京东商品页 | scroll 吸顶导航 + load/error 主图 + input 规格筛选 |
| 百度搜索框 | input/compositionend 联想 + keydown Enter 提交 |
| Notion / 飞书文档 | keydown 快捷键 + paste 剪贴板 |
| 网易云音乐播放器 | timeupdate(媒体)+ click 控件 |
| 微信 H5 分享页 | DOMContentLoaded 初始化 + touch/click 埋点 |
总结
知识点归纳(思维导图)
工程化
页面生命周期
设备事件
基础
表单与媒体
submit / input / change
img load / error
transition / animation
三种监听方式
事件流 捕获/目标/冒泡
this / target / currentTarget
鼠标 MouseEvent
键盘 KeyboardEvent
滚轮 wheel 标准
DOMContentLoaded
load
scroll / resize
事件委托
防抖节流
passive / once / AbortSignal
1. 事件监听方式
- HTML 属性:快速演示,避免生产使用
- DOM 属性:单监听器,易被覆盖
- addEventListener :多监听器、捕获/冒泡、
options扩展
2. 事件流三阶段
- 捕获 :
document→ 目标 - 目标:在目标元素执行
- 冒泡 :目标 →
document(委托依赖此阶段)
3. 常用事件分类(扩展版)
| 分类 | 主要事件 | 经典场景 |
|---|---|---|
| 鼠标 | click, dblclick, mousedown/up, mousemove, mouseenter/leave, wheel | 拖拽、缩放、悬停菜单 |
| 键盘 | keydown, keyup | 快捷键、游戏、表单校验 |
| 文档 | load, DOMContentLoaded | 入口脚本、性能打点 |
| 表单 | submit, reset, focus, blur, input, change, select | 登录、搜索、联动下拉 |
| 图片 | load, error | 预加载进度、占位图 |
| CSS | transition*, animation* | 骨架屏、轮播指示器 |
| 其他 | scroll, resize | 吸顶、懒加载、响应式布局 |
4. Event 对象核心
target/currentTarget/eventPhasepreventDefault()/stopPropagation()/stopImmediatePropagation()- 设备事件扩展:
MouseEvent.button、KeyboardEvent.key、WheelEvent.deltaY
5. 事件委托
- 动态列表、表格、聊天消息列表的标配模式
- 注意:不冒泡事件(如
mouseenter)无法委托,需直接绑定或使用mouseover并判断relatedTarget
6. 性能优化清单
- 委托减少监听器数量
scroll/resize/mousemove使用节流;搜索框input使用防抖removeEventListener或AbortController在组件卸载时清理- 滚动类监听优先
{ passive: true }
学习路径建议
掌握 addEventListener
理解事件流与 Event
分类练习 UI 事件
事件委托 + 性能
结合框架合成事件对比
- 实践优先:将文中每个 HTML 示例保存为单文件,在浏览器中逐段调试。
- 阅读规范 :以 MDN 事件索引为纲,遇到新 API 先查是否已废弃(如
keypress、keyCode)。 - 性能意识:在 DevTools Performance 中观察滚动、输入是否触发过长任务。
- 工程衔接 :理解原生事件后,再对比 React/Vue 中的事件绑定与修饰符(
.prevent、.stop)。
本篇为 JavaScript DOM 事件体系的完整技术博客:从监听注册、事件传播、各类 UI 事件到委托与性能优化,配套可运行示例与归纳总结,可作为日常开发与面试复习手册。