JavaScript|第30章:事件机制
事件机制
- JavaScript|第30章:事件机制
-
- 引言
- [30.1 浏览器事件简介与事件处理程序](#30.1 浏览器事件简介与事件处理程序)
-
- [30.1.1 什么是浏览器事件](#30.1.1 什么是浏览器事件)
- [30.1.2 事件处理程序的三种方式](#30.1.2 事件处理程序的三种方式)
-
- [1. HTML 属性(不推荐)](#1. HTML 属性(不推荐))
- [2. DOM 属性](#2. DOM 属性)
- [3. addEventListener(推荐)](#3. addEventListener(推荐))
- [30.1.3 addEventListener 详解](#30.1.3 addEventListener 详解)
- [30.1.4 removeEventListener](#30.1.4 removeEventListener)
- [30.1.5 Event 对象常用属性](#30.1.5 Event 对象常用属性)
- [30.2 事件模型:捕获 → 目标 → 冒泡](#30.2 事件模型:捕获 → 目标 → 冒泡)
-
- [30.2.1 事件流三个阶段](#30.2.1 事件流三个阶段)
- [30.2.2 事件冒泡](#30.2.2 事件冒泡)
- [30.2.3 停止冒泡](#30.2.3 停止冒泡)
- [30.2.4 事件捕获](#30.2.4 事件捕获)
- [30.2.5 event.target vs event.currentTarget](#30.2.5 event.target vs event.currentTarget)
- [30.3 事件委托与性能优化](#30.3 事件委托与性能优化)
-
- [30.3.1 事件委托原理](#30.3.1 事件委托原理)
- [30.3.2 事件委托实现](#30.3.2 事件委托实现)
- [30.3.3 事件委托的优缺点](#30.3.3 事件委托的优缺点)
- [30.3.4 closest 配合事件委托](#30.3.4 closest 配合事件委托)
- [30.3.5 标记元素:data-action 模式](#30.3.5 标记元素:data-action 模式)
- [30.4 浏览器默认行为与阻止方式](#30.4 浏览器默认行为与阻止方式)
-
- [30.4.1 常见默认行为](#30.4.1 常见默认行为)
- [30.4.2 preventDefault](#30.4.2 preventDefault)
- [30.4.3 检查默认行为是否被阻止](#30.4.3 检查默认行为是否被阻止)
- [30.4.4 stopPropagation vs preventDefault](#30.4.4 stopPropagation vs preventDefault)
- [30.4.5 passive 事件监听器](#30.4.5 passive 事件监听器)
- [30.5 自定义事件(CustomEvent、dispatchEvent)](#30.5 自定义事件(CustomEvent、dispatchEvent))
-
- [30.5.1 创建自定义事件](#30.5.1 创建自定义事件)
- [30.5.2 监听与派发自定义事件](#30.5.2 监听与派发自定义事件)
- [30.5.3 自定义事件应用:发布订阅模式](#30.5.3 自定义事件应用:发布订阅模式)
- [30.5.4 自定义事件与内置事件的对比](#30.5.4 自定义事件与内置事件的对比)
- [30.6 鼠标事件](#30.6 鼠标事件)
-
- [30.6.1 鼠标事件类型](#30.6.1 鼠标事件类型)
- [30.6.2 鼠标事件属性](#30.6.2 鼠标事件属性)
- [30.6.3 mouseover/mouseout vs mouseenter/mouseleave](#30.6.3 mouseover/mouseout vs mouseenter/mouseleave)
- [30.6.4 relatedTarget 属性](#30.6.4 relatedTarget 属性)
- [30.6.5 哪个鼠标按键被点击](#30.6.5 哪个鼠标按键被点击)
- [30.6.6 鼠标坐标与拖拽基础](#30.6.6 鼠标坐标与拖拽基础)
- [30.7 鼠标拖放事件(Drag and Drop API)](#30.7 鼠标拖放事件(Drag and Drop API))
-
- [30.7.1 HTML5 拖放 API 简介](#30.7.1 HTML5 拖放 API 简介)
- [30.7.2 基本拖放实现](#30.7.2 基本拖放实现)
- [30.7.3 dataTransfer 数据传递](#30.7.3 dataTransfer 数据传递)
- [30.7.4 拖放效果与文件上传](#30.7.4 拖放效果与文件上传)
- [30.8 指针事件(Pointer Events)](#30.8 指针事件(Pointer Events))
-
- [30.8.1 为什么需要指针事件](#30.8.1 为什么需要指针事件)
- [30.8.2 指针事件类型](#30.8.2 指针事件类型)
- [30.8.3 指针事件属性](#30.8.3 指针事件属性)
- [30.8.4 使用指针事件实现拖拽](#30.8.4 使用指针事件实现拖拽)
- [30.8.5 setPointerCapture 与多点触控](#30.8.5 setPointerCapture 与多点触控)
- [30.8.6 指针事件 vs 鼠标事件](#30.8.6 指针事件 vs 鼠标事件)
- [30.9 键盘事件(keydown 和 keyup)](#30.9 键盘事件(keydown 和 keyup))
-
- [30.9.1 键盘事件类型](#30.9.1 键盘事件类型)
- [30.9.2 key 与 code 的区别](#30.9.2 key 与 code 的区别)
- [30.9.3 修饰键](#30.9.3 修饰键)
- [30.9.4 长按重复触发](#30.9.4 长按重复触发)
- [30.9.5 实用:方向键导航](#30.9.5 实用:方向键导航)
- [30.10 滚动事件](#30.10 滚动事件)
-
- [30.10.1 scroll 事件](#30.10.1 scroll 事件)
- [30.10.2 滚动事件性能优化](#30.10.2 滚动事件性能优化)
- [30.10.3 滚动方向检测](#30.10.3 滚动方向检测)
- [30.10.4 程序化滚动控制](#30.10.4 程序化滚动控制)
- [30.10.5 wheel 事件](#30.10.5 wheel 事件)
- [30.11 实战应用](#30.11 实战应用)
-
- [30.11.1 拖拽排序组件](#30.11.1 拖拽排序组件)
- [30.11.2 全局快捷键系统](#30.11.2 全局快捷键系统)
- [30.11.3 综合示例:可交互的图片画廊](#30.11.3 综合示例:可交互的图片画廊)
- 本章小结
上篇回顾 :第29章:Document 与 DOM 操作 --- 我们学习了 DOM 树结构、节点类型与遍历、元素查询(getElement*/querySelector*)、节点增删改查、Attributes 与 Properties 的区别、样式与类操作、元素大小与滚动(offset/client/scroll)、坐标系统,以及实战应用。
下篇预告 :第31章:表单与控件 --- 我们将学习表单属性与方法、表单验证、focus/blur 事件、change/input 事件、表单提交,以及实战表单验证库。
引言
在上一章中,我们学习了如何通过 DOM API 操作网页------查询元素、增删节点、修改样式。但网页不只是静态展示,它需要响应用户的操作:点击按钮、输入文字、拖拽元素、按下键盘、滚动页面......
这一切都依赖于浏览器事件机制 。事件是浏览器或用户执行的某种动作的信号------当用户点击鼠标时,浏览器会产生一个 click 事件;当用户按下键盘时,产生 keydown 事件。JavaScript 通过注册事件监听器来响应这些事件,从而实现交互。
javascript
// 最常见的事件:点击
document.querySelector('#btn').addEventListener('click', () => {
console.log('按钮被点击了!');
});
// 键盘事件
document.addEventListener('keydown', (event) => {
console.log('按下了键:', event.key);
});
// 滚动事件
window.addEventListener('scroll', () => {
console.log('页面滚动了');
});
#mermaid-svg-kUsd8jS2wA5fPZGv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kUsd8jS2wA5fPZGv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kUsd8jS2wA5fPZGv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kUsd8jS2wA5fPZGv .error-icon{fill:#552222;}#mermaid-svg-kUsd8jS2wA5fPZGv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kUsd8jS2wA5fPZGv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kUsd8jS2wA5fPZGv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kUsd8jS2wA5fPZGv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kUsd8jS2wA5fPZGv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kUsd8jS2wA5fPZGv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kUsd8jS2wA5fPZGv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kUsd8jS2wA5fPZGv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kUsd8jS2wA5fPZGv .marker.cross{stroke:#333333;}#mermaid-svg-kUsd8jS2wA5fPZGv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kUsd8jS2wA5fPZGv p{margin:0;}#mermaid-svg-kUsd8jS2wA5fPZGv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kUsd8jS2wA5fPZGv .cluster-label text{fill:#333;}#mermaid-svg-kUsd8jS2wA5fPZGv .cluster-label span{color:#333;}#mermaid-svg-kUsd8jS2wA5fPZGv .cluster-label span p{background-color:transparent;}#mermaid-svg-kUsd8jS2wA5fPZGv .label text,#mermaid-svg-kUsd8jS2wA5fPZGv span{fill:#333;color:#333;}#mermaid-svg-kUsd8jS2wA5fPZGv .node rect,#mermaid-svg-kUsd8jS2wA5fPZGv .node circle,#mermaid-svg-kUsd8jS2wA5fPZGv .node ellipse,#mermaid-svg-kUsd8jS2wA5fPZGv .node polygon,#mermaid-svg-kUsd8jS2wA5fPZGv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kUsd8jS2wA5fPZGv .rough-node .label text,#mermaid-svg-kUsd8jS2wA5fPZGv .node .label text,#mermaid-svg-kUsd8jS2wA5fPZGv .image-shape .label,#mermaid-svg-kUsd8jS2wA5fPZGv .icon-shape .label{text-anchor:middle;}#mermaid-svg-kUsd8jS2wA5fPZGv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kUsd8jS2wA5fPZGv .rough-node .label,#mermaid-svg-kUsd8jS2wA5fPZGv .node .label,#mermaid-svg-kUsd8jS2wA5fPZGv .image-shape .label,#mermaid-svg-kUsd8jS2wA5fPZGv .icon-shape .label{text-align:center;}#mermaid-svg-kUsd8jS2wA5fPZGv .node.clickable{cursor:pointer;}#mermaid-svg-kUsd8jS2wA5fPZGv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kUsd8jS2wA5fPZGv .arrowheadPath{fill:#333333;}#mermaid-svg-kUsd8jS2wA5fPZGv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kUsd8jS2wA5fPZGv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kUsd8jS2wA5fPZGv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kUsd8jS2wA5fPZGv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kUsd8jS2wA5fPZGv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kUsd8jS2wA5fPZGv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kUsd8jS2wA5fPZGv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kUsd8jS2wA5fPZGv .cluster text{fill:#333;}#mermaid-svg-kUsd8jS2wA5fPZGv .cluster span{color:#333;}#mermaid-svg-kUsd8jS2wA5fPZGv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kUsd8jS2wA5fPZGv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kUsd8jS2wA5fPZGv rect.text{fill:none;stroke-width:0;}#mermaid-svg-kUsd8jS2wA5fPZGv .icon-shape,#mermaid-svg-kUsd8jS2wA5fPZGv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kUsd8jS2wA5fPZGv .icon-shape p,#mermaid-svg-kUsd8jS2wA5fPZGv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kUsd8jS2wA5fPZGv .icon-shape .label rect,#mermaid-svg-kUsd8jS2wA5fPZGv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kUsd8jS2wA5fPZGv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kUsd8jS2wA5fPZGv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kUsd8jS2wA5fPZGv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第30章:事件机制
事件处理程序
事件模型
事件委托
默认行为
自定义事件
鼠标事件
拖放事件
指针事件
键盘事件
滚动事件
实战应用
HTML 属性 / DOM 属性 / addEventListener
捕获 → 目标 → 冒泡
利用冒泡委托父元素处理
preventDefault / stopPropagation
CustomEvent / dispatchEvent
click / mouseover / mouseenter 等
dragstart / dragover / drop 等
pointerdown / pointermove / pointerup
keydown / keyup / key vs code
scroll / 防抖节流优化
拖拽排序 / 全局快捷键
本章目标:
- 理解浏览器事件的基本概念与三种事件处理程序(HTML 属性、DOM 属性、addEventListener)
- 掌握事件模型的三个阶段:捕获 → 目标 → 冒泡
- 熟练运用事件委托提升性能
- 掌握阻止默认行为与停止事件传播的方法
- 学会创建和派发自定义事件(CustomEvent、dispatchEvent)
- 掌握鼠标事件(click、contextmenu、mouseover/out、mouseenter/leave)
- 理解 HTML5 拖放 API(dragstart、dragover、drop)
- 掌握指针事件(Pointer Events)统一鼠标、触摸、手写笔
- 理解键盘事件(keydown、keyup、key 与 code 的区别)
- 掌握滚动事件与性能优化
- 实战:拖拽排序组件、全局快捷键系统
30.1 浏览器事件简介与事件处理程序
30.1.1 什么是浏览器事件
事件 (Event)是浏览器或用户执行的某种动作的信号。当事件发生时,浏览器会创建一个事件对象(Event Object),其中包含关于该事件的详细信息。
#mermaid-svg-JVAGyrxzDpo6kaag{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JVAGyrxzDpo6kaag .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JVAGyrxzDpo6kaag .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JVAGyrxzDpo6kaag .error-icon{fill:#552222;}#mermaid-svg-JVAGyrxzDpo6kaag .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JVAGyrxzDpo6kaag .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JVAGyrxzDpo6kaag .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JVAGyrxzDpo6kaag .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JVAGyrxzDpo6kaag .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JVAGyrxzDpo6kaag .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JVAGyrxzDpo6kaag .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JVAGyrxzDpo6kaag .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JVAGyrxzDpo6kaag .marker.cross{stroke:#333333;}#mermaid-svg-JVAGyrxzDpo6kaag svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JVAGyrxzDpo6kaag p{margin:0;}#mermaid-svg-JVAGyrxzDpo6kaag .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JVAGyrxzDpo6kaag .cluster-label text{fill:#333;}#mermaid-svg-JVAGyrxzDpo6kaag .cluster-label span{color:#333;}#mermaid-svg-JVAGyrxzDpo6kaag .cluster-label span p{background-color:transparent;}#mermaid-svg-JVAGyrxzDpo6kaag .label text,#mermaid-svg-JVAGyrxzDpo6kaag span{fill:#333;color:#333;}#mermaid-svg-JVAGyrxzDpo6kaag .node rect,#mermaid-svg-JVAGyrxzDpo6kaag .node circle,#mermaid-svg-JVAGyrxzDpo6kaag .node ellipse,#mermaid-svg-JVAGyrxzDpo6kaag .node polygon,#mermaid-svg-JVAGyrxzDpo6kaag .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JVAGyrxzDpo6kaag .rough-node .label text,#mermaid-svg-JVAGyrxzDpo6kaag .node .label text,#mermaid-svg-JVAGyrxzDpo6kaag .image-shape .label,#mermaid-svg-JVAGyrxzDpo6kaag .icon-shape .label{text-anchor:middle;}#mermaid-svg-JVAGyrxzDpo6kaag .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JVAGyrxzDpo6kaag .rough-node .label,#mermaid-svg-JVAGyrxzDpo6kaag .node .label,#mermaid-svg-JVAGyrxzDpo6kaag .image-shape .label,#mermaid-svg-JVAGyrxzDpo6kaag .icon-shape .label{text-align:center;}#mermaid-svg-JVAGyrxzDpo6kaag .node.clickable{cursor:pointer;}#mermaid-svg-JVAGyrxzDpo6kaag .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JVAGyrxzDpo6kaag .arrowheadPath{fill:#333333;}#mermaid-svg-JVAGyrxzDpo6kaag .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JVAGyrxzDpo6kaag .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JVAGyrxzDpo6kaag .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JVAGyrxzDpo6kaag .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JVAGyrxzDpo6kaag .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JVAGyrxzDpo6kaag .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JVAGyrxzDpo6kaag .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JVAGyrxzDpo6kaag .cluster text{fill:#333;}#mermaid-svg-JVAGyrxzDpo6kaag .cluster span{color:#333;}#mermaid-svg-JVAGyrxzDpo6kaag div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JVAGyrxzDpo6kaag .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JVAGyrxzDpo6kaag rect.text{fill:none;stroke-width:0;}#mermaid-svg-JVAGyrxzDpo6kaag .icon-shape,#mermaid-svg-JVAGyrxzDpo6kaag .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JVAGyrxzDpo6kaag .icon-shape p,#mermaid-svg-JVAGyrxzDpo6kaag .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JVAGyrxzDpo6kaag .icon-shape .label rect,#mermaid-svg-JVAGyrxzDpo6kaag .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JVAGyrxzDpo6kaag .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JVAGyrxzDpo6kaag .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JVAGyrxzDpo6kaag :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户操作/浏览器行为
产生事件
事件对象 Event
事件流传播
触发事件监听器
执行回调函数
常见的事件类型:
| 事件类别 | 事件名称 | 触发时机 |
|---|---|---|
| 鼠标事件 | click、dblclick、mousedown、mouseup |
鼠标点击/按下/释放 |
| 鼠标移动 | mousemove、mouseenter、mouseleave、mouseover、mouseout |
鼠标移动/进出元素 |
| 键盘事件 | keydown、keyup |
按下/释放键盘 |
| 表单事件 | submit、focus、blur、change、input |
表单交互 |
| 拖放事件 | dragstart、drag、dragend、drop |
拖拽操作 |
| 页面生命周期 | DOMContentLoaded、load、beforeunload、unload |
页面加载/卸载 |
| 资源事件 | error、abort、resize、scroll |
资源加载/窗口变化 |
| 剪贴板 | copy、cut、paste |
剪贴板操作 |
30.1.2 事件处理程序的三种方式
在 JavaScript 中,注册事件处理程序(也叫事件监听器)有三种方式:
1. HTML 属性(不推荐)
通过 HTML 标签的 on* 属性直接绑定事件处理代码:
html
<!-- 直接写 JS 代码(不推荐) -->
<button onclick="alert('Hello!')">点击我</button>
<!-- 调用全局函数 -->
<button onclick="sayHello()">点击我</button>
<script>
function sayHello() {
alert('Hello!');
}
</script>
javascript
// 可以通过 event 变量访问事件对象
// HTML: <button onclick="handleClick(event)">点击</button>
function handleClick(event) {
console.log('点击目标:', event.target);
console.log('事件类型:', event.type);
}
不推荐原因:
- HTML 与 JavaScript 耦合,不利于维护
- 事件处理函数必须是全局函数,污染全局命名空间
- 无法注册多个处理程序(后者覆盖前者)
- 存在 XSS 风险
2. DOM 属性
通过元素的 on* 属性赋值函数:
javascript
const btn = document.getElementById('myBtn');
// 赋值事件处理函数
btn.onclick = function(event) {
console.log('被点击了', event.type);
};
// 使用箭头函数
btn.onclick = (event) => {
console.log('被点击了', event.target);
};
// 移除事件处理
btn.onclick = null;
javascript
// ⚠️ 后赋值会覆盖前者
btn.onclick = function() { console.log('第一个'); };
btn.onclick = function() { console.log('第二个'); };
// 点击时只会输出 "第二个"(覆盖了第一个)
局限性:每个事件类型只能绑定一个处理函数,后者覆盖前者。
3. addEventListener(推荐)
使用 addEventListener 方法注册事件监听器,是最灵活、最推荐的方式:
javascript
const btn = document.getElementById('myBtn');
// 基本用法:addEventListener(事件类型, 回调函数)
btn.addEventListener('click', function(event) {
console.log('第一个监听器');
});
// 可以注册多个监听器(按注册顺序执行)
btn.addEventListener('click', function(event) {
console.log('第二个监听器');
});
// 点击时两个监听器都会执行,输出:
// "第一个监听器"
// "第二个监听器"
// 使用箭头函数
btn.addEventListener('click', (event) => {
console.log('点击目标:', event.target);
});
// 传入带 handleEvent 方法的对象
const handler = {
handleEvent(event) {
console.log('通过对象处理:', event.type);
}
};
btn.addEventListener('click', handler);
| 特性 | HTML 属性 | DOM 属性(on*) | addEventListener |
|---|---|---|---|
| 多个监听器 | ❌ | ❌ | ✅ |
| 移除方式 | 移除 HTML 属性 | el.onclick = null |
removeEventListener |
| 捕获/冒泡选择 | ❌ | ❌ | ✅(第三参数) |
| 事件委托 | ❌ | ✅ | ✅ |
| 推荐 | ❌ | 一般 | ✅ 最推荐 |
30.1.3 addEventListener 详解
javascript
const btn = document.getElementById('myBtn');
// 完整签名:addEventListener(type, listener, options)
// options 是一个对象(也可以是布尔值,表示 useCapture)
btn.addEventListener('click', handleClick, {
capture: false, // 是否在捕获阶段触发(默认 false,即冒泡阶段)
once: false, // 是否只触发一次后自动移除
passive: false // 是否为被动监听器(不调用 preventDefault)
});
// 第三参数也可以是布尔值(等价于 useCapture)
btn.addEventListener('click', handleClick, false); // 冒泡阶段
btn.addEventListener('click', handleClick, true); // 捕获阶段
// once:只执行一次
btn.addEventListener('click', () => {
console.log('只会执行一次');
}, { once: true });
// passive:声明不会调用 preventDefault,提升滚动性能
document.addEventListener('touchmove', handleTouch, { passive: true });
document.addEventListener('wheel', handleWheel, { passive: true });
#mermaid-svg-fjgWVB8J5if0Ae3T{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fjgWVB8J5if0Ae3T .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fjgWVB8J5if0Ae3T .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fjgWVB8J5if0Ae3T .error-icon{fill:#552222;}#mermaid-svg-fjgWVB8J5if0Ae3T .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fjgWVB8J5if0Ae3T .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fjgWVB8J5if0Ae3T .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fjgWVB8J5if0Ae3T .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fjgWVB8J5if0Ae3T .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fjgWVB8J5if0Ae3T .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fjgWVB8J5if0Ae3T .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fjgWVB8J5if0Ae3T .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fjgWVB8J5if0Ae3T .marker.cross{stroke:#333333;}#mermaid-svg-fjgWVB8J5if0Ae3T svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fjgWVB8J5if0Ae3T p{margin:0;}#mermaid-svg-fjgWVB8J5if0Ae3T .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fjgWVB8J5if0Ae3T .cluster-label text{fill:#333;}#mermaid-svg-fjgWVB8J5if0Ae3T .cluster-label span{color:#333;}#mermaid-svg-fjgWVB8J5if0Ae3T .cluster-label span p{background-color:transparent;}#mermaid-svg-fjgWVB8J5if0Ae3T .label text,#mermaid-svg-fjgWVB8J5if0Ae3T span{fill:#333;color:#333;}#mermaid-svg-fjgWVB8J5if0Ae3T .node rect,#mermaid-svg-fjgWVB8J5if0Ae3T .node circle,#mermaid-svg-fjgWVB8J5if0Ae3T .node ellipse,#mermaid-svg-fjgWVB8J5if0Ae3T .node polygon,#mermaid-svg-fjgWVB8J5if0Ae3T .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fjgWVB8J5if0Ae3T .rough-node .label text,#mermaid-svg-fjgWVB8J5if0Ae3T .node .label text,#mermaid-svg-fjgWVB8J5if0Ae3T .image-shape .label,#mermaid-svg-fjgWVB8J5if0Ae3T .icon-shape .label{text-anchor:middle;}#mermaid-svg-fjgWVB8J5if0Ae3T .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fjgWVB8J5if0Ae3T .rough-node .label,#mermaid-svg-fjgWVB8J5if0Ae3T .node .label,#mermaid-svg-fjgWVB8J5if0Ae3T .image-shape .label,#mermaid-svg-fjgWVB8J5if0Ae3T .icon-shape .label{text-align:center;}#mermaid-svg-fjgWVB8J5if0Ae3T .node.clickable{cursor:pointer;}#mermaid-svg-fjgWVB8J5if0Ae3T .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fjgWVB8J5if0Ae3T .arrowheadPath{fill:#333333;}#mermaid-svg-fjgWVB8J5if0Ae3T .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fjgWVB8J5if0Ae3T .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fjgWVB8J5if0Ae3T .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fjgWVB8J5if0Ae3T .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fjgWVB8J5if0Ae3T .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fjgWVB8J5if0Ae3T .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fjgWVB8J5if0Ae3T .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fjgWVB8J5if0Ae3T .cluster text{fill:#333;}#mermaid-svg-fjgWVB8J5if0Ae3T .cluster span{color:#333;}#mermaid-svg-fjgWVB8J5if0Ae3T div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fjgWVB8J5if0Ae3T .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fjgWVB8J5if0Ae3T rect.text{fill:none;stroke-width:0;}#mermaid-svg-fjgWVB8J5if0Ae3T .icon-shape,#mermaid-svg-fjgWVB8J5if0Ae3T .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fjgWVB8J5if0Ae3T .icon-shape p,#mermaid-svg-fjgWVB8J5if0Ae3T .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fjgWVB8J5if0Ae3T .icon-shape .label rect,#mermaid-svg-fjgWVB8J5if0Ae3T .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fjgWVB8J5if0Ae3T .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fjgWVB8J5if0Ae3T .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fjgWVB8J5if0Ae3T :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} addEventListener 第三参数
options 对象
capture: false(默认)
冒泡阶段触发
capture: true
捕获阶段触发
once: true
触发一次后自动移除
passive: true
不调用 preventDefault,优化滚动
30.1.4 removeEventListener
移除事件监听器时,必须传入与注册时相同的函数引用:
javascript
const btn = document.getElementById('myBtn');
// ✅ 正确:用具名函数,可以移除
function handleClick(event) {
console.log('被点击了');
}
btn.addEventListener('click', handleClick);
btn.removeEventListener('click', handleClick); // 成功移除
// ❌ 错误:匿名函数无法移除
btn.addEventListener('click', function(event) {
console.log('被点击了');
});
btn.removeEventListener('click', function(event) {
console.log('被点击了');
}); // 移除失败!两个匿名函数是不同的引用
// ❌ 错误:箭头函数同理
btn.addEventListener('click', () => console.log('click'));
btn.removeEventListener('click', () => console.log('click')); // 移除失败!
// ✅ 正确:使用对象引用
const handler = (event) => console.log('click');
btn.addEventListener('click', handler);
btn.removeEventListener('click', handler); // 成功移除
注意 :
removeEventListener的第三参数(capture/useCapture)必须与addEventListener一致。如果注册时用了capture: true,移除时也需要传true。
javascript
// 注册时使用捕获
btn.addEventListener('click', handleClick, true);
// 移除时也需要传 true
btn.removeEventListener('click', handleClick, true);
30.1.5 Event 对象常用属性
当事件触发时,回调函数会收到一个 Event 对象,包含事件的详细信息:
javascript
document.addEventListener('click', function(event) {
// 基本属性
console.log(event.type); // 事件类型,如 'click'
console.log(event.target); // 触发事件的原始元素(最深层的元素)
console.log(event.currentTarget); // 绑定事件监听器的元素(当前正在处理的元素)
console.log(event.eventPhase); // 事件阶段:1=捕获,2=目标,3=冒泡
console.log(event.bubbles); // 事件是否冒泡
console.log(event.cancelable); // 事件是否可以取消默认行为
console.log(event.timeStamp); // 事件创建的时间戳
console.log(event.isTrusted); // 是否由用户操作触发(true)还是脚本派发(false)
// 阻止行为
event.preventDefault(); // 阻止默认行为
event.stopPropagation(); // 停止事件冒泡
event.stopImmediatePropagation(); // 停止冒泡 + 阻止同元素上的其他监听器
});
| 属性/方法 | 说明 |
|---|---|
event.type |
事件类型字符串(如 'click'、'keydown') |
event.target |
触发事件的最深层元素(事件的实际目标) |
event.currentTarget |
当前正在处理事件的元素(绑定监听器的元素) |
event.eventPhase |
事件当前阶段(1=捕获,2=目标,3=冒泡) |
event.bubbles |
事件是否冒泡 |
event.cancelable |
事件是否可以 preventDefault |
event.isTrusted |
true = 用户触发,false = 脚本派发 |
event.preventDefault() |
阻止浏览器默认行为 |
event.stopPropagation() |
阻止事件继续冒泡/捕获 |
event.stopImmediatePropagation() |
阻止冒泡 + 阻止同元素其他监听器 |
30.2 事件模型:捕获 → 目标 → 冒泡
30.2.1 事件流三个阶段
当事件发生时,它会经历三个阶段的传播过程,这被称为事件流(Event Flow):
#mermaid-svg-j6LvMjhCkknlLQfB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-j6LvMjhCkknlLQfB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-j6LvMjhCkknlLQfB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-j6LvMjhCkknlLQfB .error-icon{fill:#552222;}#mermaid-svg-j6LvMjhCkknlLQfB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-j6LvMjhCkknlLQfB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-j6LvMjhCkknlLQfB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-j6LvMjhCkknlLQfB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-j6LvMjhCkknlLQfB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-j6LvMjhCkknlLQfB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-j6LvMjhCkknlLQfB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-j6LvMjhCkknlLQfB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-j6LvMjhCkknlLQfB .marker.cross{stroke:#333333;}#mermaid-svg-j6LvMjhCkknlLQfB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-j6LvMjhCkknlLQfB p{margin:0;}#mermaid-svg-j6LvMjhCkknlLQfB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-j6LvMjhCkknlLQfB .cluster-label text{fill:#333;}#mermaid-svg-j6LvMjhCkknlLQfB .cluster-label span{color:#333;}#mermaid-svg-j6LvMjhCkknlLQfB .cluster-label span p{background-color:transparent;}#mermaid-svg-j6LvMjhCkknlLQfB .label text,#mermaid-svg-j6LvMjhCkknlLQfB span{fill:#333;color:#333;}#mermaid-svg-j6LvMjhCkknlLQfB .node rect,#mermaid-svg-j6LvMjhCkknlLQfB .node circle,#mermaid-svg-j6LvMjhCkknlLQfB .node ellipse,#mermaid-svg-j6LvMjhCkknlLQfB .node polygon,#mermaid-svg-j6LvMjhCkknlLQfB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-j6LvMjhCkknlLQfB .rough-node .label text,#mermaid-svg-j6LvMjhCkknlLQfB .node .label text,#mermaid-svg-j6LvMjhCkknlLQfB .image-shape .label,#mermaid-svg-j6LvMjhCkknlLQfB .icon-shape .label{text-anchor:middle;}#mermaid-svg-j6LvMjhCkknlLQfB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-j6LvMjhCkknlLQfB .rough-node .label,#mermaid-svg-j6LvMjhCkknlLQfB .node .label,#mermaid-svg-j6LvMjhCkknlLQfB .image-shape .label,#mermaid-svg-j6LvMjhCkknlLQfB .icon-shape .label{text-align:center;}#mermaid-svg-j6LvMjhCkknlLQfB .node.clickable{cursor:pointer;}#mermaid-svg-j6LvMjhCkknlLQfB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-j6LvMjhCkknlLQfB .arrowheadPath{fill:#333333;}#mermaid-svg-j6LvMjhCkknlLQfB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-j6LvMjhCkknlLQfB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-j6LvMjhCkknlLQfB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j6LvMjhCkknlLQfB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-j6LvMjhCkknlLQfB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j6LvMjhCkknlLQfB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-j6LvMjhCkknlLQfB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-j6LvMjhCkknlLQfB .cluster text{fill:#333;}#mermaid-svg-j6LvMjhCkknlLQfB .cluster span{color:#333;}#mermaid-svg-j6LvMjhCkknlLQfB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-j6LvMjhCkknlLQfB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-j6LvMjhCkknlLQfB rect.text{fill:none;stroke-width:0;}#mermaid-svg-j6LvMjhCkknlLQfB .icon-shape,#mermaid-svg-j6LvMjhCkknlLQfB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-j6LvMjhCkknlLQfB .icon-shape p,#mermaid-svg-j6LvMjhCkknlLQfB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-j6LvMjhCkknlLQfB .icon-shape .label rect,#mermaid-svg-j6LvMjhCkknlLQfB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-j6LvMjhCkknlLQfB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-j6LvMjhCkknlLQfB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-j6LvMjhCkknlLQfB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段3:冒泡阶段 Bubbling
button#target
div#parent
body
html
document
window
阶段2:目标阶段 Target
button#target
事件到达目标元素
阶段1:捕获阶段 Capturing
window
document
html
body
div#parent
button#target
以如下 HTML 为例:
html
<form id="form">
<div id="parent">
<button id="btn">点击我</button>
</div>
</form>
当用户点击 button#btn 时,事件传播路径为:
#mermaid-svg-kFzyL6mfLproOU3X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kFzyL6mfLproOU3X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kFzyL6mfLproOU3X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kFzyL6mfLproOU3X .error-icon{fill:#552222;}#mermaid-svg-kFzyL6mfLproOU3X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kFzyL6mfLproOU3X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kFzyL6mfLproOU3X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kFzyL6mfLproOU3X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kFzyL6mfLproOU3X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kFzyL6mfLproOU3X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kFzyL6mfLproOU3X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kFzyL6mfLproOU3X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kFzyL6mfLproOU3X .marker.cross{stroke:#333333;}#mermaid-svg-kFzyL6mfLproOU3X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kFzyL6mfLproOU3X p{margin:0;}#mermaid-svg-kFzyL6mfLproOU3X .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kFzyL6mfLproOU3X .cluster-label text{fill:#333;}#mermaid-svg-kFzyL6mfLproOU3X .cluster-label span{color:#333;}#mermaid-svg-kFzyL6mfLproOU3X .cluster-label span p{background-color:transparent;}#mermaid-svg-kFzyL6mfLproOU3X .label text,#mermaid-svg-kFzyL6mfLproOU3X span{fill:#333;color:#333;}#mermaid-svg-kFzyL6mfLproOU3X .node rect,#mermaid-svg-kFzyL6mfLproOU3X .node circle,#mermaid-svg-kFzyL6mfLproOU3X .node ellipse,#mermaid-svg-kFzyL6mfLproOU3X .node polygon,#mermaid-svg-kFzyL6mfLproOU3X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kFzyL6mfLproOU3X .rough-node .label text,#mermaid-svg-kFzyL6mfLproOU3X .node .label text,#mermaid-svg-kFzyL6mfLproOU3X .image-shape .label,#mermaid-svg-kFzyL6mfLproOU3X .icon-shape .label{text-anchor:middle;}#mermaid-svg-kFzyL6mfLproOU3X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kFzyL6mfLproOU3X .rough-node .label,#mermaid-svg-kFzyL6mfLproOU3X .node .label,#mermaid-svg-kFzyL6mfLproOU3X .image-shape .label,#mermaid-svg-kFzyL6mfLproOU3X .icon-shape .label{text-align:center;}#mermaid-svg-kFzyL6mfLproOU3X .node.clickable{cursor:pointer;}#mermaid-svg-kFzyL6mfLproOU3X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kFzyL6mfLproOU3X .arrowheadPath{fill:#333333;}#mermaid-svg-kFzyL6mfLproOU3X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kFzyL6mfLproOU3X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kFzyL6mfLproOU3X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kFzyL6mfLproOU3X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kFzyL6mfLproOU3X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kFzyL6mfLproOU3X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kFzyL6mfLproOU3X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kFzyL6mfLproOU3X .cluster text{fill:#333;}#mermaid-svg-kFzyL6mfLproOU3X .cluster span{color:#333;}#mermaid-svg-kFzyL6mfLproOU3X div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kFzyL6mfLproOU3X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kFzyL6mfLproOU3X rect.text{fill:none;stroke-width:0;}#mermaid-svg-kFzyL6mfLproOU3X .icon-shape,#mermaid-svg-kFzyL6mfLproOU3X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kFzyL6mfLproOU3X .icon-shape p,#mermaid-svg-kFzyL6mfLproOU3X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kFzyL6mfLproOU3X .icon-shape .label rect,#mermaid-svg-kFzyL6mfLproOU3X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kFzyL6mfLproOU3X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kFzyL6mfLproOU3X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kFzyL6mfLproOU3X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 捕获阶段(从外到内)
③ 冒泡阶段(从内到外)
window
document
html
body
form#form
div#parent
button#btn(目标)
div#parent
form#form
body
html
document
window
javascript
// 验证事件流的三个阶段
const phases = {
1: 'CAPTURING_PHASE(捕获阶段)',
2: 'AT_TARGET(目标阶段)',
3: 'BUBBLING_PHASE(冒泡阶段)'
};
// 在每个元素上注册捕获和冒泡监听器
['window', 'document', 'documentElement', 'body', 'form', 'parent', 'btn'].forEach(id => {
const el = id === 'window' ? window : id === 'document' ? document : document.getElementById(id);
if (!el) return;
// 冒泡阶段(默认)
el.addEventListener('click', (e) => {
console.log(`[冒泡] ${id} - 阶段: ${phases[e.eventPhase]}`);
});
// 捕获阶段
el.addEventListener('click', (e) => {
console.log(`[捕获] ${id} - 阶段: ${phases[e.eventPhase]}`);
}, true);
});
// 点击 button#btn 后输出:
// [捕获] window - 阶段: CAPTURING_PHASE(捕获阶段)
// [捕获] document - 阶段: CAPTURING_PHASE(捕获阶段)
// [捕获] html - 阶段: CAPTURING_PHASE(捕获阶段)
// [捕获] body - 阶段: CAPTURING_PHASE(捕获阶段)
// [捕获] form - 阶段: CAPTURING_PHASE(捕获阶段)
// [捕获] parent - 阶段: CAPTURING_PHASE(捕获阶段)
// [冒泡] btn - 阶段: AT_TARGET(目标阶段)
// [捕获] btn - 阶段: AT_TARGET(目标阶段)
// [冒泡] parent - 阶段: BUBBLING_PHASE(冒泡阶段)
// [冒泡] form - 阶段: BUBBLING_PHASE(冒泡阶段)
// [冒泡] body - 阶段: BUBBLING_PHASE(冒泡阶段)
// [冒泡] html - 阶段: BUBBLING_PHASE(冒泡阶段)
// [冒泡] document - 阶段: BUBBLING_PHASE(冒泡阶段)
// [冒泡] window - 阶段: BUBBLING_PHASE(冒泡阶段)
注意 :在目标元素 上注册的监听器,无论
capture是true还是false,都会在目标阶段触发(eventPhase === 2)。捕获和冒泡的区分只在非目标元素(祖先元素)上才有意义。
30.2.2 事件冒泡
事件冒泡 (Event Bubbling)是指事件从目标元素开始,逐级向上传播到父元素,直到 document 和 window。
javascript
// HTML: <div id="outer"><div id="inner">点击</div></div>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
outer.addEventListener('click', () => {
console.log('outer 被触发了(冒泡)');
});
inner.addEventListener('click', () => {
console.log('inner 被触发了');
});
// 点击 inner 后输出:
// "inner 被触发了"
// "outer 被触发了(冒泡)"
#mermaid-svg-lGdHs1FlQZUdPJqj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lGdHs1FlQZUdPJqj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lGdHs1FlQZUdPJqj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lGdHs1FlQZUdPJqj .error-icon{fill:#552222;}#mermaid-svg-lGdHs1FlQZUdPJqj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lGdHs1FlQZUdPJqj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lGdHs1FlQZUdPJqj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lGdHs1FlQZUdPJqj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lGdHs1FlQZUdPJqj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lGdHs1FlQZUdPJqj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lGdHs1FlQZUdPJqj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lGdHs1FlQZUdPJqj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lGdHs1FlQZUdPJqj .marker.cross{stroke:#333333;}#mermaid-svg-lGdHs1FlQZUdPJqj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lGdHs1FlQZUdPJqj p{margin:0;}#mermaid-svg-lGdHs1FlQZUdPJqj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lGdHs1FlQZUdPJqj .cluster-label text{fill:#333;}#mermaid-svg-lGdHs1FlQZUdPJqj .cluster-label span{color:#333;}#mermaid-svg-lGdHs1FlQZUdPJqj .cluster-label span p{background-color:transparent;}#mermaid-svg-lGdHs1FlQZUdPJqj .label text,#mermaid-svg-lGdHs1FlQZUdPJqj span{fill:#333;color:#333;}#mermaid-svg-lGdHs1FlQZUdPJqj .node rect,#mermaid-svg-lGdHs1FlQZUdPJqj .node circle,#mermaid-svg-lGdHs1FlQZUdPJqj .node ellipse,#mermaid-svg-lGdHs1FlQZUdPJqj .node polygon,#mermaid-svg-lGdHs1FlQZUdPJqj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lGdHs1FlQZUdPJqj .rough-node .label text,#mermaid-svg-lGdHs1FlQZUdPJqj .node .label text,#mermaid-svg-lGdHs1FlQZUdPJqj .image-shape .label,#mermaid-svg-lGdHs1FlQZUdPJqj .icon-shape .label{text-anchor:middle;}#mermaid-svg-lGdHs1FlQZUdPJqj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lGdHs1FlQZUdPJqj .rough-node .label,#mermaid-svg-lGdHs1FlQZUdPJqj .node .label,#mermaid-svg-lGdHs1FlQZUdPJqj .image-shape .label,#mermaid-svg-lGdHs1FlQZUdPJqj .icon-shape .label{text-align:center;}#mermaid-svg-lGdHs1FlQZUdPJqj .node.clickable{cursor:pointer;}#mermaid-svg-lGdHs1FlQZUdPJqj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lGdHs1FlQZUdPJqj .arrowheadPath{fill:#333333;}#mermaid-svg-lGdHs1FlQZUdPJqj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lGdHs1FlQZUdPJqj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lGdHs1FlQZUdPJqj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lGdHs1FlQZUdPJqj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lGdHs1FlQZUdPJqj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lGdHs1FlQZUdPJqj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lGdHs1FlQZUdPJqj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lGdHs1FlQZUdPJqj .cluster text{fill:#333;}#mermaid-svg-lGdHs1FlQZUdPJqj .cluster span{color:#333;}#mermaid-svg-lGdHs1FlQZUdPJqj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lGdHs1FlQZUdPJqj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lGdHs1FlQZUdPJqj rect.text{fill:none;stroke-width:0;}#mermaid-svg-lGdHs1FlQZUdPJqj .icon-shape,#mermaid-svg-lGdHs1FlQZUdPJqj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lGdHs1FlQZUdPJqj .icon-shape p,#mermaid-svg-lGdHs1FlQZUdPJqj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lGdHs1FlQZUdPJqj .icon-shape .label rect,#mermaid-svg-lGdHs1FlQZUdPJqj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lGdHs1FlQZUdPJqj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lGdHs1FlQZUdPJqj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lGdHs1FlQZUdPJqj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} button(目标元素)
① 事件首先在这里触发
div#parent
② 冒泡到父元素
body
③ 继续冒泡
html
④ 继续冒泡
document
⑤ 继续冒泡
window
⑥ 最终到达
并非所有事件都冒泡 :大部分事件会冒泡,但有些事件不冒泡,如
focus、blur、mouseenter、mouseleave、load、error等。可以通过event.bubbles属性检查。
30.2.3 停止冒泡
使用 event.stopPropagation() 可以阻止事件继续冒泡:
javascript
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
inner.addEventListener('click', (event) => {
console.log('inner 被触发了');
event.stopPropagation(); // 阻止事件冒泡到 outer
});
outer.addEventListener('click', () => {
console.log('outer 被触发了');
});
// 点击 inner 后只输出:
// "inner 被触发了"
// outer 不会收到事件
stopImmediatePropagation() 不仅阻止冒泡,还阻止同一元素上后续注册的其他监听器执行:
javascript
const btn = document.getElementById('btn');
btn.addEventListener('click', (event) => {
console.log('第一个监听器');
event.stopImmediatePropagation(); // 阻止同元素其他监听器 + 阻止冒泡
});
btn.addEventListener('click', () => {
console.log('第二个监听器'); // 不会执行!
});
// 对比 stopPropagation()
btn.addEventListener('click', (event) => {
console.log('第一个监听器');
event.stopPropagation(); // 只阻止冒泡,不阻止同元素其他监听器
});
btn.addEventListener('click', () => {
console.log('第二个监听器'); // 会执行!
});
| 方法 | 阻止冒泡 | 阻止同元素其他监听器 |
|---|---|---|
stopPropagation() |
✅ | ❌ |
stopImmediatePropagation() |
✅ | ✅ |
最佳实践 :不要滥用
stopPropagation()。如果上层有事件委托或其他逻辑依赖事件冒泡,随意阻止冒泡会导致难以调试的 bug。只在确有必要时才使用。
30.2.4 事件捕获
事件捕获 (Event Capturing)是事件传播的第一个阶段,事件从 window 开始,逐级向下到达目标元素:
javascript
// 在捕获阶段监听(第三参数传 true)
document.getElementById('parent').addEventListener('click', (event) => {
console.log('parent 捕获阶段');
}, true); // ← true 表示在捕获阶段触发
document.getElementById('btn').addEventListener('click', (event) => {
console.log('btn 目标阶段');
});
// 点击 btn 后输出:
// "parent 捕获阶段" ← 先触发(捕获从外到内)
// "btn 目标阶段" ← 后触发(到达目标)
#mermaid-svg-GlgPZwQ6pkJfsOx4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .error-icon{fill:#552222;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .marker.cross{stroke:#333333;}#mermaid-svg-GlgPZwQ6pkJfsOx4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GlgPZwQ6pkJfsOx4 p{margin:0;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .cluster-label text{fill:#333;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .cluster-label span{color:#333;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .cluster-label span p{background-color:transparent;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .label text,#mermaid-svg-GlgPZwQ6pkJfsOx4 span{fill:#333;color:#333;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .node rect,#mermaid-svg-GlgPZwQ6pkJfsOx4 .node circle,#mermaid-svg-GlgPZwQ6pkJfsOx4 .node ellipse,#mermaid-svg-GlgPZwQ6pkJfsOx4 .node polygon,#mermaid-svg-GlgPZwQ6pkJfsOx4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .rough-node .label text,#mermaid-svg-GlgPZwQ6pkJfsOx4 .node .label text,#mermaid-svg-GlgPZwQ6pkJfsOx4 .image-shape .label,#mermaid-svg-GlgPZwQ6pkJfsOx4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .rough-node .label,#mermaid-svg-GlgPZwQ6pkJfsOx4 .node .label,#mermaid-svg-GlgPZwQ6pkJfsOx4 .image-shape .label,#mermaid-svg-GlgPZwQ6pkJfsOx4 .icon-shape .label{text-align:center;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .node.clickable{cursor:pointer;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .arrowheadPath{fill:#333333;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GlgPZwQ6pkJfsOx4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GlgPZwQ6pkJfsOx4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GlgPZwQ6pkJfsOx4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .cluster text{fill:#333;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .cluster span{color:#333;}#mermaid-svg-GlgPZwQ6pkJfsOx4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GlgPZwQ6pkJfsOx4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .icon-shape,#mermaid-svg-GlgPZwQ6pkJfsOx4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .icon-shape p,#mermaid-svg-GlgPZwQ6pkJfsOx4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .icon-shape .label rect,#mermaid-svg-GlgPZwQ6pkJfsOx4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GlgPZwQ6pkJfsOx4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GlgPZwQ6pkJfsOx4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GlgPZwQ6pkJfsOx4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 完整事件流
window
捕获①
document
捕获②
html
捕获③
body
捕获④
div#parent
捕获⑤
button#btn
目标⑥
div#parent
冒泡⑦
body
冒泡⑧
html
冒泡⑨
document
冒泡⑩
window
冒泡⑪
实际应用:事件捕获在开发中较少直接使用,主要用于需要在子元素处理事件之前先拦截的场景,如全局快捷键拦截、权限控制等。
30.2.5 event.target vs event.currentTarget
这是事件处理中最重要的区分:
javascript
// HTML: <div id="container"><button id="btn">按钮</button></div>
const container = document.getElementById('container');
container.addEventListener('click', (event) => {
console.log('target:', event.target.id); // 'btn'(实际被点击的元素)
console.log('currentTarget:', event.currentTarget.id); // 'container'(绑定监听器的元素)
});
// 点击 button 时:
// target: btn
// currentTarget: container
| 属性 | 说明 | 在事件传播中变化 |
|---|---|---|
event.target |
事件的实际目标(最深层触发元素) | ❌ 不变 |
event.currentTarget |
当前正在处理事件的元素(绑定监听器的元素) | ✅ 随传播阶段变化 |
javascript
// 实用:通过 target 判断实际点击的是哪个子元素
const list = document.getElementById('list');
list.addEventListener('click', (event) => {
// event.target 可能是 li、span、或 li 内的任何元素
const li = event.target.closest('li');
if (li) {
console.log('点击了列表项:', li.textContent);
}
});
30.3 事件委托与性能优化
30.3.1 事件委托原理
事件委托 (Event Delegation)是利用事件冒泡机制,将子元素的事件统一绑定在父元素上处理。这样无需为每个子元素单独绑定事件监听器。
#mermaid-svg-qTfBioOW5zTpDxzQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qTfBioOW5zTpDxzQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qTfBioOW5zTpDxzQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qTfBioOW5zTpDxzQ .error-icon{fill:#552222;}#mermaid-svg-qTfBioOW5zTpDxzQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qTfBioOW5zTpDxzQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qTfBioOW5zTpDxzQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qTfBioOW5zTpDxzQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qTfBioOW5zTpDxzQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qTfBioOW5zTpDxzQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qTfBioOW5zTpDxzQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qTfBioOW5zTpDxzQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qTfBioOW5zTpDxzQ .marker.cross{stroke:#333333;}#mermaid-svg-qTfBioOW5zTpDxzQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qTfBioOW5zTpDxzQ p{margin:0;}#mermaid-svg-qTfBioOW5zTpDxzQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qTfBioOW5zTpDxzQ .cluster-label text{fill:#333;}#mermaid-svg-qTfBioOW5zTpDxzQ .cluster-label span{color:#333;}#mermaid-svg-qTfBioOW5zTpDxzQ .cluster-label span p{background-color:transparent;}#mermaid-svg-qTfBioOW5zTpDxzQ .label text,#mermaid-svg-qTfBioOW5zTpDxzQ span{fill:#333;color:#333;}#mermaid-svg-qTfBioOW5zTpDxzQ .node rect,#mermaid-svg-qTfBioOW5zTpDxzQ .node circle,#mermaid-svg-qTfBioOW5zTpDxzQ .node ellipse,#mermaid-svg-qTfBioOW5zTpDxzQ .node polygon,#mermaid-svg-qTfBioOW5zTpDxzQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qTfBioOW5zTpDxzQ .rough-node .label text,#mermaid-svg-qTfBioOW5zTpDxzQ .node .label text,#mermaid-svg-qTfBioOW5zTpDxzQ .image-shape .label,#mermaid-svg-qTfBioOW5zTpDxzQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-qTfBioOW5zTpDxzQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qTfBioOW5zTpDxzQ .rough-node .label,#mermaid-svg-qTfBioOW5zTpDxzQ .node .label,#mermaid-svg-qTfBioOW5zTpDxzQ .image-shape .label,#mermaid-svg-qTfBioOW5zTpDxzQ .icon-shape .label{text-align:center;}#mermaid-svg-qTfBioOW5zTpDxzQ .node.clickable{cursor:pointer;}#mermaid-svg-qTfBioOW5zTpDxzQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qTfBioOW5zTpDxzQ .arrowheadPath{fill:#333333;}#mermaid-svg-qTfBioOW5zTpDxzQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qTfBioOW5zTpDxzQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qTfBioOW5zTpDxzQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qTfBioOW5zTpDxzQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qTfBioOW5zTpDxzQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qTfBioOW5zTpDxzQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qTfBioOW5zTpDxzQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qTfBioOW5zTpDxzQ .cluster text{fill:#333;}#mermaid-svg-qTfBioOW5zTpDxzQ .cluster span{color:#333;}#mermaid-svg-qTfBioOW5zTpDxzQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qTfBioOW5zTpDxzQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qTfBioOW5zTpDxzQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-qTfBioOW5zTpDxzQ .icon-shape,#mermaid-svg-qTfBioOW5zTpDxzQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qTfBioOW5zTpDxzQ .icon-shape p,#mermaid-svg-qTfBioOW5zTpDxzQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qTfBioOW5zTpDxzQ .icon-shape .label rect,#mermaid-svg-qTfBioOW5zTpDxzQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qTfBioOW5zTpDxzQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qTfBioOW5zTpDxzQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qTfBioOW5zTpDxzQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 使用事件委托
ul(监听 click)
li
li
li
li
只在 ul 绑定一个监听器
N 个元素 = 1 个监听器
不使用事件委托
ul
li(监听 click)
li(监听 click)
li(监听 click)
li(监听 click)
为每个 li 绑定监听器
N 个元素 = N 个监听器
30.3.2 事件委托实现
html
<!-- 一个动态列表,列表项可能动态增删 -->
<ul id="todo-list">
<li class="todo-item" data-id="1">学习 JavaScript <button class="delete">删除</button></li>
<li class="todo-item" data-id="2">学习 DOM <button class="delete">删除</button></li>
<li class="todo-item" data-id="3">学习事件 <button class="delete">删除</button></li>
</ul>
javascript
// ❌ 不使用事件委托:为每个 li 绑定监听器
document.querySelectorAll('.todo-item').forEach(item => {
item.addEventListener('click', (event) => {
console.log('点击了:', item.textContent);
});
});
// 问题:如果动态添加新的 li,需要手动再绑定一次
// ✅ 使用事件委托:在父元素 ul 上绑定一个监听器
const list = document.getElementById('todo-list');
list.addEventListener('click', (event) => {
// event.target 是实际被点击的元素
const deleteBtn = event.target.closest('.delete');
if (deleteBtn) {
// 点击了删除按钮
const item = deleteBtn.closest('.todo-item');
const id = item.dataset.id;
console.log(`删除 ID=${id} 的项目`);
item.remove();
return;
}
// 点击了列表项本身
const item = event.target.closest('.todo-item');
if (item) {
console.log('点击了项目:', item.textContent);
item.classList.toggle('completed');
}
});
// 动态添加新项目,无需额外绑定事件
const newItem = document.createElement('li');
newItem.className = 'todo-item';
newItem.dataset.id = '4';
newItem.innerHTML = '学习事件委托 <button class="delete">删除</button>';
list.appendChild(newItem);
// 新项目自动获得点击事件处理能力!
30.3.3 事件委托的优缺点
| 优点 | 说明 |
|---|---|
| 减少内存占用 | 只需一个监听器,而非 N 个 |
| 动态元素自动支持 | 新增子元素无需手动绑定事件 |
| 方便管理 | 修改逻辑只需改一处 |
| 方便卸载 | removeEventListener 只需一次 |
| 缺点 / 注意事项 | 说明 |
|---|---|
| 不适合不冒泡的事件 | focus、blur、mouseenter、mouseleave 等不冒泡,需用 focusin/focusout/mouseover/mouseout 替代 |
| 需要判断 target | 回调函数中需要通过 event.target 判断实际触发的元素 |
| 层级过深时有风险 | 如果嵌套很深,closest 查找可能有性能影响 |
最佳实践 :对于大量同类型子元素的事件处理,优先使用事件委托。结合
closest()方法可以优雅地找到目标元素。
30.3.4 closest 配合事件委托
element.closest(selector) 是事件委托的最佳搭档:
javascript
// 表格点击委托
document.getElementById('data-table').addEventListener('click', (event) => {
// 检查是否点击了编辑按钮
const editBtn = event.target.closest('.btn-edit');
if (editBtn) {
const row = editBtn.closest('tr');
const id = row.dataset.id;
editRow(id);
return;
}
// 检查是否点击了删除按钮
const deleteBtn = event.target.closest('.btn-delete');
if (deleteBtn) {
const row = deleteBtn.closest('tr');
const id = row.dataset.id;
deleteRow(id);
return;
}
// 检查是否点击了行(选中行)
const row = event.target.closest('tr');
if (row && row.parentElement.tagName === 'TBODY') {
selectRow(row.dataset.id);
}
});
function editRow(id) { console.log('编辑行:', id); }
function deleteRow(id) { console.log('删除行:', id); }
function selectRow(id) { console.log('选中行:', id); }
30.3.5 标记元素:data-action 模式
对于复杂的事件委托,可以使用 data-action 属性统一标记:
html
<div id="toolbar">
<button data-action="bold">加粗</button>
<button data-action="italic">斜体</button>
<button data-action="underline">下划线</button>
<button data-action="clear">清除格式</button>
</div>
javascript
const actions = {
bold() {
document.execCommand('bold');
console.log('加粗');
},
italic() {
document.execCommand('italic');
console.log('斜体');
},
underline() {
document.execCommand('underline');
console.log('下划线');
},
clear() {
document.execCommand('removeFormat');
console.log('清除格式');
}
};
// 统一委托
document.getElementById('toolbar').addEventListener('click', (event) => {
const btn = event.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
if (actions[action]) {
actions[action]();
}
});
30.4 浏览器默认行为与阻止方式
30.4.1 常见默认行为
浏览器为许多事件预设了默认行为,例如:
| 事件 | 默认行为 |
|---|---|
点击 <a href> |
跳转到链接地址 |
提交 <form> |
提交表单并刷新页面 |
| 点击右键 | 弹出上下文菜单 |
| 按下 Ctrl+S | 保存网页 |
| 输入框按键 | 在输入框中输入字符 |
| 选中文字拖动 | 选中文字 |
| 滚轮滚动 | 滚动页面 |
30.4.2 preventDefault
使用 event.preventDefault() 阻止浏览器的默认行为:
javascript
// 阻止链接跳转
document.querySelectorAll('a').forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault(); // 阻止默认跳转
console.log('链接被点击,但不跳转:', link.href);
});
});
// 阻止表单默认提交
document.getElementById('myForm').addEventListener('submit', (event) => {
event.preventDefault(); // 阻止页面刷新
console.log('表单数据:', new FormData(event.target));
});
// 阻止右键菜单
document.addEventListener('contextmenu', (event) => {
event.preventDefault();
console.log('自定义右键菜单');
});
// 阻止 Ctrl+S 保存网页
document.addEventListener('keydown', (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
console.log('自定义保存逻辑');
}
});
30.4.3 检查默认行为是否被阻止
javascript
document.getElementById('myLink').addEventListener('click', (event) => {
event.preventDefault();
console.log('默认行为已被阻止:', event.defaultPrevented); // true
});
// 也可以通过返回 false 阻止(仅 DOM 属性方式)
document.getElementById('myLink').onclick = function(event) {
event.preventDefault();
return false; // 在 jQuery 中会自动调用 stopPropagation,原生 DOM 中仅等价于 preventDefault
};
注意 :
addEventListener中return false不会 阻止默认行为,只有preventDefault()才会。
30.4.4 stopPropagation vs preventDefault
这两个方法经常被混淆,但它们的用途完全不同:
| 方法 | 作用 | 影响范围 |
|---|---|---|
preventDefault() |
阻止浏览器的默认行为 | 不影响事件传播 |
stopPropagation() |
阻止事件继续传播(冒泡/捕获) | 不影响默认行为 |
javascript
// 场景:点击链接,既要阻止跳转,又要阻止冒泡
document.getElementById('myLink').addEventListener('click', (event) => {
event.preventDefault(); // 阻止链接跳转
event.stopPropagation(); // 阻止事件冒泡到父元素
console.log('链接被点击,既不跳转也不冒泡');
});
30.4.5 passive 事件监听器
在移动端,touchmove 和 wheel 事件的监听器中如果调用了 preventDefault(),浏览器必须等待监听器执行完毕才能开始滚动,这会导致滚动卡顿。
passive: true 告诉浏览器这个监听器不会调用 preventDefault(),浏览器可以立即开始滚动,无需等待:
javascript
// ❌ 非 passive:浏览器需要等待监听器执行完毕才能滚动
document.addEventListener('touchmove', (event) => {
// 即使这里不调用 preventDefault,浏览器也要等待
console.log('touchmove');
});
// ✅ passive:浏览器可以并行滚动,无需等待
document.addEventListener('touchmove', (event) => {
console.log('touchmove');
}, { passive: true });
// ⚠️ passive 监听器中调用 preventDefault 会被忽略,并产生警告
document.addEventListener('touchmove', (event) => {
event.preventDefault(); // 无效!控制台会报警告 "Unable to preventDefault inside passive event listener"
}, { passive: true });
#mermaid-svg-yTuMnKOuq9BmPdH7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yTuMnKOuq9BmPdH7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yTuMnKOuq9BmPdH7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yTuMnKOuq9BmPdH7 .error-icon{fill:#552222;}#mermaid-svg-yTuMnKOuq9BmPdH7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yTuMnKOuq9BmPdH7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yTuMnKOuq9BmPdH7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yTuMnKOuq9BmPdH7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yTuMnKOuq9BmPdH7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yTuMnKOuq9BmPdH7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yTuMnKOuq9BmPdH7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yTuMnKOuq9BmPdH7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yTuMnKOuq9BmPdH7 .marker.cross{stroke:#333333;}#mermaid-svg-yTuMnKOuq9BmPdH7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yTuMnKOuq9BmPdH7 p{margin:0;}#mermaid-svg-yTuMnKOuq9BmPdH7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yTuMnKOuq9BmPdH7 .cluster-label text{fill:#333;}#mermaid-svg-yTuMnKOuq9BmPdH7 .cluster-label span{color:#333;}#mermaid-svg-yTuMnKOuq9BmPdH7 .cluster-label span p{background-color:transparent;}#mermaid-svg-yTuMnKOuq9BmPdH7 .label text,#mermaid-svg-yTuMnKOuq9BmPdH7 span{fill:#333;color:#333;}#mermaid-svg-yTuMnKOuq9BmPdH7 .node rect,#mermaid-svg-yTuMnKOuq9BmPdH7 .node circle,#mermaid-svg-yTuMnKOuq9BmPdH7 .node ellipse,#mermaid-svg-yTuMnKOuq9BmPdH7 .node polygon,#mermaid-svg-yTuMnKOuq9BmPdH7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yTuMnKOuq9BmPdH7 .rough-node .label text,#mermaid-svg-yTuMnKOuq9BmPdH7 .node .label text,#mermaid-svg-yTuMnKOuq9BmPdH7 .image-shape .label,#mermaid-svg-yTuMnKOuq9BmPdH7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-yTuMnKOuq9BmPdH7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yTuMnKOuq9BmPdH7 .rough-node .label,#mermaid-svg-yTuMnKOuq9BmPdH7 .node .label,#mermaid-svg-yTuMnKOuq9BmPdH7 .image-shape .label,#mermaid-svg-yTuMnKOuq9BmPdH7 .icon-shape .label{text-align:center;}#mermaid-svg-yTuMnKOuq9BmPdH7 .node.clickable{cursor:pointer;}#mermaid-svg-yTuMnKOuq9BmPdH7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yTuMnKOuq9BmPdH7 .arrowheadPath{fill:#333333;}#mermaid-svg-yTuMnKOuq9BmPdH7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yTuMnKOuq9BmPdH7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yTuMnKOuq9BmPdH7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yTuMnKOuq9BmPdH7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yTuMnKOuq9BmPdH7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yTuMnKOuq9BmPdH7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yTuMnKOuq9BmPdH7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yTuMnKOuq9BmPdH7 .cluster text{fill:#333;}#mermaid-svg-yTuMnKOuq9BmPdH7 .cluster span{color:#333;}#mermaid-svg-yTuMnKOuq9BmPdH7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yTuMnKOuq9BmPdH7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yTuMnKOuq9BmPdH7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-yTuMnKOuq9BmPdH7 .icon-shape,#mermaid-svg-yTuMnKOuq9BmPdH7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yTuMnKOuq9BmPdH7 .icon-shape p,#mermaid-svg-yTuMnKOuq9BmPdH7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yTuMnKOuq9BmPdH7 .icon-shape .label rect,#mermaid-svg-yTuMnKOuq9BmPdH7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yTuMnKOuq9BmPdH7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yTuMnKOuq9BmPdH7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yTuMnKOuq9BmPdH7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} passive: true
touchmove 事件
并行执行
立即滚动
非 passive(默认)
是
否
touchmove 事件
执行监听器
调用了
preventDefault?
不滚动
滚动
最佳实践 :对于
touchstart、touchmove、wheel等影响滚动的监听器,如果没有调用preventDefault的需求,都应该加上{ passive: true }。
30.5 自定义事件(CustomEvent、dispatchEvent)
30.5.1 创建自定义事件
除了浏览器内置的事件,我们可以创建自定义事件,用于组件间通信:
javascript
// 方式一:使用 Event 构造函数
const event1 = new Event('build');
element.dispatchEvent(event1);
// 方式二:使用 CustomEvent 构造函数(推荐,可携带数据)
const event2 = new CustomEvent('userLogin', {
detail: {
userId: 123,
userName: 'Alice',
timestamp: Date.now()
},
bubbles: true, // 是否冒泡(默认 false)
cancelable: true // 是否可取消(默认 false)
});
element.dispatchEvent(event2);
30.5.2 监听与派发自定义事件
javascript
const cart = document.getElementById('cart');
// 监听自定义事件
cart.addEventListener('itemAdded', (event) => {
console.log('商品已添加到购物车');
console.log('商品 ID:', event.detail.itemId);
console.log('商品名称:', event.detail.itemName);
console.log('数量:', event.detail.quantity);
});
// 派发自定义事件
function addToCart(itemId, itemName, quantity = 1) {
const event = new CustomEvent('itemAdded', {
detail: { itemId, itemName, quantity },
bubbles: true,
cancelable: true
});
cart.dispatchEvent(event);
}
// 触发
addToCart(101, 'JavaScript 高级编程', 2);
// 输出:
// 商品已添加到购物车
// 商品 ID:101
// 商品名称:JavaScript 高级编程
// 数量:2
30.5.3 自定义事件应用:发布订阅模式
javascript
/**
* 简易事件总线(发布订阅模式)
*/
class EventBus {
constructor() {
this._el = document.createElement('div');
}
/**
* 订阅事件
* @param {string} type - 事件类型
* @param {Function} handler - 事件处理函数
* @returns {Function} 取消订阅函数
*/
on(type, handler) {
this._el.addEventListener(type, handler);
return () => this.off(type, handler);
}
/**
* 订阅事件(只触发一次)
*/
once(type, handler) {
this._el.addEventListener(type, handler, { once: true });
return () => this.off(type, handler);
}
/**
* 取消订阅
*/
off(type, handler) {
this._el.removeEventListener(type, handler);
}
/**
* 发布事件
* @param {string} type - 事件类型
* @param {*} data - 要传递的数据
*/
emit(type, data = {}) {
const event = new CustomEvent(type, { detail: data });
this._el.dispatchEvent(event);
}
}
// 使用示例
const bus = new EventBus();
// 订阅
const unsubscribe = bus.on('notification', (event) => {
console.log('收到通知:', event.detail.message);
});
// 发布
bus.emit('notification', { message: '你好,世界!' });
// 输出:收到通知:你好,世界!
// 取消订阅
unsubscribe();
bus.emit('notification', { message: '这条不会被收到' }); // 无输出
30.5.4 自定义事件与内置事件的对比
| 特性 | 内置事件 | 自定义事件 |
|---|---|---|
| 创建方式 | 浏览器自动创建 | new Event() / new CustomEvent() |
| 派发方式 | 用户操作触发 | element.dispatchEvent() |
| 数据传递 | 通过 Event 子类属性 | 通过 event.detail |
isTrusted |
true |
false |
| 冒泡 | 部分冒泡 | 通过 bubbles 选项控制 |
javascript
// dispatchEvent 是同步执行的
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
console.log('2. 监听器执行');
});
console.log('1. 派发前');
btn.dispatchEvent(new Event('click'));
console.log('3. 派发后');
// 输出顺序:1 → 2 → 3(同步执行)
30.6 鼠标事件
30.6.1 鼠标事件类型
| 事件 | 触发时机 | 是否冒泡 |
|---|---|---|
mousedown |
鼠标按键按下 | ✅ |
mouseup |
鼠标按键释放 | ✅ |
click |
鼠标按下并释放(完整点击) | ✅ |
dblclick |
双击 | ✅ |
contextmenu |
右键点击(上下文菜单) | ✅ |
mouseover |
鼠标进入元素(冒泡) | ✅ |
mouseout |
鼠标离开元素(冒泡) | ✅ |
mouseenter |
鼠标进入元素(不冒泡) | ❌ |
mouseleave |
鼠标离开元素(不冒泡) | ❌ |
mousemove |
鼠标在元素上移动 | ✅ |
wheel |
鼠标滚轮滚动 | ✅ |
#mermaid-svg-rDobEKlWQnX6EBBG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rDobEKlWQnX6EBBG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rDobEKlWQnX6EBBG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rDobEKlWQnX6EBBG .error-icon{fill:#552222;}#mermaid-svg-rDobEKlWQnX6EBBG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rDobEKlWQnX6EBBG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rDobEKlWQnX6EBBG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rDobEKlWQnX6EBBG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rDobEKlWQnX6EBBG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rDobEKlWQnX6EBBG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rDobEKlWQnX6EBBG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rDobEKlWQnX6EBBG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rDobEKlWQnX6EBBG .marker.cross{stroke:#333333;}#mermaid-svg-rDobEKlWQnX6EBBG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rDobEKlWQnX6EBBG p{margin:0;}#mermaid-svg-rDobEKlWQnX6EBBG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rDobEKlWQnX6EBBG .cluster-label text{fill:#333;}#mermaid-svg-rDobEKlWQnX6EBBG .cluster-label span{color:#333;}#mermaid-svg-rDobEKlWQnX6EBBG .cluster-label span p{background-color:transparent;}#mermaid-svg-rDobEKlWQnX6EBBG .label text,#mermaid-svg-rDobEKlWQnX6EBBG span{fill:#333;color:#333;}#mermaid-svg-rDobEKlWQnX6EBBG .node rect,#mermaid-svg-rDobEKlWQnX6EBBG .node circle,#mermaid-svg-rDobEKlWQnX6EBBG .node ellipse,#mermaid-svg-rDobEKlWQnX6EBBG .node polygon,#mermaid-svg-rDobEKlWQnX6EBBG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rDobEKlWQnX6EBBG .rough-node .label text,#mermaid-svg-rDobEKlWQnX6EBBG .node .label text,#mermaid-svg-rDobEKlWQnX6EBBG .image-shape .label,#mermaid-svg-rDobEKlWQnX6EBBG .icon-shape .label{text-anchor:middle;}#mermaid-svg-rDobEKlWQnX6EBBG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rDobEKlWQnX6EBBG .rough-node .label,#mermaid-svg-rDobEKlWQnX6EBBG .node .label,#mermaid-svg-rDobEKlWQnX6EBBG .image-shape .label,#mermaid-svg-rDobEKlWQnX6EBBG .icon-shape .label{text-align:center;}#mermaid-svg-rDobEKlWQnX6EBBG .node.clickable{cursor:pointer;}#mermaid-svg-rDobEKlWQnX6EBBG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rDobEKlWQnX6EBBG .arrowheadPath{fill:#333333;}#mermaid-svg-rDobEKlWQnX6EBBG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rDobEKlWQnX6EBBG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rDobEKlWQnX6EBBG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rDobEKlWQnX6EBBG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rDobEKlWQnX6EBBG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rDobEKlWQnX6EBBG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rDobEKlWQnX6EBBG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rDobEKlWQnX6EBBG .cluster text{fill:#333;}#mermaid-svg-rDobEKlWQnX6EBBG .cluster span{color:#333;}#mermaid-svg-rDobEKlWQnX6EBBG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rDobEKlWQnX6EBBG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rDobEKlWQnX6EBBG rect.text{fill:none;stroke-width:0;}#mermaid-svg-rDobEKlWQnX6EBBG .icon-shape,#mermaid-svg-rDobEKlWQnX6EBBG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rDobEKlWQnX6EBBG .icon-shape p,#mermaid-svg-rDobEKlWQnX6EBBG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rDobEKlWQnX6EBBG .icon-shape .label rect,#mermaid-svg-rDobEKlWQnX6EBBG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rDobEKlWQnX6EBBG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rDobEKlWQnX6EBBG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rDobEKlWQnX6EBBG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 鼠标按下
mousedown
mouseup
click
dblclick(快速连续两次 click)
鼠标移动进入元素
mouseover(冒泡)
mouseenter(不冒泡)
鼠标移动离开元素
mouseout(冒泡)
mouseleave(不冒泡)
30.6.2 鼠标事件属性
javascript
document.addEventListener('click', (event) => {
// 鼠标坐标
console.log(event.clientX, event.clientY); // 相对视口(浏览器可见区域)
console.log(event.pageX, event.pageY); // 相对文档(含滚动偏移)
console.log(event.screenX, event.screenY); // 相对屏幕
// 哪个按键被按下
console.log(event.button); // 0=左键,1=中键,2=右键
console.log(event.buttons); // 位掩码:1=左键,2=右键,4=中键
// 修饰键状态
console.log(event.ctrlKey); // Ctrl 是否按下
console.log(event.shiftKey); // Shift 是否按下
console.log(event.altKey); // Alt 是否按下
console.log(event.metaKey); // Meta(Win键/Command键)是否按下
});
| 属性 | 说明 | 取值 |
|---|---|---|
clientX/Y |
相对视口的坐标 | 不含滚动偏移 |
pageX/Y |
相对文档的坐标 | 含滚动偏移 |
screenX/Y |
相对屏幕的坐标 | 物理屏幕坐标 |
offsetX/Y |
相对事件目标的坐标 | 相对 target 元素 |
button |
触发事件的按键 | 0=左键,1=中键,2=右键 |
buttons |
当前按下的按键组合 | 位掩码:1+2+4 |
ctrlKey |
Ctrl 键状态 | boolean |
shiftKey |
Shift 键状态 | boolean |
altKey |
Alt 键状态 | boolean |
metaKey |
Meta 键状态 | boolean |
30.6.3 mouseover/mouseout vs mouseenter/mouseleave
这两组事件都涉及鼠标进出元素,但行为有重要区别:
#mermaid-svg-JGbOA03alzKtMrnr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JGbOA03alzKtMrnr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JGbOA03alzKtMrnr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JGbOA03alzKtMrnr .error-icon{fill:#552222;}#mermaid-svg-JGbOA03alzKtMrnr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JGbOA03alzKtMrnr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JGbOA03alzKtMrnr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JGbOA03alzKtMrnr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JGbOA03alzKtMrnr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JGbOA03alzKtMrnr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JGbOA03alzKtMrnr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JGbOA03alzKtMrnr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JGbOA03alzKtMrnr .marker.cross{stroke:#333333;}#mermaid-svg-JGbOA03alzKtMrnr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JGbOA03alzKtMrnr p{margin:0;}#mermaid-svg-JGbOA03alzKtMrnr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JGbOA03alzKtMrnr .cluster-label text{fill:#333;}#mermaid-svg-JGbOA03alzKtMrnr .cluster-label span{color:#333;}#mermaid-svg-JGbOA03alzKtMrnr .cluster-label span p{background-color:transparent;}#mermaid-svg-JGbOA03alzKtMrnr .label text,#mermaid-svg-JGbOA03alzKtMrnr span{fill:#333;color:#333;}#mermaid-svg-JGbOA03alzKtMrnr .node rect,#mermaid-svg-JGbOA03alzKtMrnr .node circle,#mermaid-svg-JGbOA03alzKtMrnr .node ellipse,#mermaid-svg-JGbOA03alzKtMrnr .node polygon,#mermaid-svg-JGbOA03alzKtMrnr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JGbOA03alzKtMrnr .rough-node .label text,#mermaid-svg-JGbOA03alzKtMrnr .node .label text,#mermaid-svg-JGbOA03alzKtMrnr .image-shape .label,#mermaid-svg-JGbOA03alzKtMrnr .icon-shape .label{text-anchor:middle;}#mermaid-svg-JGbOA03alzKtMrnr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JGbOA03alzKtMrnr .rough-node .label,#mermaid-svg-JGbOA03alzKtMrnr .node .label,#mermaid-svg-JGbOA03alzKtMrnr .image-shape .label,#mermaid-svg-JGbOA03alzKtMrnr .icon-shape .label{text-align:center;}#mermaid-svg-JGbOA03alzKtMrnr .node.clickable{cursor:pointer;}#mermaid-svg-JGbOA03alzKtMrnr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JGbOA03alzKtMrnr .arrowheadPath{fill:#333333;}#mermaid-svg-JGbOA03alzKtMrnr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JGbOA03alzKtMrnr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JGbOA03alzKtMrnr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JGbOA03alzKtMrnr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JGbOA03alzKtMrnr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JGbOA03alzKtMrnr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JGbOA03alzKtMrnr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JGbOA03alzKtMrnr .cluster text{fill:#333;}#mermaid-svg-JGbOA03alzKtMrnr .cluster span{color:#333;}#mermaid-svg-JGbOA03alzKtMrnr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JGbOA03alzKtMrnr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JGbOA03alzKtMrnr rect.text{fill:none;stroke-width:0;}#mermaid-svg-JGbOA03alzKtMrnr .icon-shape,#mermaid-svg-JGbOA03alzKtMrnr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JGbOA03alzKtMrnr .icon-shape p,#mermaid-svg-JGbOA03alzKtMrnr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JGbOA03alzKtMrnr .icon-shape .label rect,#mermaid-svg-JGbOA03alzKtMrnr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JGbOA03alzKtMrnr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JGbOA03alzKtMrnr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JGbOA03alzKtMrnr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} mouseenter / mouseleave(不冒泡)
div#parent
span#child
鼠标从 parent 进入 child
不触发任何事件
(parent 的 mouseenter 不会重复触发)
mouseover / mouseout(冒泡)
div#parent
span#child
鼠标从 parent 进入 child
触发:parent.mouseout → child.mouseover
(因为冒泡,parent 也会收到 mouseover)
html
<div id="parent" style="padding: 50px; background: #eee;">
父元素
<div id="child" style="padding: 30px; background: #ddd;">
子元素
</div>
</div>
javascript
const parent = document.getElementById('parent');
const child = document.getElementById('child');
// mouseover/mouseout:冒泡,进出子元素也会触发
parent.addEventListener('mouseover', (e) => {
console.log('mouseover:', e.target.id, '→ from:', e.relatedTarget?.tagName);
});
parent.addEventListener('mouseout', (e) => {
console.log('mouseout:', e.target.id, '→ to:', e.relatedTarget?.tagName);
});
// 鼠标从 parent 外部进入 parent → 输出:mouseover: parent
// 鼠标从 parent 进入 child → 输出:mouseout: parent,然后 mouseover: child
// (因为冒泡,parent 也会收到 mouseover 事件)
// mouseenter/mouseleave:不冒泡,只在真正进入/离开元素时触发
parent.addEventListener('mouseenter', (e) => {
console.log('mouseenter: 进入 parent');
});
parent.addEventListener('mouseleave', (e) => {
console.log('mouseleave: 离开 parent');
});
// 鼠标从 parent 外部进入 parent → 输出:mouseenter: 进入 parent
// 鼠标从 parent 进入 child → 不输出(child 仍在 parent 内部)
// 鼠标从 child 离开到 parent 外 → 输出:mouseleave: 离开 parent
| 对比项 | mouseover / mouseout |
mouseenter / mouseleave |
|---|---|---|
| 是否冒泡 | ✅ 冒泡 | ❌ 不冒泡 |
| 子元素进出时触发 | ✅ 每次进出子元素都触发 | ❌ 只在进出元素本身时触发 |
relatedTarget |
✅ 有(上一个/下一个目标) | ✅ 有 |
| 事件委托 | ✅ 适合(冒泡) | ❌ 不适合(不冒泡) |
| 典型用途 | 事件委托场景 | hover 效果、 Tooltip 显示/隐藏 |
30.6.4 relatedTarget 属性
mouseover/mouseout 事件有 relatedTarget 属性,指向鼠标来自 或去往的元素:
javascript
// mouseover:relatedTarget 是鼠标来自的元素
// mouseout:relatedTarget 是鼠标去往的元素
parent.addEventListener('mouseout', (event) => {
console.log('鼠标离开了', event.target.tagName);
console.log('去往了', event.relatedTarget?.tagName || 'null');
});
// 实用:判断鼠标是否真的离开了 parent(而不是进入了 child)
parent.addEventListener('mouseleave', (event) => {
// mouseleave 只在真正离开 parent 时触发,无需判断
console.log('真正离开了 parent');
});
30.6.5 哪个鼠标按键被点击
mousedown 和 mouseup 事件的 button 属性标识按下的按键:
javascript
document.addEventListener('mousedown', (event) => {
switch (event.button) {
case 0:
console.log('左键按下');
break;
case 1:
console.log('中键按下');
break;
case 2:
console.log('右键按下');
break;
}
});
// 注意:click 事件只在左键点击时触发
// 右键点击触发的是 contextmenu 事件
document.addEventListener('contextmenu', (event) => {
event.preventDefault(); // 阻止默认右键菜单
console.log('右键点击了:', event.target.tagName);
});
30.6.6 鼠标坐标与拖拽基础
javascript
// 实现一个可拖拽的元素(使用 mousedown/mousemove/mouseup)
function makeDraggable(element) {
let isDragging = false;
let startX = 0, startY = 0;
let initLeft = 0, initTop = 0;
element.addEventListener('mousedown', (event) => {
if (event.button !== 0) return; // 只响应左键
isDragging = true;
startX = event.clientX;
startY = event.clientY;
const rect = element.getBoundingClientRect();
initLeft = rect.left;
initTop = rect.top;
element.style.position = 'fixed';
element.style.zIndex = 1000;
event.preventDefault(); // 防止选中文本
});
// 监听在 document 上,这样鼠标移出元素也能继续拖拽
document.addEventListener('mousemove', (event) => {
if (!isDragging) return;
const dx = event.clientX - startX;
const dy = event.clientY - startY;
element.style.left = initLeft + dx + 'px';
element.style.top = initTop + dy + 'px';
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
}
// 使用
makeDraggable(document.getElementById('draggable-box'));
关键点 :
mousemove和mouseup应该绑定在document上,而不是元素本身,这样鼠标移出元素时仍能继续追踪拖拽。
30.7 鼠标拖放事件(Drag and Drop API)
30.7.1 HTML5 拖放 API 简介
HTML5 提供了一套标准的拖放 API,通过 draggable 属性和一组拖放事件实现:
#mermaid-svg-II0iW6whNQsVzu3M{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-II0iW6whNQsVzu3M .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-II0iW6whNQsVzu3M .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-II0iW6whNQsVzu3M .error-icon{fill:#552222;}#mermaid-svg-II0iW6whNQsVzu3M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-II0iW6whNQsVzu3M .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-II0iW6whNQsVzu3M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-II0iW6whNQsVzu3M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-II0iW6whNQsVzu3M .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-II0iW6whNQsVzu3M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-II0iW6whNQsVzu3M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-II0iW6whNQsVzu3M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-II0iW6whNQsVzu3M .marker.cross{stroke:#333333;}#mermaid-svg-II0iW6whNQsVzu3M svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-II0iW6whNQsVzu3M p{margin:0;}#mermaid-svg-II0iW6whNQsVzu3M .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-II0iW6whNQsVzu3M .cluster-label text{fill:#333;}#mermaid-svg-II0iW6whNQsVzu3M .cluster-label span{color:#333;}#mermaid-svg-II0iW6whNQsVzu3M .cluster-label span p{background-color:transparent;}#mermaid-svg-II0iW6whNQsVzu3M .label text,#mermaid-svg-II0iW6whNQsVzu3M span{fill:#333;color:#333;}#mermaid-svg-II0iW6whNQsVzu3M .node rect,#mermaid-svg-II0iW6whNQsVzu3M .node circle,#mermaid-svg-II0iW6whNQsVzu3M .node ellipse,#mermaid-svg-II0iW6whNQsVzu3M .node polygon,#mermaid-svg-II0iW6whNQsVzu3M .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-II0iW6whNQsVzu3M .rough-node .label text,#mermaid-svg-II0iW6whNQsVzu3M .node .label text,#mermaid-svg-II0iW6whNQsVzu3M .image-shape .label,#mermaid-svg-II0iW6whNQsVzu3M .icon-shape .label{text-anchor:middle;}#mermaid-svg-II0iW6whNQsVzu3M .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-II0iW6whNQsVzu3M .rough-node .label,#mermaid-svg-II0iW6whNQsVzu3M .node .label,#mermaid-svg-II0iW6whNQsVzu3M .image-shape .label,#mermaid-svg-II0iW6whNQsVzu3M .icon-shape .label{text-align:center;}#mermaid-svg-II0iW6whNQsVzu3M .node.clickable{cursor:pointer;}#mermaid-svg-II0iW6whNQsVzu3M .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-II0iW6whNQsVzu3M .arrowheadPath{fill:#333333;}#mermaid-svg-II0iW6whNQsVzu3M .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-II0iW6whNQsVzu3M .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-II0iW6whNQsVzu3M .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-II0iW6whNQsVzu3M .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-II0iW6whNQsVzu3M .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-II0iW6whNQsVzu3M .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-II0iW6whNQsVzu3M .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-II0iW6whNQsVzu3M .cluster text{fill:#333;}#mermaid-svg-II0iW6whNQsVzu3M .cluster span{color:#333;}#mermaid-svg-II0iW6whNQsVzu3M div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-II0iW6whNQsVzu3M .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-II0iW6whNQsVzu3M rect.text{fill:none;stroke-width:0;}#mermaid-svg-II0iW6whNQsVzu3M .icon-shape,#mermaid-svg-II0iW6whNQsVzu3M .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-II0iW6whNQsVzu3M .icon-shape p,#mermaid-svg-II0iW6whNQsVzu3M .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-II0iW6whNQsVzu3M .icon-shape .label rect,#mermaid-svg-II0iW6whNQsVzu3M .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-II0iW6whNQsVzu3M .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-II0iW6whNQsVzu3M .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-II0iW6whNQsVzu3M :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} dragstart
开始拖拽
drag
拖拽中(持续触发)
dragend
拖拽结束
dragenter
拖入放置区域
dragover
在放置区域内移动
dragleave
拖出放置区域
drop
释放到放置区域
| 事件 | 触发对象 | 说明 |
|---|---|---|
dragstart |
被拖拽元素 | 开始拖拽时触发 |
drag |
被拖拽元素 | 拖拽过程中持续触发 |
dragend |
被拖拽元素 | 拖拽结束时触发 |
dragenter |
放置目标 | 拖拽进入放置目标时触发 |
dragover |
放置目标 | 拖拽在放置目标内移动时持续触发 |
dragleave |
放置目标 | 拖拽离开放置目标时触发 |
drop |
放置目标 | 在放置目标上释放时触发 |
30.7.2 基本拖放实现
html
<div id="item" draggable="true" style="width:100px;height:100px;background:cyan;">
拖拽我
</div>
<div id="dropzone" style="width:200px;height:200px;border:2px dashed #ccc;margin-top:20px;">
放置区域
</div>
javascript
const item = document.getElementById('item');
const dropzone = document.getElementById('dropzone');
// 1. 被拖拽元素的事件
item.addEventListener('dragstart', (event) => {
console.log('开始拖拽');
// 通过 dataTransfer 传递数据
event.dataTransfer.setData('text/plain', item.id);
event.dataTransfer.effectAllowed = 'move';
// 可选:设置拖拽时的预览图像
// event.dataTransfer.setDragImage(previewElement, offsetX, offsetY);
});
item.addEventListener('drag', (event) => {
// 持续触发,通常不需要做太多处理
});
item.addEventListener('dragend', (event) => {
console.log('拖拽结束');
});
// 2. 放置目标的事件
dropzone.addEventListener('dragenter', (event) => {
event.preventDefault(); // 必须阻止默认行为才能触发 drop
dropzone.style.borderColor = 'green';
dropzone.style.backgroundColor = '#e8f5e9';
});
// ⚠️ 关键:必须阻止 dragover 的默认行为,否则 drop 事件不会触发!
dropzone.addEventListener('dragover', (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
});
dropzone.addEventListener('dragleave', (event) => {
dropzone.style.borderColor = '#ccc';
dropzone.style.backgroundColor = '';
});
dropzone.addEventListener('drop', (event) => {
event.preventDefault(); // 阻止默认行为(如打开链接)
// 获取拖拽数据
const itemId = event.dataTransfer.getData('text/plain');
const draggedElement = document.getElementById(itemId);
// 将元素移动到放置区域
if (draggedElement) {
dropzone.appendChild(draggedElement);
}
dropzone.style.borderColor = '#ccc';
dropzone.style.backgroundColor = '';
console.log('放置完成');
});
关键点 :
dragover事件中必须 调用event.preventDefault(),否则drop事件不会触发。这是拖放 API 最常见的坑。
30.7.3 dataTransfer 数据传递
dataTransfer 对象用于在拖拽源和放置目标之间传递数据:
javascript
// 拖拽源:设置数据
item.addEventListener('dragstart', (event) => {
// 支持多种 MIME 类型
event.dataTransfer.setData('text/plain', '纯文本数据');
event.dataTransfer.setData('text/html', '<b>HTML 数据</b>');
event.dataTransfer.setData('text/uri-list', 'https://example.com');
// 自定义 MIME 类型
event.dataTransfer.setData('application/x-my-data', JSON.stringify({
id: 123,
type: 'task'
}));
// 设置拖拽效果
event.dataTransfer.effectAllowed = 'copyMove'; // none | copy | move | link | copyMove | copyLink | linkMove | all
});
// 放置目标:获取数据
dropzone.addEventListener('drop', (event) => {
event.preventDefault();
const text = event.dataTransfer.getData('text/plain');
const html = event.dataTransfer.getData('text/html');
const url = event.dataTransfer.getData('text/uri-list');
const myData = JSON.parse(event.dataTransfer.getData('application/x-my-data') || '{}');
console.log('文本:', text);
console.log('HTML:', html);
console.log('URL:', url);
console.log('自定义数据:', myData);
});
30.7.4 拖放效果与文件上传
拖放 API 最常见的应用之一是文件拖拽上传:
javascript
const dropArea = document.getElementById('drop-area');
dropArea.addEventListener('dragover', (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
dropArea.classList.add('dragover');
});
dropArea.addEventListener('dragleave', () => {
dropArea.classList.remove('dragover');
});
dropArea.addEventListener('drop', (event) => {
event.preventDefault();
dropArea.classList.remove('dragover');
// 获取拖放的文件
const files = event.dataTransfer.files;
console.log(`共拖放了 ${files.length} 个文件`);
for (const file of files) {
console.log(`文件名:${file.name}`);
console.log(`大小:${file.size} bytes`);
console.log(`类型:${file.type}`);
console.log('---');
}
// 上传文件
uploadFiles(files);
});
function uploadFiles(files) {
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
fetch('/api/upload', {
method: 'POST',
body: formData
}).then(response => {
console.log('上传成功');
}).catch(error => {
console.error('上传失败:', error);
});
}
30.8 指针事件(Pointer Events)
30.8.1 为什么需要指针事件
在移动设备上,除了鼠标还有触摸屏和手写笔。过去开发者需要同时处理鼠标事件(mousedown、mousemove、mouseup)和触摸事件(touchstart、touchmove、touchend),代码复杂且容易出错。
指针事件 (Pointer Events)是一套统一的 API,将鼠标、触摸、手写笔等所有输入设备统一为"指针"概念:
| 事件类型 | 鼠标事件 | 触摸事件 | 指针事件 |
|---|---|---|---|
| 按下 | mousedown |
touchstart |
pointerdown |
| 移动 | mousemove |
touchmove |
pointermove |
| 释放 | mouseup |
touchend |
pointerup |
| 进入 | mouseover/mouseenter |
--- | pointerover/pointerenter |
| 离开 | mouseout/mouseleave |
--- | pointerout/pointerleave |
| 取消 | --- | touchcancel |
pointercancel |
30.8.2 指针事件类型
| 事件 | 说明 | 是否冒泡 |
|---|---|---|
pointerdown |
指针按下 | ✅ |
pointermove |
指针移动 | ✅ |
pointerup |
指针释放 | ✅ |
pointercancel |
指针被中断(如来电) | ✅ |
pointerover |
指针进入元素(冒泡) | ✅ |
pointerout |
指针离开元素(冒泡) | ✅ |
pointerenter |
指针进入元素(不冒泡) | ❌ |
pointerleave |
指针离开元素(不冒泡) | ❌ |
30.8.3 指针事件属性
javascript
element.addEventListener('pointerdown', (event) => {
// 指针类型
console.log(event.pointerType); // 'mouse' | 'touch' | 'pen'
// 唯一标识(多点触控时区分不同手指)
console.log(event.pointerId); // 数字,每个指针唯一
// 坐标
console.log(event.clientX, event.clientY);
console.log(event.pageX, event.pageY);
// 按键
console.log(event.button); // 同鼠标事件
console.log(event.buttons); // 同鼠标事件
// 压力(0~1,触摸/手写笔支持,鼠标固定为 0.5 按下 / 0 释放)
console.log(event.pressure);
// 倾斜角(手写笔)
console.log(event.tiltX, event.tiltY);
// 宽度和高度(触摸面积)
console.log(event.width, event.height);
// 修饰键
console.log(event.ctrlKey, event.shiftKey, event.altKey, event.metaKey);
});
| 属性 | 说明 |
|---|---|
pointerType |
'mouse'、'touch'、'pen' |
pointerId |
指针唯一标识(多点触控区分) |
pressure |
压力值 0~1(触摸/手写笔) |
width / height |
接触面积(触摸) |
tiltX / tiltY |
倾斜角度(手写笔) |
isPrimary |
是否为主指针(多点触控时第一个触点) |
30.8.4 使用指针事件实现拖拽
javascript
/**
* 使用指针事件实现通用拖拽(支持鼠标、触摸、手写笔)
*/
function makePointerDraggable(element) {
let isDragging = false;
let offsetX = 0, offsetY = 0;
element.addEventListener('pointerdown', (event) => {
isDragging = true;
element.setPointerCapture(event.pointerId); // 捕获指针
const rect = element.getBoundingClientRect();
offsetX = event.clientX - rect.left;
offsetY = event.clientY - rect.top;
element.style.position = 'fixed';
element.style.zIndex = 1000;
});
element.addEventListener('pointermove', (event) => {
if (!isDragging) return;
element.style.left = (event.clientX - offsetX) + 'px';
element.style.top = (event.clientY - offsetY) + 'px';
});
element.addEventListener('pointerup', (event) => {
isDragging = false;
element.releasePointerCapture(event.pointerId); // 释放捕获
});
}
30.8.5 setPointerCapture 与多点触控
setPointerCapture 确保后续的指针事件都被定向到捕获元素,即使指针移出了元素边界:
javascript
element.addEventListener('pointerdown', (event) => {
// 捕获指针:即使手指/鼠标移出元素,事件仍会发送给该元素
element.setPointerCapture(event.pointerId);
// 检查是否是主指针(多指操作中的第一根手指)
if (event.isPrimary) {
console.log('主指针,pointerId:', event.pointerId);
}
});
element.addEventListener('pointerup', (event) => {
// 释放捕获
element.releasePointerCapture(event.pointerId);
});
#mermaid-svg-1mXH0pvpBmBmk16X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1mXH0pvpBmBmk16X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1mXH0pvpBmBmk16X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1mXH0pvpBmBmk16X .error-icon{fill:#552222;}#mermaid-svg-1mXH0pvpBmBmk16X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1mXH0pvpBmBmk16X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1mXH0pvpBmBmk16X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1mXH0pvpBmBmk16X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1mXH0pvpBmBmk16X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1mXH0pvpBmBmk16X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1mXH0pvpBmBmk16X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1mXH0pvpBmBmk16X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1mXH0pvpBmBmk16X .marker.cross{stroke:#333333;}#mermaid-svg-1mXH0pvpBmBmk16X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1mXH0pvpBmBmk16X p{margin:0;}#mermaid-svg-1mXH0pvpBmBmk16X .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1mXH0pvpBmBmk16X .cluster-label text{fill:#333;}#mermaid-svg-1mXH0pvpBmBmk16X .cluster-label span{color:#333;}#mermaid-svg-1mXH0pvpBmBmk16X .cluster-label span p{background-color:transparent;}#mermaid-svg-1mXH0pvpBmBmk16X .label text,#mermaid-svg-1mXH0pvpBmBmk16X span{fill:#333;color:#333;}#mermaid-svg-1mXH0pvpBmBmk16X .node rect,#mermaid-svg-1mXH0pvpBmBmk16X .node circle,#mermaid-svg-1mXH0pvpBmBmk16X .node ellipse,#mermaid-svg-1mXH0pvpBmBmk16X .node polygon,#mermaid-svg-1mXH0pvpBmBmk16X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1mXH0pvpBmBmk16X .rough-node .label text,#mermaid-svg-1mXH0pvpBmBmk16X .node .label text,#mermaid-svg-1mXH0pvpBmBmk16X .image-shape .label,#mermaid-svg-1mXH0pvpBmBmk16X .icon-shape .label{text-anchor:middle;}#mermaid-svg-1mXH0pvpBmBmk16X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1mXH0pvpBmBmk16X .rough-node .label,#mermaid-svg-1mXH0pvpBmBmk16X .node .label,#mermaid-svg-1mXH0pvpBmBmk16X .image-shape .label,#mermaid-svg-1mXH0pvpBmBmk16X .icon-shape .label{text-align:center;}#mermaid-svg-1mXH0pvpBmBmk16X .node.clickable{cursor:pointer;}#mermaid-svg-1mXH0pvpBmBmk16X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1mXH0pvpBmBmk16X .arrowheadPath{fill:#333333;}#mermaid-svg-1mXH0pvpBmBmk16X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1mXH0pvpBmBmk16X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1mXH0pvpBmBmk16X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1mXH0pvpBmBmk16X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1mXH0pvpBmBmk16X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1mXH0pvpBmBmk16X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1mXH0pvpBmBmk16X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1mXH0pvpBmBmk16X .cluster text{fill:#333;}#mermaid-svg-1mXH0pvpBmBmk16X .cluster span{color:#333;}#mermaid-svg-1mXH0pvpBmBmk16X div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1mXH0pvpBmBmk16X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1mXH0pvpBmBmk16X rect.text{fill:none;stroke-width:0;}#mermaid-svg-1mXH0pvpBmBmk16X .icon-shape,#mermaid-svg-1mXH0pvpBmBmk16X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1mXH0pvpBmBmk16X .icon-shape p,#mermaid-svg-1mXH0pvpBmBmk16X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1mXH0pvpBmBmk16X .icon-shape .label rect,#mermaid-svg-1mXH0pvpBmBmk16X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1mXH0pvpBmBmk16X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1mXH0pvpBmBmk16X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1mXH0pvpBmBmk16X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} pointerdown
setPointerCapture
捕获指针
pointermove
(即使移出元素仍触发)
pointerup
releasePointerCapture
释放捕获
30.8.6 指针事件 vs 鼠标事件
| 对比项 | 鼠标事件 | 指针事件 |
|---|---|---|
| 设备支持 | 仅鼠标 | 鼠标 + 触摸 + 手写笔 |
| 多点触控 | ❌ | ✅(通过 pointerId 区分) |
| 捕获机制 | 需在 document 上监听 | setPointerCapture |
| 兼容性 | 全部浏览器 | IE10+(主流浏览器支持) |
| 推荐 | 兼容旧浏览器时使用 | 现代项目优先使用 |
最佳实践 :现代项目优先使用指针事件,如果需要兼容旧浏览器,可以用 pointerevents polyfill 或同时监听鼠标/触摸事件。
30.9 键盘事件(keydown 和 keyup)
30.9.1 键盘事件类型
| 事件 | 触发时机 | 备注 |
|---|---|---|
keydown |
按键按下 | 可重复触发(长按时) |
keyup |
按键释放 | 每次释放触发一次 |
keypress |
按键产生字符时 | ⚠️ 已废弃,不推荐使用 |
#mermaid-svg-C2q26c3jWC7oyCGK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-C2q26c3jWC7oyCGK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C2q26c3jWC7oyCGK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C2q26c3jWC7oyCGK .error-icon{fill:#552222;}#mermaid-svg-C2q26c3jWC7oyCGK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C2q26c3jWC7oyCGK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C2q26c3jWC7oyCGK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C2q26c3jWC7oyCGK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C2q26c3jWC7oyCGK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C2q26c3jWC7oyCGK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C2q26c3jWC7oyCGK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C2q26c3jWC7oyCGK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C2q26c3jWC7oyCGK .marker.cross{stroke:#333333;}#mermaid-svg-C2q26c3jWC7oyCGK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C2q26c3jWC7oyCGK p{margin:0;}#mermaid-svg-C2q26c3jWC7oyCGK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-C2q26c3jWC7oyCGK .cluster-label text{fill:#333;}#mermaid-svg-C2q26c3jWC7oyCGK .cluster-label span{color:#333;}#mermaid-svg-C2q26c3jWC7oyCGK .cluster-label span p{background-color:transparent;}#mermaid-svg-C2q26c3jWC7oyCGK .label text,#mermaid-svg-C2q26c3jWC7oyCGK span{fill:#333;color:#333;}#mermaid-svg-C2q26c3jWC7oyCGK .node rect,#mermaid-svg-C2q26c3jWC7oyCGK .node circle,#mermaid-svg-C2q26c3jWC7oyCGK .node ellipse,#mermaid-svg-C2q26c3jWC7oyCGK .node polygon,#mermaid-svg-C2q26c3jWC7oyCGK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C2q26c3jWC7oyCGK .rough-node .label text,#mermaid-svg-C2q26c3jWC7oyCGK .node .label text,#mermaid-svg-C2q26c3jWC7oyCGK .image-shape .label,#mermaid-svg-C2q26c3jWC7oyCGK .icon-shape .label{text-anchor:middle;}#mermaid-svg-C2q26c3jWC7oyCGK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-C2q26c3jWC7oyCGK .rough-node .label,#mermaid-svg-C2q26c3jWC7oyCGK .node .label,#mermaid-svg-C2q26c3jWC7oyCGK .image-shape .label,#mermaid-svg-C2q26c3jWC7oyCGK .icon-shape .label{text-align:center;}#mermaid-svg-C2q26c3jWC7oyCGK .node.clickable{cursor:pointer;}#mermaid-svg-C2q26c3jWC7oyCGK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-C2q26c3jWC7oyCGK .arrowheadPath{fill:#333333;}#mermaid-svg-C2q26c3jWC7oyCGK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C2q26c3jWC7oyCGK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C2q26c3jWC7oyCGK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C2q26c3jWC7oyCGK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-C2q26c3jWC7oyCGK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C2q26c3jWC7oyCGK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-C2q26c3jWC7oyCGK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C2q26c3jWC7oyCGK .cluster text{fill:#333;}#mermaid-svg-C2q26c3jWC7oyCGK .cluster span{color:#333;}#mermaid-svg-C2q26c3jWC7oyCGK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-C2q26c3jWC7oyCGK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-C2q26c3jWC7oyCGK rect.text{fill:none;stroke-width:0;}#mermaid-svg-C2q26c3jWC7oyCGK .icon-shape,#mermaid-svg-C2q26c3jWC7oyCGK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C2q26c3jWC7oyCGK .icon-shape p,#mermaid-svg-C2q26c3jWC7oyCGK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-C2q26c3jWC7oyCGK .icon-shape .label rect,#mermaid-svg-C2q26c3jWC7oyCGK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C2q26c3jWC7oyCGK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-C2q26c3jWC7oyCGK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-C2q26c3jWC7oyCGK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
按下按键
keydown
产生字符?
keypress(已废弃)
长按中...
重复 keydown
释放按键
keyup
javascript
// 基本用法
document.addEventListener('keydown', (event) => {
console.log('按下:', event.key, '(code:', event.code + ')');
});
document.addEventListener('keyup', (event) => {
console.log('释放:', event.key);
});
30.9.2 key 与 code 的区别
这是键盘事件中最重要的概念:
| 属性 | 说明 | 示例 |
|---|---|---|
event.key |
按键产生的字符(受输入法和布局影响) | 'a'、'A'、'Enter'、'1'、'!' |
event.code |
按键的物理位置(不受布局影响) | 'KeyA'、'Enter'、'Digit1'、'ShiftLeft' |
javascript
document.addEventListener('keydown', (event) => {
// 同一个键,Shift 按下与否 key 不同
// 按下 1 键
console.log(event.key); // '1'
console.log(event.code); // 'Digit1'
// Shift + 1 键
console.log(event.key); // '!'(美式键盘)
console.log(event.code); // 'Digit1'(物理位置不变)
// 字母键
// 按下 A 键
console.log(event.key); // 'a'(小写)
console.log(event.code); // 'KeyA'
// Shift + A
console.log(event.key); // 'A'(大写)
console.log(event.code); // 'KeyA'
});
| 按键 | event.key |
event.code |
|---|---|---|
| 字母 A(小写) | 'a' |
'KeyA' |
| 字母 A(大写,Shift+A) | 'A' |
'KeyA' |
| 数字 1 | '1' |
'Digit1' |
| Shift + 1(美式键盘) | '!' |
'Digit1' |
| 回车 | 'Enter' |
'Enter' |
| 空格 | ' '(空格字符) |
'Space' |
| 左 Shift | 'Shift' |
'ShiftLeft' |
| 右 Shift | 'Shift' |
'ShiftRight' |
| 方向键 ↑ | 'ArrowUp' |
'ArrowUp' |
| Ctrl | 'Control' |
'ControlLeft' / 'ControlRight' |
| Esc | 'Escape' |
'Escape' |
| Tab | 'Tab' |
'Tab' |
| Backspace | 'Backspace' |
'Backspace' |
最佳实践:
- 需要根据输入内容处理时,用
event.key(如输入框中输入字符)- 需要根据物理位置处理时(如游戏中的 WASD 控制键),用
event.code- 检测修饰键时,用
event.ctrlKey/event.shiftKey/event.altKey/event.metaKey
30.9.3 修饰键
javascript
document.addEventListener('keydown', (event) => {
// 检测组合键
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
console.log('Ctrl+S:保存');
}
if (event.ctrlKey && event.shiftKey && event.key === 'S') {
event.preventDefault();
console.log('Ctrl+Shift+S:另存为');
}
// Meta 键(Mac: Command,Windows: Win键)
if (event.metaKey && event.key === 's') {
event.preventDefault();
console.log('Cmd+S / Win+S:保存');
}
// Alt 键
if (event.altKey && event.key === 'F4') {
event.preventDefault();
console.log('Alt+F4');
}
});
30.9.4 长按重复触发
当用户长按一个键时,keydown 事件会重复触发。可以通过 event.repeat 判断是否是重复触发:
javascript
document.addEventListener('keydown', (event) => {
if (event.repeat) {
console.log('重复触发(长按中)');
} else {
console.log('首次按下');
}
});
30.9.5 实用:方向键导航
javascript
// 方向键控制焦点移动
const items = document.querySelectorAll('.nav-item');
let currentIndex = 0;
items[currentIndex].focus();
document.addEventListener('keydown', (event) => {
switch (event.key) {
case 'ArrowDown':
case 'ArrowRight':
event.preventDefault();
currentIndex = (currentIndex + 1) % items.length;
items[currentIndex].focus();
break;
case 'ArrowUp':
case 'ArrowLeft':
event.preventDefault();
currentIndex = (currentIndex - 1 + items.length) % items.length;
items[currentIndex].focus();
break;
case 'Home':
event.preventDefault();
currentIndex = 0;
items[currentIndex].focus();
break;
case 'End':
event.preventDefault();
currentIndex = items.length - 1;
items[currentIndex].focus();
break;
}
});
30.10 滚动事件
30.10.1 scroll 事件
scroll 事件在元素或页面滚动时触发:
javascript
// 监听页面滚动
window.addEventListener('scroll', () => {
console.log('滚动位置:', window.scrollY);
});
// 监听元素滚动
const scrollContainer = document.getElementById('scroll-container');
scrollContainer.addEventListener('scroll', () => {
console.log('容器滚动位置:', scrollContainer.scrollTop);
});
注意 :
scroll事件不冒泡,但可以在window或具体元素上监听。如果需要在父元素上委托监听,可以使用capture: true或监听document的scroll事件。
30.10.2 滚动事件性能优化
scroll 事件在滚动过程中会高频触发,直接在其中执行复杂逻辑会导致性能问题:
javascript
// ❌ 低效:每次滚动都执行重操作
window.addEventListener('scroll', () => {
// 查询 DOM + 计算位置(可能导致布局抖动 jank)
const elements = document.querySelectorAll('.lazy');
elements.forEach(el => {
if (el.getBoundingClientRect().top < window.innerHeight) {
el.classList.add('visible');
}
});
});
// ✅ 优化1:使用 requestAnimationFrame 节流
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
console.log('滚动位置:', window.scrollY);
ticking = false;
});
ticking = true;
}
});
// ✅ 优化2:使用防抖(debounce)
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
window.addEventListener('scroll', debounce(() => {
console.log('滚动结束后触发(150ms 无新滚动)');
}, 150));
// ✅ 优化3:使用节流(throttle)
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
};
}
window.addEventListener('scroll', throttle(() => {
console.log('每 100ms 最多触发一次');
}, 100));
| 优化方式 | 特点 | 适用场景 |
|---|---|---|
requestAnimationFrame |
与浏览器渲染同步,每帧最多一次 | 需要平滑更新的场景 |
| 防抖(debounce) | 滚动停止后才触发 | 不需要实时跟踪的场景 |
| 节流(throttle) | 固定时间间隔触发 | 需要定期跟踪但不要太高频 |
30.10.3 滚动方向检测
javascript
let lastScrollY = window.scrollY;
let scrollDirection = 'none';
window.addEventListener('scroll', () => {
const currentScrollY = window.scrollY;
if (currentScrollY > lastScrollY) {
scrollDirection = 'down';
} else if (currentScrollY < lastScrollY) {
scrollDirection = 'up';
}
lastScrollY = currentScrollY;
// 显示/隐藏导航栏
const navbar = document.getElementById('navbar');
if (scrollDirection === 'down' && currentScrollY > 100) {
navbar.classList.add('hidden'); // 向下滚动时隐藏
} else {
navbar.classList.remove('hidden'); // 向上滚动时显示
}
});
30.10.4 程序化滚动控制
javascript
// 瞬间滚动到指定位置
window.scrollTo(0, 500);
// 平滑滚动到指定位置
window.scrollTo({
top: 500,
left: 0,
behavior: 'smooth' // 'auto'(默认)或 'smooth'
});
// 相对滚动
window.scrollBy(0, 100); // 向下滚动 100px
window.scrollBy({
top: 100,
behavior: 'smooth'
});
// 滚动元素到视口
document.getElementById('target').scrollIntoView({
behavior: 'smooth',
block: 'start', // 'start' | 'center' | 'end' | 'nearest'
inline: 'nearest' // 'start' | 'center' | 'end' | 'nearest'
});
// 检测是否滚动到底部
window.addEventListener('scroll', () => {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
const clientHeight = document.documentElement.clientHeight;
if (scrollTop + clientHeight >= scrollHeight - 50) {
console.log('接近底部,可以加载更多');
loadMore();
}
});
30.10.5 wheel 事件
wheel 事件是鼠标滚轮滚动的更现代的事件:
javascript
document.addEventListener('wheel', (event) => {
console.log('滚动方向:', event.deltaY > 0 ? '向下' : '向上');
console.log('滚动量:', event.deltaY);
console.log('滚动模式:', event.deltaMode); // 0=像素,1=行,2=页
// 阻止默认页面滚动
// event.preventDefault(); // 如果非 passive 监听器
});
注意 :在现代浏览器中,
wheel和touchmove事件监听器默认被标记为passive。如果需要调用preventDefault(),需要显式声明{ passive: false }。
30.11 实战应用
30.11.1 拖拽排序组件
实现一个可以拖拽排序的列表组件,使用 HTML5 拖放 API:
html
<ul id="sortable-list" class="sortable-list">
<li class="sortable-item" draggable="true" data-id="1">
<span class="drag-handle">⋮⋮</span>
<span class="item-text">项目一</span>
</li>
<li class="sortable-item" draggable="true" data-id="2">
<span class="drag-handle">⋮⋮</span>
<span class="item-text">项目二</span>
</li>
<li class="sortable-item" draggable="true" data-id="3">
<span class="drag-handle">⋮⋮</span>
<span class="item-text">项目三</span>
</li>
<li class="sortable-item" draggable="true" data-id="4">
<span class="drag-handle">⋮⋮</span>
<span class="item-text">项目四</span>
</li>
<li class="sortable-item" draggable="true" data-id="5">
<span class="drag-handle">⋮⋮</span>
<span class="item-text">项目五</span>
</li>
</ul>
css
.sortable-list {
list-style: none;
padding: 0;
max-width: 400px;
}
.sortable-item {
display: flex;
align-items: center;
padding: 12px 16px;
margin-bottom: 8px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
cursor: grab;
transition: all 0.2s ease;
}
.sortable-item.dragging {
opacity: 0.4;
cursor: grabbing;
}
.sortable-item.drag-over {
border-color: #42b883;
border-top: 3px solid #42b883;
}
.drag-handle {
margin-right: 12px;
color: #ccc;
font-size: 18px;
user-select: none;
}
javascript
/**
* 拖拽排序组件
*/
class SortableList {
constructor(listElement, options = {}) {
this.list = listElement;
this.draggedItem = null;
this.onReorder = options.onReorder || (() => {});
this.init();
}
init() {
const items = this.list.querySelectorAll('.sortable-item');
items.forEach(item => {
// 开始拖拽
item.addEventListener('dragstart', (e) => this.handleDragStart(e));
item.addEventListener('dragend', (e) => this.handleDragEnd(e));
// 拖拽经过
item.addEventListener('dragover', (e) => this.handleDragOver(e));
item.addEventListener('dragenter', (e) => this.handleDragEnter(e));
item.addEventListener('dragleave', (e) => this.handleDragLeave(e));
// 放置
item.addEventListener('drop', (e) => this.handleDrop(e));
});
}
handleDragStart(event) {
this.draggedItem = event.target;
event.target.classList.add('dragging');
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', event.target.dataset.id);
}
handleDragEnd(event) {
event.target.classList.remove('dragging');
// 清除所有 drag-over 样式
this.list.querySelectorAll('.drag-over').forEach(el => {
el.classList.remove('drag-over');
});
this.draggedItem = null;
}
handleDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
return false;
}
handleDragEnter(event) {
const target = this.getListItem(event.target);
if (target && target !== this.draggedItem) {
target.classList.add('drag-over');
}
}
handleDragLeave(event) {
const target = this.getListItem(event.target);
if (target) {
target.classList.remove('drag-over');
}
}
handleDrop(event) {
event.preventDefault();
event.stopPropagation();
const target = this.getListItem(event.target);
if (target && target !== this.draggedItem) {
// 获取所有列表项的顺序
const allItems = Array.from(this.list.querySelectorAll('.sortable-item'));
const draggedIndex = allItems.indexOf(this.draggedItem);
const targetIndex = allItems.indexOf(target);
// 根据拖拽方向决定插入位置
if (draggedIndex < targetIndex) {
target.parentNode.insertBefore(this.draggedItem, target.nextSibling);
} else {
target.parentNode.insertBefore(this.draggedItem, target);
}
// 触发回调
const newOrder = Array.from(this.list.querySelectorAll('.sortable-item'))
.map(item => item.dataset.id);
this.onReorder(newOrder);
}
target?.classList.remove('drag-over');
return false;
}
getListItem(element) {
return element.closest('.sortable-item');
}
}
// 使用
const sortable = new SortableList(
document.getElementById('sortable-list'),
{
onReorder: (newOrder) => {
console.log('新顺序:', newOrder);
// 可以在这里发送请求保存新顺序到服务器
}
}
);
#mermaid-svg-xulckeJaiX1W9pP1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xulckeJaiX1W9pP1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xulckeJaiX1W9pP1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xulckeJaiX1W9pP1 .error-icon{fill:#552222;}#mermaid-svg-xulckeJaiX1W9pP1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xulckeJaiX1W9pP1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xulckeJaiX1W9pP1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xulckeJaiX1W9pP1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xulckeJaiX1W9pP1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xulckeJaiX1W9pP1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xulckeJaiX1W9pP1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xulckeJaiX1W9pP1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xulckeJaiX1W9pP1 .marker.cross{stroke:#333333;}#mermaid-svg-xulckeJaiX1W9pP1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xulckeJaiX1W9pP1 p{margin:0;}#mermaid-svg-xulckeJaiX1W9pP1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xulckeJaiX1W9pP1 .cluster-label text{fill:#333;}#mermaid-svg-xulckeJaiX1W9pP1 .cluster-label span{color:#333;}#mermaid-svg-xulckeJaiX1W9pP1 .cluster-label span p{background-color:transparent;}#mermaid-svg-xulckeJaiX1W9pP1 .label text,#mermaid-svg-xulckeJaiX1W9pP1 span{fill:#333;color:#333;}#mermaid-svg-xulckeJaiX1W9pP1 .node rect,#mermaid-svg-xulckeJaiX1W9pP1 .node circle,#mermaid-svg-xulckeJaiX1W9pP1 .node ellipse,#mermaid-svg-xulckeJaiX1W9pP1 .node polygon,#mermaid-svg-xulckeJaiX1W9pP1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xulckeJaiX1W9pP1 .rough-node .label text,#mermaid-svg-xulckeJaiX1W9pP1 .node .label text,#mermaid-svg-xulckeJaiX1W9pP1 .image-shape .label,#mermaid-svg-xulckeJaiX1W9pP1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-xulckeJaiX1W9pP1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xulckeJaiX1W9pP1 .rough-node .label,#mermaid-svg-xulckeJaiX1W9pP1 .node .label,#mermaid-svg-xulckeJaiX1W9pP1 .image-shape .label,#mermaid-svg-xulckeJaiX1W9pP1 .icon-shape .label{text-align:center;}#mermaid-svg-xulckeJaiX1W9pP1 .node.clickable{cursor:pointer;}#mermaid-svg-xulckeJaiX1W9pP1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xulckeJaiX1W9pP1 .arrowheadPath{fill:#333333;}#mermaid-svg-xulckeJaiX1W9pP1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xulckeJaiX1W9pP1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xulckeJaiX1W9pP1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xulckeJaiX1W9pP1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xulckeJaiX1W9pP1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xulckeJaiX1W9pP1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xulckeJaiX1W9pP1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xulckeJaiX1W9pP1 .cluster text{fill:#333;}#mermaid-svg-xulckeJaiX1W9pP1 .cluster span{color:#333;}#mermaid-svg-xulckeJaiX1W9pP1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xulckeJaiX1W9pP1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xulckeJaiX1W9pP1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-xulckeJaiX1W9pP1 .icon-shape,#mermaid-svg-xulckeJaiX1W9pP1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xulckeJaiX1W9pP1 .icon-shape p,#mermaid-svg-xulckeJaiX1W9pP1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xulckeJaiX1W9pP1 .icon-shape .label rect,#mermaid-svg-xulckeJaiX1W9pP1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xulckeJaiX1W9pP1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xulckeJaiX1W9pP1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xulckeJaiX1W9pP1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} dragstart
记录被拖拽元素
添加 dragging 样式
dragover
preventDefault
允许放置
dragenter
添加 drag-over 样式
标记目标位置
drop
重新排列 DOM
触发 onReorder 回调
dragend
清除样式
重置状态
30.11.2 全局快捷键系统
实现一个支持组合键、作用域、快捷键冲突检测的全局快捷键系统:
javascript
/**
* 全局快捷键管理系统
*
* 功能:
* - 支持组合键(Ctrl+Shift+K 等)
* - 支持作用域(不同区域不同快捷键)
* - 快捷键冲突检测
* - 动态注册/注销
*/
class HotkeyManager {
constructor() {
this._bindings = new Map(); // key → [{ handler, scope, description }]
this._currentScope = 'global';
this._init();
}
/**
* 初始化全局监听
*/
_init() {
document.addEventListener('keydown', (event) => this._handleKeyDown(event));
}
/**
* 解析快捷键字符串为标准化格式
* @param {string} hotkey - 如 'Ctrl+Shift+K'
* @returns {string} 标准化后的键,如 'ctrl+shift+k'
*/
_normalizeHotkey(hotkey) {
const parts = hotkey.toLowerCase().split('+').map(p => p.trim());
const modifiers = [];
let key = '';
for (const part of parts) {
if (['ctrl', 'control', 'cmd', 'meta', 'command'].includes(part)) {
modifiers.push('ctrl'); // 统一 Ctrl 和 Cmd
} else if (part === 'shift') {
modifiers.push('shift');
} else if (part === 'alt' || part === 'option') {
modifiers.push('alt');
} else {
key = part;
}
}
modifiers.sort();
return [...modifiers, key].join('+');
}
/**
* 从键盘事件生成标准化键
*/
_eventToHotkey(event) {
const modifiers = [];
if (event.ctrlKey || event.metaKey) modifiers.push('ctrl');
if (event.shiftKey) modifiers.push('shift');
if (event.altKey) modifiers.push('alt');
// 使用 event.code 作为物理键标识
let key = event.key.toLowerCase();
// 特殊键转换
const keyMap = {
' ': 'space',
'escape': 'esc',
'delete': 'del',
'arrowup': 'up',
'arrowdown': 'down',
'arrowleft': 'left',
'arrowright': 'right'
};
key = keyMap[key] || key;
modifiers.sort();
return [...modifiers, key].join('+');
}
/**
* 注册快捷键
* @param {string} hotkey - 快捷键字符串,如 'Ctrl+S'
* @param {Function} handler - 处理函数
* @param {Object} options - { scope, description, preventDefault }
*/
register(hotkey, handler, options = {}) {
const normalizedKey = this._normalizeHotkey(hotkey);
const scope = options.scope || 'global';
const description = options.description || '';
const preventDefault = options.preventDefault !== false;
// 检查冲突
if (!this._bindings.has(normalizedKey)) {
this._bindings.set(normalizedKey, []);
}
const bindings = this._bindings.get(normalizedKey);
const existing = bindings.find(b => b.scope === scope);
if (existing) {
console.warn(`快捷键冲突:${hotkey} 已在 scope "${scope}" 中注册`);
return false;
}
bindings.push({ handler, scope, description, preventDefault, original: hotkey });
return true;
}
/**
* 注销快捷键
*/
unregister(hotkey, scope = 'global') {
const normalizedKey = this._normalizeHotkey(hotkey);
const bindings = this._bindings.get(normalizedKey);
if (!bindings) return;
const index = bindings.findIndex(b => b.scope === scope);
if (index !== -1) {
bindings.splice(index, 1);
if (bindings.length === 0) {
this._bindings.delete(normalizedKey);
}
}
}
/**
* 设置当前作用域
*/
setScope(scope) {
this._currentScope = scope;
}
/**
* 处理键盘事件
*/
_handleKeyDown(event) {
// 在输入框中不触发快捷键(除非绑定了特殊标记)
const target = event.target;
const isInputField = target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable;
const hotkey = this._eventToHotkey(event);
const bindings = this._bindings.get(hotkey);
if (!bindings || bindings.length === 0) return;
// 优先匹配当前作用域,其次匹配全局
const binding = bindings.find(b => b.scope === this._currentScope) ||
bindings.find(b => b.scope === 'global');
if (!binding) return;
// 如果在输入框中且非全局快捷键,跳过
if (isInputField && binding.scope !== 'global') return;
if (binding.preventDefault) {
event.preventDefault();
}
binding.handler(event);
}
/**
* 获取所有已注册的快捷键列表
*/
getAllBindings() {
const result = [];
for (const [key, bindings] of this._bindings) {
for (const b of bindings) {
result.push({
hotkey: b.original,
scope: b.scope,
description: b.description
});
}
}
return result;
}
}
// ========== 使用示例 ==========
const hotkeys = new HotkeyManager();
// 全局快捷键
hotkeys.register('Ctrl+S', () => {
console.log('保存文档');
saveDocument();
}, { description: '保存' });
hotkeys.register('Ctrl+Shift+S', () => {
console.log('另存为');
}, { description: '另存为' });
hotkeys.register('Ctrl+Z', () => {
console.log('撤销');
undo();
}, { description: '撤销' });
hotkeys.register('Ctrl+Y', () => {
console.log('重做');
redo();
}, { description: '重做' });
// 编辑器作用域快捷键
hotkeys.register('Ctrl+B', () => {
console.log('加粗');
}, { scope: 'editor', description: '加粗文本' });
hotkeys.register('Ctrl+I', () => {
console.log('斜体');
}, { scope: 'editor', description: '斜体文本' });
hotkeys.register('Ctrl+/', () => {
console.log('注释代码');
}, { scope: 'editor', description: '注释/取消注释' });
// 切换作用域
const editor = document.getElementById('editor');
editor.addEventListener('focus', () => hotkeys.setScope('editor'));
editor.addEventListener('blur', () => hotkeys.setScope('global'));
// 查看所有快捷键
console.table(hotkeys.getAllBindings());
function saveDocument() { /* 保存逻辑 */ }
function undo() { /* 撤销逻辑 */ }
function redo() { /* 重做逻辑 */ }
#mermaid-svg-tGp2eMBGsra49A6v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tGp2eMBGsra49A6v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tGp2eMBGsra49A6v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tGp2eMBGsra49A6v .error-icon{fill:#552222;}#mermaid-svg-tGp2eMBGsra49A6v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tGp2eMBGsra49A6v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tGp2eMBGsra49A6v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tGp2eMBGsra49A6v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tGp2eMBGsra49A6v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tGp2eMBGsra49A6v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tGp2eMBGsra49A6v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tGp2eMBGsra49A6v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tGp2eMBGsra49A6v .marker.cross{stroke:#333333;}#mermaid-svg-tGp2eMBGsra49A6v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tGp2eMBGsra49A6v p{margin:0;}#mermaid-svg-tGp2eMBGsra49A6v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tGp2eMBGsra49A6v .cluster-label text{fill:#333;}#mermaid-svg-tGp2eMBGsra49A6v .cluster-label span{color:#333;}#mermaid-svg-tGp2eMBGsra49A6v .cluster-label span p{background-color:transparent;}#mermaid-svg-tGp2eMBGsra49A6v .label text,#mermaid-svg-tGp2eMBGsra49A6v span{fill:#333;color:#333;}#mermaid-svg-tGp2eMBGsra49A6v .node rect,#mermaid-svg-tGp2eMBGsra49A6v .node circle,#mermaid-svg-tGp2eMBGsra49A6v .node ellipse,#mermaid-svg-tGp2eMBGsra49A6v .node polygon,#mermaid-svg-tGp2eMBGsra49A6v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tGp2eMBGsra49A6v .rough-node .label text,#mermaid-svg-tGp2eMBGsra49A6v .node .label text,#mermaid-svg-tGp2eMBGsra49A6v .image-shape .label,#mermaid-svg-tGp2eMBGsra49A6v .icon-shape .label{text-anchor:middle;}#mermaid-svg-tGp2eMBGsra49A6v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tGp2eMBGsra49A6v .rough-node .label,#mermaid-svg-tGp2eMBGsra49A6v .node .label,#mermaid-svg-tGp2eMBGsra49A6v .image-shape .label,#mermaid-svg-tGp2eMBGsra49A6v .icon-shape .label{text-align:center;}#mermaid-svg-tGp2eMBGsra49A6v .node.clickable{cursor:pointer;}#mermaid-svg-tGp2eMBGsra49A6v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tGp2eMBGsra49A6v .arrowheadPath{fill:#333333;}#mermaid-svg-tGp2eMBGsra49A6v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tGp2eMBGsra49A6v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tGp2eMBGsra49A6v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tGp2eMBGsra49A6v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tGp2eMBGsra49A6v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tGp2eMBGsra49A6v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tGp2eMBGsra49A6v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tGp2eMBGsra49A6v .cluster text{fill:#333;}#mermaid-svg-tGp2eMBGsra49A6v .cluster span{color:#333;}#mermaid-svg-tGp2eMBGsra49A6v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tGp2eMBGsra49A6v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tGp2eMBGsra49A6v rect.text{fill:none;stroke-width:0;}#mermaid-svg-tGp2eMBGsra49A6v .icon-shape,#mermaid-svg-tGp2eMBGsra49A6v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tGp2eMBGsra49A6v .icon-shape p,#mermaid-svg-tGp2eMBGsra49A6v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tGp2eMBGsra49A6v .icon-shape .label rect,#mermaid-svg-tGp2eMBGsra49A6v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tGp2eMBGsra49A6v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tGp2eMBGsra49A6v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tGp2eMBGsra49A6v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
是 且 非全局
否 或 全局
用户按下快捷键
keydown 事件
HotkeyManager._handleKeyDown
标准化快捷键
查找已注册绑定
找到匹配?
不处理
在输入框中?
preventDefault
执行 handler
30.11.3 综合示例:可交互的图片画廊
结合事件委托、拖放、键盘导航的综合示例:
javascript
/**
* 图片画廊组件
* - 事件委托处理图片点击
* - 键盘导航(左右箭头切换)
* - 拖放排序
*/
class ImageGallery {
constructor(container) {
this.container = container;
this.images = Array.from(container.querySelectorAll('.gallery-item'));
this.currentIndex = 0;
this._initEventDelegation();
this._initKeyboardNav();
this._initDragSort();
}
/**
* 事件委托:点击预览、双击删除
*/
_initEventDelegation() {
this.container.addEventListener('click', (event) => {
const item = event.target.closest('.gallery-item');
if (!item) return;
const action = event.target.closest('[data-action]')?.dataset.action;
switch (action) {
case 'preview':
this.preview(item.dataset.index);
break;
case 'delete':
this.deleteItem(item);
break;
default:
// 点击图片本身也预览
if (event.target.classList.contains('gallery-image')) {
this.preview(item.dataset.index);
}
}
});
// 双击删除
this.container.addEventListener('dblclick', (event) => {
const item = event.target.closest('.gallery-item');
if (item && event.target.classList.contains('gallery-image')) {
this.deleteItem(item);
}
});
}
/**
* 键盘导航
*/
_initKeyboardNav() {
document.addEventListener('keydown', (event) => {
// 只在预览模式或画廊聚焦时响应
if (!this.isPreviewing && !this.container.contains(document.activeElement)) {
return;
}
switch (event.key) {
case 'ArrowLeft':
event.preventDefault();
this.prev();
break;
case 'ArrowRight':
event.preventDefault();
this.next();
break;
case 'Escape':
this.closePreview();
break;
}
});
}
/**
* 拖放排序
*/
_initDragSort() {
let draggedItem = null;
this.container.addEventListener('dragstart', (event) => {
const item = event.target.closest('.gallery-item');
if (!item) return;
draggedItem = item;
item.classList.add('dragging');
event.dataTransfer.effectAllowed = 'move';
});
this.container.addEventListener('dragover', (event) => {
event.preventDefault();
const item = event.target.closest('.gallery-item');
if (item && item !== draggedItem) {
item.classList.add('drag-over');
}
});
this.container.addEventListener('dragleave', (event) => {
const item = event.target.closest('.gallery-item');
if (item) item.classList.remove('drag-over');
});
this.container.addEventListener('drop', (event) => {
event.preventDefault();
const target = event.target.closest('.gallery-item');
if (target && target !== draggedItem) {
const rect = target.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
if (event.clientY < midpoint) {
target.parentNode.insertBefore(draggedItem, target);
} else {
target.parentNode.insertBefore(draggedItem, target.nextSibling);
}
target.classList.remove('drag-over');
this._updateIndices();
}
});
this.container.addEventListener('dragend', () => {
this.container.querySelectorAll('.drag-over').forEach(el => {
el.classList.remove('drag-over');
});
draggedItem?.classList.remove('dragging');
draggedItem = null;
});
}
_updateIndices() {
this.images = Array.from(this.container.querySelectorAll('.gallery-item'));
this.images.forEach((item, index) => {
item.dataset.index = index;
});
}
preview(index) {
this.currentIndex = parseInt(index);
this.isPreviewing = true;
console.log('预览图片:', this.currentIndex);
// 显示预览 UI...
}
closePreview() {
this.isPreviewing = false;
console.log('关闭预览');
}
next() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
this.preview(this.currentIndex);
}
prev() {
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
this.preview(this.currentIndex);
}
deleteItem(item) {
const index = parseInt(item.dataset.index);
console.log('删除图片:', index);
item.remove();
this._updateIndices();
}
}
// 使用
const gallery = new ImageGallery(document.getElementById('gallery'));
本章小结
| 知识点 | 要点 |
|---|---|
| 事件处理程序 | HTML 属性(不推荐)、DOM 属性 on*(覆盖)、addEventListener(推荐,支持多个监听器) |
| addEventListener | 第三参数:capture(捕获阶段)、once(只触发一次)、passive(不调用 preventDefault) |
| removeEventListener | 必须传入相同函数引用 ,capture 参数需一致 |
| Event 对象 | type、target(实际触发元素)、currentTarget(绑定监听器的元素) |
| 事件流三阶段 | ① 捕获(外→内)→ ② 目标 → ③ 冒泡(内→外) |
| 事件冒泡 | 事件从 target 向上传播到 window,大部分事件冒泡 |
| stopPropagation | 阻止事件继续传播 |
| stopImmediatePropagation | 阻止传播 + 阻止同元素其他监听器 |
| 事件委托 | 利用冒泡在父元素统一处理子元素事件,结合 closest() 查找目标 |
| preventDefault | 阻止浏览器默认行为(链接跳转、表单提交等) |
| passive | 声明不调用 preventDefault,优化滚动性能 |
| 自定义事件 | new CustomEvent(type, { detail, bubbles, cancelable }) + dispatchEvent |
| 鼠标事件 | click、mousedown/up、contextmenu、mouseover/out(冒泡)、mouseenter/leave(不冒泡) |
| 鼠标坐标 | clientX/Y(视口)、pageX/Y(文档)、screenX/Y(屏幕) |
| 鼠标按键 | event.button:0=左键,1=中键,2=右键 |
| 拖放 API | dragstart→dragover(需 preventDefault)→drop,通过 dataTransfer 传递数据 |
| 指针事件 | 统一鼠标/触摸/手写笔,pointerType 区分设备,setPointerCapture 捕获指针 |
| 键盘事件 | keydown(按下)、keyup(释放),keypress 已废弃 |
| key vs code | key 是字符(受布局影响),code 是物理位置(不受影响) |
| 修饰键 | ctrlKey、shiftKey、altKey、metaKey |
| 滚动事件 | scroll 不冒泡,高频触发需用 rAF/防抖/节流优化 |
| 滚动控制 | scrollTo、scrollBy、scrollIntoView(支持 behavior: 'smooth') |
| 事件重复 | event.repeat 判断长按重复触发 |
一句话总结 :浏览器事件是用户与页面交互的核心机制------通过
addEventListener注册监听器,事件经历捕获→目标→冒泡 三个阶段传播,利用事件冒泡 可以实现事件委托 (在父元素统一处理子元素事件,结合closest()查找目标),用preventDefault()阻止默认行为、stopPropagation()阻止传播;鼠标事件区分mouseover/mouseout(冒泡)与mouseenter/mouseleave(不冒泡),event.key反映输入字符而event.code反映物理按键位置;HTML5 拖放 API 通过dragstart/dragover(必须preventDefault)/drop实现,指针事件 统一了鼠标、触摸、手写笔三种输入;滚动事件高频触发需用requestAnimationFrame或防抖节流优化,passive: true可以提升滚动性能------掌握这些事件机制,就能构建出丰富的交互体验。
下篇预告 :第31章:表单与控件 --- 我们将学习表单属性与方法(form.elements)、表单验证、focus/blur 事件、change/input 事件、剪贴板事件、表单提交,以及实战表单验证库和多步表单。
作者 :xhmico | CSDN 博客 :https://blog.csdn.net/xhmico
参考资源 :The Modern JavaScript Tutorial | MDN Web Docs | ECMAScript 规范