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.target
vse.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