JavaScript|第30章:事件机制

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
事件流传播
触发事件监听器
执行回调函数

常见的事件类型:

事件类别 事件名称 触发时机
鼠标事件 clickdblclickmousedownmouseup 鼠标点击/按下/释放
鼠标移动 mousemovemouseentermouseleavemouseovermouseout 鼠标移动/进出元素
键盘事件 keydownkeyup 按下/释放键盘
表单事件 submitfocusblurchangeinput 表单交互
拖放事件 dragstartdragdragenddrop 拖拽操作
页面生命周期 DOMContentLoadedloadbeforeunloadunload 页面加载/卸载
资源事件 errorabortresizescroll 资源加载/窗口变化
剪贴板 copycutpaste 剪贴板操作

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(冒泡阶段)

注意 :在目标元素 上注册的监听器,无论 capturetrue 还是 false,都会在目标阶段触发(eventPhase === 2)。捕获和冒泡的区分只在非目标元素(祖先元素)上才有意义。

30.2.2 事件冒泡

事件冒泡 (Event Bubbling)是指事件从目标元素开始,逐级向上传播到父元素,直到 documentwindow

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

⑥ 最终到达

并非所有事件都冒泡 :大部分事件会冒泡,但有些事件不冒泡,如 focusblurmouseentermouseleaveloaderror 等。可以通过 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 只需一次
缺点 / 注意事项 说明
不适合不冒泡的事件 focusblurmouseentermouseleave 等不冒泡,需用 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
};

注意addEventListenerreturn 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 事件监听器

在移动端,touchmovewheel 事件的监听器中如果调用了 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?
不滚动
滚动

最佳实践 :对于 touchstarttouchmovewheel 等影响滚动的监听器,如果没有调用 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 哪个鼠标按键被点击

mousedownmouseup 事件的 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'));

关键点mousemovemouseup 应该绑定在 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 为什么需要指针事件

在移动设备上,除了鼠标还有触摸屏和手写笔。过去开发者需要同时处理鼠标事件(mousedownmousemovemouseup)和触摸事件(touchstarttouchmovetouchend),代码复杂且容易出错。

指针事件 (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 或监听 documentscroll 事件。

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 监听器
});

注意 :在现代浏览器中,wheeltouchmove 事件监听器默认被标记为 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 对象 typetarget(实际触发元素)、currentTarget(绑定监听器的元素)
事件流三阶段 ① 捕获(外→内)→ ② 目标 → ③ 冒泡(内→外)
事件冒泡 事件从 target 向上传播到 window,大部分事件冒泡
stopPropagation 阻止事件继续传播
stopImmediatePropagation 阻止传播 + 阻止同元素其他监听器
事件委托 利用冒泡在父元素统一处理子元素事件,结合 closest() 查找目标
preventDefault 阻止浏览器默认行为(链接跳转、表单提交等)
passive 声明不调用 preventDefault,优化滚动性能
自定义事件 new CustomEvent(type, { detail, bubbles, cancelable }) + dispatchEvent
鼠标事件 clickmousedown/upcontextmenumouseover/out(冒泡)、mouseenter/leave(不冒泡)
鼠标坐标 clientX/Y(视口)、pageX/Y(文档)、screenX/Y(屏幕)
鼠标按键 event.button:0=左键,1=中键,2=右键
拖放 API dragstartdragover(需 preventDefault)→drop,通过 dataTransfer 传递数据
指针事件 统一鼠标/触摸/手写笔,pointerType 区分设备,setPointerCapture 捕获指针
键盘事件 keydown(按下)、keyup(释放),keypress 已废弃
key vs code key 是字符(受布局影响),code 是物理位置(不受影响)
修饰键 ctrlKeyshiftKeyaltKeymetaKey
滚动事件 scroll 不冒泡,高频触发需用 rAF/防抖/节流优化
滚动控制 scrollToscrollByscrollIntoView(支持 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 规范

相关推荐
小大宇1 小时前
python milvus 案例
开发语言·python·milvus
Gauss松鼠会2 小时前
【GaussDB】GaussDB锁阻塞源头查询
java·开发语言·前端·数据库·算法·gaussdb·经验总结
mingo_敏2 小时前
DeepAgents : 后端(Backends)
java·开发语言
YHHLAI2 小时前
Vue 3 流式输出实战:从零掌握 LLM Streaming 与 SSE 协议
前端·javascript·vue.js
mabing9932 小时前
Qt QMessageBox、QDialogButtonBox中英文翻译动态切换
开发语言·qt
Sagittarius_A*2 小时前
【RCELABS】Level 17~18 —— PHP命令执行函数与环境变量注入
开发语言·安全·web安全·靶场·php·rce
冻柠檬飞冰走茶2 小时前
PTA基础编程题目集 7-19 支票面额(C语言实现)
c语言·开发语言·数据结构·算法
破碎的南瓜3 小时前
靶场中使用到的php函数以及每种漏洞的防御方式
开发语言·php
不简说3 小时前
JS 代码技巧 vol.10 — 20 个 V8 引擎原理,解释"为什么这样写快"
前端·javascript·面试