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 事件处理程序(类型概述)与示例
常见有三类处理方式(按历史和能力区分):
- HTML 内联处理器 (HTML attributes):
<button onclick="doIt()"> - DOM0(属性式)处理器 :
el.onclick = fn------ 只能保存 一个 处理器,会被覆盖。 - DOM2(标准)处理器 :
el.addEventListener(type, fn, options)------ 推荐,支持多监听器、阶段/选项、可移除。 - IE 老 API :
el.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被移除或不推荐。建议现代项目不再使用attachEvent。Microsoft 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(传入) vswindow.event(IE);目标元素e.targetvse.srcElement。 preventDefault()vsreturnValue = false。stopPropagation()vscancelBubble = 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(冒泡)。bubbles、cancelable、isTrusted、timeStamp、defaultPrevented。- 鼠标相关:
clientX/Y、pageX/Y、screenX/Y、button、buttons、relatedTarget、detail(点击次数)。 - 键盘相关:
key(建议使用)、code、keyCode(历史兼容)、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) :
load、unload、resize、scroll等。 - 焦点事件(Focus Events) :
focus、blur(不冒泡);focusin、focusout(会冒泡)。 - 鼠标事件(Mouse Events) :
click、dblclick、mousedown、mouseup、mousemove、mouseover、mouseout、mouseenter、mouseleave。 - 滚轮事件 :标准为
wheel(推荐);历史还有mousewheel(非标准)和DOMMouseScroll(早期 Firefox)。 - 输入事件/表单事件 :
input、change、submit、reset。 - 键盘事件 :
keydown、keypress(已逐步弃用) 、keyup。 - 合成/组合事件 :
compositionstart、compositionupdate、compositionend(IME 输入)。 - 触摸/指针事件 :
touchstart、touchmove、touchend;现代推荐使用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 鼠标事件(简述)
鼠标可以产生:click、dblclick、mousedown、mouseup、mousemove、mouseenter、mouseleave、mouseover、mouseout,以及 contextmenu(右键菜单)等。通常通过 clientX/Y、pageX/Y、screenX/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事件。不同浏览器历史上使用wheelDelta、detail、deltaY等属性。要写兼容代码时检测deltaY或wheelDelta。MDN网络文档+1
触摸屏与 pointer 事件
touchstart/touchmove/touchend:触摸设备专用(多点触控需处理 touches、changedTouches)。- 现代推荐使用 Pointer Events(pointerdown/pointermove 等) ,它统一鼠标、触控、笔输入。MDN网络文档
无障碍(A11y)
- 对鼠标事件提供键盘等价(Enter / Space),使用
role、tabindex、并确保 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 事件(新事件和变化)
- 新增
input、beforeinput、pointer*事件、pagevisibility(页面可见性)、hashchange、popstate等,提高对现代 web 应用的支持(例如 SPA、移动设备交互等)。
23 事件委托(Event Delegation)------概念、场景与示例
概念 :把多个子元素的事件处理器集中注册在公共父元素上,通过事件冒泡并判断 event.target 或 target.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 总览(权威、常用):MDN DOM Events。MDN网络文档
- MDN ---
addEventListener()(标准 API,推荐使用)。MDN网络文档 - MDN --- Pointer Events(统一鼠标/触摸/笔)。MDN网络文档
- MDN --- WheelEvent(鼠标滚轮,标准)。MDN网络文档
- MDN --- CustomEvent(自定义事件与
detail)。MDN网络文档
(更多权威条目可在 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 结束后立即执行。
总结与实践建议(精简)
- 永远优先使用标准 API
addEventListener/removeEventListener。MDN网络文档 - 使用事件委托以减少监听器数量(提高性能);对滚动/触摸使用
{passive:true}。 - 处理兼容:
e = e || window.event,target = e.target || e.srcElement,preventDefault / returnValue。 - 现代交互优先使用 Pointer Events(统一鼠标/触摸/笔)。MDN网络文档
- 模拟事件用
new Event()/MouseEvent()/KeyboardEvent()/CustomEvent(),注意浏览器对"trusted"事件的限制。MDN网络文档+1