前端事件机制入门到精通:事件流、冒泡捕获与事件委托全解析

1--4 事件流(Event Flow)、事件捕获、事件冒泡、DOM 事件流(示例)

概念要点

  • 事件流(Event flow)通常分 3 个阶段:捕获(capturing)→ 目标(at target)→ 冒泡(bubbling)
  • 默认 addEventListener(type, fn)冒泡阶段 (等价于第三个参数 false)。如果传 true 则在捕获阶段触发。
  • 有些事件不冒泡(例如 focus/blur),但有对应的 focusin/focusout 会冒泡。

示例(直观观察捕获/目标/冒泡)

xml 复制代码
<!-- 保存为 demo.html,打开控制台点击按钮 -->
<div id="outer" style="padding:20px; border:2px solid #f00;">
  OUTER
  <div id="middle" style="padding:20px; border:2px solid #0a0;">
    MIDDLE
    <button id="btn">点击我</button>
  </div>
</div>

<script>
  function logger(e) {
    const phase = e.eventPhase; // 1=capturing,2=at target,3=bubbling
    const pText = phase === 1 ? 'capturing' : phase === 2 ? 'at-target' : 'bubbling';
    console.log(this.id || this.tagName, e.type, pText);
  }

  document.getElementById('outer').addEventListener('click', logger, true);  // 捕获
  document.getElementById('middle').addEventListener('click', logger, true); // 捕获
  document.getElementById('btn').addEventListener('click', logger);         // 默认(冒泡阶段/目标处)
  document.getElementById('middle').addEventListener('click', logger);      // 冒泡
  document.getElementById('outer').addEventListener('click', function(e){
    console.log('outer second handler (bubbling)'); 
  }); // 演示多 handler
</script>

说明 / 常见操作

  • e.stopPropagation():阻止向上(或向下)传播(不影响同一元素上其它 handler,除非用 stopImmediatePropagation())。
  • e.stopImmediatePropagation():阻止该事件再触发该元素上的其它监听器。
  • e.preventDefault():阻止浏览器默认动作(可用于阻止表单提交、链接跳转、右键菜单等;当 event.cancelable === false 时无效)。

5 HTML/DOM 事件处理程序(类型概述)与示例

常见有三类处理方式(按历史和能力区分):

  1. HTML 内联处理器 (HTML attributes):<button onclick="doIt()">
  2. DOM0(属性式)处理器el.onclick = fn ------ 只能保存 一个 处理器,会被覆盖。
  3. DOM2(标准)处理器el.addEventListener(type, fn, options) ------ 推荐,支持多监听器、阶段/选项、可移除。
  4. IE 老 APIel.attachEvent('onclick', fn)(IE 专用,this 指向 window,已废弃/被移除)。

示例:三种写法对比

xml 复制代码
<button id="a" onclick="console.log('inline handler', this.id)">Inline</button>

<script>
  const b = document.getElementById('b') || document.createElement('button');
  b.id = 'b';
  b.textContent = 'DOM0';
  document.body.appendChild(b);

  // DOM0(属性)
  b.onclick = function(){ console.log('DOM0 handler', this.id); };

  // DOM2(推荐)
  const c = document.createElement('button');
  c.id = 'c'; c.textContent = 'DOM2';
  document.body.appendChild(c);

  function onClick(e) { console.log('DOM2 handler', this.id); }
  c.addEventListener('click', onClick);          // 添加
  // c.removeEventListener('click', onClick);    // 移除(必须传同一个函数引用)
</script>

6 with 在事件处理程序中的用法与问题

  • with 是 JavaScript 的语法糖:with(obj){ ... } 在代码块中可以直接访问 obj 的属性作为局部变量。with 被废弃且在 strict mode 中禁止,会让作用域难以阅读与调试。
  • 事件处理 中有人旧式用法写 with (this) { value = something },但强烈不推荐。现代代码不要使用 with,用直接访问或解构代替。

7 DOM0 事件处理程序(on 开头的事件)详解与示例

  • DOM0 即元素属性如 el.onclick = fn。它相当简单但功能受限(单 handler,无法指定捕获/冒泡选项)。
  • 注意移除el.onclick = null
ini 复制代码
const el = document.querySelector('#some');
function handler(){ console.log('clicked'); }
el.onclick = handler;      // 设置
el.onclick = null;         // 移除

8 DOM2 事件(addEventListener / removeEventListener)------添加与移除详解

  • 标准:addEventListener(type, listener, options)options 可以是布尔(capture)或对象 {capture, once, passive}

    • capture:是否在捕获阶段触发。
    • once:调用一次后自动移除。
    • passive:告诉浏览器监听器不会调用 preventDefault()(用于滚动/触摸性能优化)。
  • 移除要求removeEventListener(type, sameFunction, sameCapture) ------ 必须传 同一个函数引用同样的捕获标志 才能真正移除。匿名函数无法被移除。

示例

javascript 复制代码
function onScroll(e){ /* ... */ }
window.addEventListener('scroll', onScroll, {passive:true}); // 性能友好
// later:
window.removeEventListener('scroll', onScroll, {passive:true}); // 与添加时的选项匹配

9 IE 事件处理程序(attachEvent / detachEvent / window.event)

  • 旧 IE(IE8 及更早)使用 attachEvent('on' + type, handler):没有捕获阶段,this 指向 window 而非元素,事件对象通过全局 window.event 获得。
  • IE9+ 开始支持标准 addEventListener,IE11 中 attachEvent 被移除或不推荐。建议现代项目不再使用 attachEventMicrosoft Learn

兼容写法(封装)

rust 复制代码
function addEvent(el, type, fn, options) {
  if (el.addEventListener) el.addEventListener(type, fn, options || false);
  else if (el.attachEvent) el.attachEvent('on' + type, fn); // old IE
  else el['on' + type] = fn;
}
function removeEvent(el, type, fn, options){
  if (el.removeEventListener) el.removeEventListener(type, fn, options || false);
  else if (el.detachEvent) el.detachEvent('on' + type, fn); // old IE
  else el['on' + type] = null;
}

10 跨浏览器事件处理(兼容问题与范例)

常见差异

  • 事件对象位置:e(传入) vs window.event(IE);目标元素 e.target vs e.srcElement
  • preventDefault() vs returnValue = false
  • stopPropagation() vs cancelBubble = true
    兼容化示例
ini 复制代码
function normalizeEvent(e) {
  e = e || window.event;
  if (!e.target) e.target = e.srcElement;
  if (!e.preventDefault) e.preventDefault = function(){ e.returnValue = false; };
  if (!e.stopPropagation) e.stopPropagation = function(){ e.cancelBubble = true; };
  return e;
}

11 事件对象(Event object)------属性与方法详解与示例

常用属性

  • type:事件类型("click")。
  • target:事件最初的目标元素。
  • currentTarget:当前正在处理事件的元素(this / addEventListener 注册的元素)。
  • eventPhase:1(捕获)、2(目标)、3(冒泡)。
  • bubblescancelableisTrustedtimeStampdefaultPrevented
  • 鼠标相关:clientX/YpageX/YscreenX/YbuttonbuttonsrelatedTargetdetail(点击次数)。
  • 键盘相关:key(建议使用)、codekeyCode(历史兼容)、charCode(历史)。
    常用方法
  • preventDefault()stopPropagation()stopImmediatePropagation()composedPath()

示例:阻止链接默认跳转并停止冒泡

xml 复制代码
<a id="link" href="https://example.com">链接</a>
<script>
  document.getElementById('link').addEventListener('click', function(e){
    e.preventDefault();       // 阻止跳转
    e.stopPropagation();      // 阻止冒泡
    console.log('link clicked, but no navigation');
  });
</script>

12 IE 事件对象(IE 特有属性与方法)

  • IE(旧)事件对象通过 window.event 提供:常见字段 srcElement(代替 target)、fromElement/toElement(代替 relatedTarget)、returnValue(代替 preventDefault)、cancelBubble(代替 stopPropagation)、keyCode 等。
  • 迁移要点:在跨浏览器处理时做 e = e || window.event; target = e.target || e.srcElement;

13 跨浏览器事件对象兼容问题(示例)

见上面 normalize 示例。再给一个完整处理函数:

javascript 复制代码
function handler(e) {
  e = e || window.event;
  var target = e.target || e.srcElement;
  // 防止默认
  if (e.preventDefault) e.preventDefault(); else e.returnValue = false;
  // stop
  if (e.stopPropagation) e.stopPropagation(); else e.cancelBubble = true;
}

14 事件类型(分类并举例)

在 JS/DOM 中常见的事件类型(示例):

  • 用户界面事件(UI Events)loadunloadresizescroll 等。
  • 焦点事件(Focus Events)focusblur(不冒泡);focusinfocusout(会冒泡)。
  • 鼠标事件(Mouse Events)clickdblclickmousedownmouseupmousemovemouseovermouseoutmouseentermouseleave
  • 滚轮事件 :标准为 wheel(推荐);历史还有 mousewheel(非标准)和 DOMMouseScroll(早期 Firefox)。
  • 输入事件/表单事件inputchangesubmitreset
  • 键盘事件keydownkeypress(已逐步弃用) 、keyup
  • 合成/组合事件compositionstartcompositionupdatecompositionend(IME 输入)。
  • 触摸/指针事件touchstarttouchmovetouchend;现代推荐使用 pointerdown 等指针事件以统一输入。MDN网络文档

15 用户界面事件(load、unload、resize、scroll)

load

javascript 复制代码
window.addEventListener('load', () => console.log('全部资源加载完成(包括图片)'));
document.addEventListener('DOMContentLoaded', () => console.log('DOM 已解析(不等图片)'));

unload / beforeunload

  • beforeunload 可用于提示用户离开(现代浏览器限制自定义消息),示例:
ini 复制代码
window.addEventListener('beforeunload', function(e){
  e.preventDefault();
  e.returnValue = ''; // 很多浏览器只显示默认提示
});

resize / scroll

  • 这类高频事件要做防抖/节流或使用 {passive:true} 优化滚动监听,以提升性能。

16 焦点事件(focus / blur)触发方式(示例)

  • focus/blur 不冒泡。要在父级捕获到焦点事件使用 focusin/focusout 或在捕获阶段监听:
javascript 复制代码
document.addEventListener('focus', (e)=>console.log('focus', e.target), true); // 捕获阶段能捕获到 focus
document.addEventListener('focusin', (e)=>console.log('focusin bubble', e.target));

17 鼠标事件(简述)

鼠标可以产生:clickdblclickmousedownmouseupmousemovemouseentermouseleavemouseovermouseout,以及 contextmenu(右键菜单)等。通常通过 clientX/YpageX/YscreenX/Y 获取坐标。


18 滚轮事件、坐标与修饰键、相关元素、mousewheel 兼容、触摸设备、无障碍

坐标

  • clientX/Y:相对于可视窗口(不含滚动)。
  • pageX/Y:相对于整个文档(包含滚动)。
  • screenX/Y:相对于屏幕。

修饰键

  • e.shiftKey, e.ctrlKey, e.altKey, e.metaKey(⌘ / Windows key)。

按钮

  • e.button(历史;左 0 / 中 1 / 右 2),e.buttons(按键位掩码,一次可多个)。

相关元素(relatedTarget)

  • mouseover/mouseout 中说明进入/离开的另一个元素。

mousewheel/wheel 兼容

  • 推荐使用标准 wheel 事件。不同浏览器历史上使用 wheelDeltadetaildeltaY 等属性。要写兼容代码时检测 deltaYwheelDeltaMDN网络文档+1

触摸屏与 pointer 事件

  • touchstart/touchmove/touchend:触摸设备专用(多点触控需处理 touches、changedTouches)。
  • 现代推荐使用 Pointer Events(pointerdown/pointermove 等) ,它统一鼠标、触控、笔输入。MDN网络文档

无障碍(A11y)

  • 对鼠标事件提供键盘等价(Enter / Space),使用 roletabindex、并确保 focus 样式与可访问性提示。

19 键盘与输入事件(keydown, keypress, keyup 等)

要点

  • keydown/keyup:物理按键触发(持续按下会重复 keydown)。
  • keypress:历史上用于字符输入,但行为不一致并逐步弃用,不建议新代码依赖它。
  • 使用 e.key(字符)和 e.code(物理按键)比 keyCode 更可靠。
  • event.location(DOM3) 表示按键位置(如左/右 Ctrl、数字键盘)。

示例

javascript 复制代码
document.addEventListener('keydown', function(e){
  console.log('key:', e.key, 'code:', e.code, 'location:', e.location);
  if (e.key === 'Enter') { /* ... */ }
});

20 合成事件(composition events)

  • compositionstart/compositionupdate/compositionend:处理输入法(IME)输入(中文/日文/韩文)时的中间状态,监听这些事件可在输入提交前获得更细粒度控制。

21 DOM2 变化(通知/事件 API 的演进)

  • DOM Level 2 引入 addEventListener 与事件阶段(捕获/冒泡),从而支持更灵活的事件处理(替代早期 DOM0/IE 特有方法)。

22 HTML5 事件(新事件和变化)

  • 新增 inputbeforeinputpointer* 事件、pagevisibility(页面可见性)、hashchangepopstate 等,提高对现代 web 应用的支持(例如 SPA、移动设备交互等)。

23 事件委托(Event Delegation)------概念、场景与示例

概念 :把多个子元素的事件处理器集中注册在公共父元素上,通过事件冒泡并判断 event.targettarget.closest(selector) 来决定具体目标,从而减少监听器数量、支持动态元素。
优点:节省内存/监听器,支持动态添加的子元素。

示例(列表委托)

xml 复制代码
<ul id="list">
  <li data-id="1">A</li>
  <li data-id="2">B</li>
</ul>

<script>
  document.getElementById('list').addEventListener('click', function(e){
    const li = e.target.closest('li');
    if (!li || !this.contains(li)) return;
    console.log('clicked item', li.dataset.id);
  });
</script>

特殊事件

  • contextmenu(右键):可用来定制右键菜单,但需注意可访问性与用户期望。
  • beforeunload:用于离开页面的确认(现代浏览器限制自定义文本)。
  • DOMContentLoaded:DOM 可操作时触发(比 load 更早)。
  • readystatechange:document 状态变更(loading / interactive / complete)。
  • pageshow / pagehide:页面被显示/隐藏(包括 bfcache 回归场景)。
  • hashchange:URL hash 变化(SPA 常用)。

24 设备事件(orientationchange、devicemotion、deviceorientation)

orientationchange

javascript 复制代码
window.addEventListener('orientationchange', () => {
  console.log('orientation changed', window.orientation);
});

deviceorientation / devicemotion

  • deviceorientation 提供设备朝向角(alpha/beta/gamma)。
  • devicemotion 提供加速度/加速度变化/旋转速率。
  • 兼容性与权限(移动浏览器通常需要用户授权)需注意。

25 触摸与手势事件

  • 触摸事件touchstart / touchmove / touchend,要处理 touches/changedTouches 数组。
  • 手势 :浏览器原生手势事件(如 iOS 的 gesturestart 等)并非所有平台都支持;通常使用 Pointer Events 或借助库(Hammer.js 等)做复杂手势识别。
  • 推荐:优先考虑 Pointer Events 以便统一输入模型。

26 事件参考(官方/权威资料)

(更多权威条目可在 MDN 上按需检索:Events / API 文档非常齐全。)


27 内存与性能(大量事件的影响与优化)

问题

  • 页面上为大量元素分别绑定事件会造成大量函数引用与内存占用,尤其在单页应用里频繁创建/销毁 DOM 时容易导致内存泄漏(如果 handler 闭包引用了外部对象,且没有正确移除 handler,则 GC 无法释放)。
    优化手段
  • 使用事件委托减少监听器总数。
  • 使用 passive: true 在滚动/触摸监听上提升性能(告诉浏览器不会阻止默认行为)。
  • 使用 once: true 自动移除只需处理一次的监听器。
  • 在销毁元素时显式 removeEventListener
  • 对高频事件(scroll, resize, mousemove)使用节流(throttle)或防抖(debounce)。

示例:用 once 自动移除

php 复制代码
el.addEventListener('click', handler, { once: true });

28 删除事件处理程序(方式与兼容问题)

  • DOM2:removeEventListener(type, handler, options) ------ 必须与 addEventListener 时传入的同一 handler 引用与相同的 capture 值(或者相同 options)匹配。
  • DOM0:el.onclick = null
  • IE:detachEvent('on' + type, handler)

29 事件模拟(synthetic events)与用例

用途 :测试、自动化、程序性触发行为(例如用代码模拟用户点击)。
标准 API(现代)

php 复制代码
// 简单事件
el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));

// 鼠标事件
el.dispatchEvent(new MouseEvent('click', { bubbles:true, clientX:10 }));

// 自定义事件
const ev = new CustomEvent('myapp:done', { detail: { id:123 }, bubbles:true });
el.dispatchEvent(ev);

注意:程序创建的事件多数是不"trusted"的(不是浏览器直接产生),某些默认动作可能不会被触发(例如某些安全敏感的默认动作)。

老 API(兼容)

javascript 复制代码
// 旧式 createEvent / initMouseEvent(仍有些旧浏览器支持)
var evt = document.createEvent('MouseEvents');
evt.initMouseEvent('click', true, true, window, 1, 0,0,0,0, false,false,false,false, 0, null);
el.dispatchEvent(evt);

// IE (createEventObject / fireEvent)
var evIE = document.createEventObject();
el.fireEvent('onclick', evIE);

30 DOM 事件模拟(鼠标/键盘/其它)详解

  • 鼠标new MouseEvent('click', {clientX, clientY, button, bubbles:true})
  • 键盘new KeyboardEvent('keydown', {key:'a', code:'KeyA', keyCode:65, bubbles:true}) ------ 注意:部分浏览器出于安全理由限制可设置字段(如 keyCode)或把合成事件标记为不可信。
  • 其他new Event('input') / new CustomEvent()
  • React/框架:框架可能封装合成事件(React 的 SyntheticEvent),直接 dispatch DOM 事件在框架层面未必触发框架的事件机制(要注意)。

31 IE 事件模拟(createEventObject)

  • IE 专用:var ev = document.createEventObject(); el.fireEvent('onclick', ev);。旧代码库中还会见到这种写法,现代不再推荐。

32 事件循环(Event Loop)------事件队列与执行顺序(示例)

关键点

  • 浏览器 JS 有 任务队列(macrotasks)微任务队列(microtasks)
  • 在一次事件处理(task)执行完毕后,会清空微任务队列(比如 Promise.then 回调),然后再执行下一个 macrotask(例如下一个 setTimeout 回调或下一个用户事件)。

示例(看顺序)

xml 复制代码
<button id="b">click</button>
<script>
  const b = document.getElementById('b');
  b.addEventListener('click', () => {
    console.log('handler start');
    Promise.resolve().then(()=>console.log('microtask'));
    setTimeout(()=>console.log('macrotask'), 0);
    console.log('handler end');
  });
  // 点击按钮观察打印顺序:
  // handler start
  // handler end
  // microtask
  // macrotask
</script>

说明:事件(如 click)的处理被视为一个 macrotask;其中产生的微任务会在该 macrotask 结束后立即执行。


总结与实践建议(精简)

  1. 永远优先使用标准 API addEventListener / removeEventListenerMDN网络文档
  2. 使用事件委托以减少监听器数量(提高性能);对滚动/触摸使用 {passive:true}
  3. 处理兼容:e = e || window.eventtarget = e.target || e.srcElementpreventDefault / returnValue
  4. 现代交互优先使用 Pointer Events(统一鼠标/触摸/笔)。MDN网络文档
  5. 模拟事件用 new Event()/MouseEvent()/KeyboardEvent()/CustomEvent(),注意浏览器对"trusted"事件的限制。MDN网络文档+1

参考(可点开阅读)

相关推荐
Pedantic3 分钟前
swiftUI视图修改器(ViewModifier)解析
前端
yukin7 分钟前
一文搞懂JS类型转换!!!
前端
数字人直播8 分钟前
干货分享:AI 数字人直播怎么做才能适配多平台规则?
前端·后端
胡gh8 分钟前
中断渲染,利用fiber解决性能问题,性能优化又有的说了
前端·javascript·面试
日月晨曦8 分钟前
JavaScript原型:对象世界的"族谱"与"共享仓库"
前端
AliciaIr9 分钟前
前端面试:红绿灯问题与异步编程的底层实践
前端·javascript
日月晨曦11 分钟前
从XMLHttpRequest到Fetch:前后端通信的"进化史"
前端
已读不回14311 分钟前
移动端视口终极解决方案:使用 Visual Viewport封装一个优雅的 React Hook
前端·javascript·react.js
PineappleCoder12 分钟前
同源策略是啥?浏览器为啥拦我的跨域请求?(二)
前端·后端·node.js
洋流13 分钟前
0基础进大厂,第13天:Promise:你先等等我
前端·javascript·面试