说说JavaScript中的事件模型?
问题解析
事件是 JavaScript 与 HTML 交互的核心机制。理解事件模型对于处理用户交互、优化页面性能至关重要。JavaScript 中的事件模型经历了从 DOM0 到 DOM2/3 的发展,同时还需要考虑 IE 的兼容性。
核心概念
事件流
事件流描述了页面接收事件的顺序。事件流分为三个阶段:
- 事件捕获阶段(Capture Phase):事件从文档根节点向下传播到目标元素
- 目标阶段(Target Phase):事件到达目标元素
- 事件冒泡阶段(Bubbling Phase):事件从目标元素向上冒泡到文档根节点
捕获阶段 目标阶段 冒泡阶段
┌─────────────────────────────────────────────────────┐
│ │
document document
│ ▲
│ │
html html
│ ▲
│ │
body body
│ ▲
│ ┌─────────────┐ │
└──────────→│ target │─────────────────────────┘
│ (div) │
└─────────────┘
详细解答
一、事件模型分类
1. DOM0 级事件模型(原始事件模型)
// 方式1:HTML 内联事件处理程序(不推荐)
// <button onclick="handleClick()">点击</button>
// 方式2:JavaScript 属性赋值
const btn = document.getElementById('btn');
// 绑定事件
btn.onclick = function() {
console.log('按钮被点击');
console.log(this); // 指向触发事件的元素
};
// 移除事件
btn.onclick = null;
特点:
- 绑定速度快,跨浏览器兼容性好
- 同一个事件只能绑定一个处理函数(后绑定的会覆盖先绑定的)
- 只支持冒泡阶段
- 事件处理函数中的
this指向触发事件的元素
2. DOM2 级事件模型(标准事件模型)
const btn = document.getElementById('btn');
// 添加事件监听
btn.addEventListener('click', function(event) {
console.log('点击事件1');
}, false); // false 表示在冒泡阶段处理(默认)
// 同一个事件可以绑定多个处理函数
btn.addEventListener('click', function(event) {
console.log('点击事件2');
}, false);
// 在捕获阶段处理事件
btn.addEventListener('click', function(event) {
console.log('捕获阶段点击');
}, true);
// 移除事件监听
function handler() {
console.log('可以被移除的处理函数');
}
btn.addEventListener('click', handler);
btn.removeEventListener('click', handler);
// 注意:移除时必须传入相同的函数引用
特点:
- 可以绑定多个事件处理函数
- 可以指定在捕获或冒泡阶段处理
- 更灵活,是推荐的方式
3. IE 事件模型(已过时,了解即可)
const btn = document.getElementById('btn');
// IE8 及以下
btn.attachEvent('onclick', function() {
console.log('IE 点击事件');
});
// 移除事件
btn.detachEvent('onclick', handler);
// 注意:
// 1. 事件名需要加 'on' 前缀
// 2. 只支持冒泡阶段
// 3. 处理函数中的 this 指向 window
二、事件对象
document.getElementById('btn').addEventListener('click', function(event) {
// event 就是事件对象
// 常用属性
console.log(event.type); // 事件类型:"click"
console.log(event.target); // 触发事件的目标元素
console.log(event.currentTarget); // 当前绑定事件的元素
console.log(event.bubbles); // 事件是否冒泡
console.log(event.cancelable); // 事件是否可以取消默认行为
// 鼠标事件特有属性
console.log(event.clientX); // 鼠标在视口中的 X 坐标
console.log(event.clientY); // 鼠标在视口中的 Y 坐标
console.log(event.pageX); // 鼠标在页面中的 X 坐标(包含滚动)
console.log(event.pageY); // 鼠标在页面中的 Y 坐标
console.log(event.button); // 按下的鼠标按钮(0: 左键, 1: 中键, 2: 右键)
// 键盘事件特有属性
console.log(event.key); // 按下的键名
console.log(event.keyCode); // 键码(已废弃)
console.log(event.code); // 物理键位
console.log(event.ctrlKey); // Ctrl 是否按下
console.log(event.shiftKey); // Shift 是否按下
console.log(event.altKey); // Alt 是否按下
});
三、事件传播控制
document.getElementById('btn').addEventListener('click', function(event) {
// 阻止事件冒泡
event.stopPropagation();
// 阻止事件捕获和传播(彻底停止)
event.stopImmediatePropagation();
// 阻止默认行为
event.preventDefault();
// 检查默认行为是否被阻止
console.log(event.defaultPrevented);
});
// 阻止默认行为的其他方式
// 1. return false(仅对 DOM0 级事件有效)
btn.onclick = function() {
return false; // 阻止默认行为 + 阻止冒泡
};
四、事件委托
// ❌ 不好的做法:为每个子元素绑定事件
const items = document.querySelectorAll('li');
items.forEach(item => {
item.addEventListener('click', function() {
console.log(this.textContent);
});
});
// ✅ 好的做法:使用事件委托
const list = document.getElementById('list');
list.addEventListener('click', function(event) {
// 检查点击的是否是 li 元素
if (event.target.tagName === 'LI') {
console.log(event.target.textContent);
}
// 或使用 closest 方法
const li = event.target.closest('li');
if (li && list.contains(li)) {
console.log(li.textContent);
}
});
深入理解
事件执行顺序示例
<div id="outer">
<div id="inner">
<button id="btn">点击</button>
</div>
</div>
<script>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
const btn = document.getElementById('btn');
// 捕获阶段
outer.addEventListener('click', () => console.log('outer 捕获'), true);
inner.addEventListener('click', () => console.log('inner 捕获'), true);
btn.addEventListener('click', () => console.log('btn 捕获'), true);
// 目标阶段(按绑定顺序执行)
btn.addEventListener('click', () => console.log('btn 目标1'), false);
btn.addEventListener('click', () => console.log('btn 目标2'), false);
// 冒泡阶段
btn.addEventListener('click', () => console.log('btn 冒泡'), false);
inner.addEventListener('click', () => console.log('inner 冒泡'), false);
outer.addEventListener('click', () => console.log('outer 冒泡'), false);
// 点击按钮后的输出顺序:
// outer 捕获
// inner 捕获
// btn 捕获
// btn 目标1
// btn 目标2
// btn 冒泡
// inner 冒泡
// outer 冒泡
</script>
自定义事件
// 创建自定义事件
const customEvent = new Event('myEvent', {
bubbles: true, // 是否冒泡
cancelable: true // 是否可以取消
});
// 创建带数据的自定义事件
const customEventWithData = new CustomEvent('userLogin', {
bubbles: true,
cancelable: true,
detail: { // 自定义数据
username: 'Alice',
timestamp: Date.now()
}
});
// 监听自定义事件
document.addEventListener('userLogin', function(event) {
console.log('用户登录:', event.detail.username);
});
// 触发自定义事件
document.dispatchEvent(customEventWithData);
事件循环与宏任务/微任务
console.log('1');
document.addEventListener('click', function() {
console.log('click');
});
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
console.log('2');
// 输出顺序:
// 1
// 2
// promise(微任务)
// timeout(宏任务)
// 如果点击:click(宏任务,在 timeout 之后触发)
最佳实践
1. 优先使用 addEventListener
// ✅ 推荐
btn.addEventListener('click', handler);
// ❌ 不推荐(会被覆盖)
btn.onclick = handler;
2. 使用事件委托处理大量元素
// 处理动态添加的元素时特别有用
document.getElementById('list').addEventListener('click', function(e) {
if (e.target.matches('li.item')) {
// 处理点击
}
});
3. 及时移除不需要的事件监听
function handler() {
// 处理逻辑
}
element.addEventListener('click', handler);
// 组件销毁时移除监听
element.removeEventListener('click', handler);
4. 使用 passive 事件监听器优化滚动性能
// 告诉浏览器不会调用 preventDefault()
window.addEventListener('scroll', handler, { passive: true });
// 适用于 touchstart, touchmove, wheel 等事件
5. 使用 once 选项自动移除监听
// 事件只触发一次,自动移除监听
element.addEventListener('click', handler, { once: true });
面试要点
-
什么是事件流?
- 事件捕获阶段:从文档根节点向下到目标元素
- 目标阶段:到达目标元素
- 事件冒泡阶段:从目标元素向上到文档根节点
-
DOM0、DOM2 事件模型的区别?
- DOM0:属性赋值,只能绑定一个处理函数,只支持冒泡
- DOM2:addEventListener,可绑定多个,可选择捕获/冒泡
-
事件委托是什么?有什么优点?
- 将事件绑定到父元素,利用冒泡机制处理子元素事件
- 优点:减少内存占用、动态添加的元素也能响应
-
如何阻止事件冒泡和默认行为?
- 阻止冒泡:
event.stopPropagation() - 阻止默认行为:
event.preventDefault() - 两者都阻止:
return false(DOM0 级)
- 阻止冒泡:
-
target 和 currentTarget 的区别?
- target:触发事件的实际元素
- currentTarget:绑定事件的元素
-
什么是自定义事件?
- 使用 Event 或 CustomEvent 创建
- 可以携带自定义数据
- 通过 dispatchEvent 触发