JavaScript 事件处理
事件是用户或浏览器执行的某种动作,如点击、提交表单、页面加载等。JavaScript 通过事件处理程序来响应这些动作,实现与用户的交互。
事件基础
什么是事件
事件是浏览器或用户执行的某种动作,JavaScript 可以"监听"这些事件并执行相应的代码。常见的事件包括:
- 鼠标事件:click、dblclick、mouseenter、mouseleave、mousemove
- 键盘事件:keydown、keyup、keypress
- 表单事件:submit、change、input、focus、blur
- 窗口事件:load、resize、scroll、unload
事件流
当事件发生时,事件会在 DOM 树中传播,这个过程称为事件流。事件流分为三个阶段:
- 捕获阶段:事件从 window 向下传播到目标元素
- 目标阶段:事件到达目标元素
- 冒泡阶段:事件从目标元素向上传播到 window
捕获阶段:window → document → body → div → button
↓
目标阶段: button(事件目标)
↓
冒泡阶段:button → div → body → document → window
大多数事件都会冒泡,但 focus、blur 等事件不会冒泡。
事件绑定方式
HTML 属性方式(不推荐)
直接在 HTML 元素上添加事件属性:
<button onclick="alert('点击了')">点击我</button>
<button onclick="handleClick()">点击我</button>
<script>
function handleClick() {
console.log("按钮被点击了");
}
</script>
这种方式不推荐使用,因为 HTML 和 JavaScript 代码混在一起,不利于维护。
DOM 属性方式
通过 DOM 元素的属性绑定事件:
const button = document.getElementById("myButton");
button.onclick = function() {
console.log("按钮被点击了");
};
button.onclick = function() {
console.log("新的事件处理程序"); // 会覆盖前面的
};
这种方式简单,但同一个事件只能绑定一个处理程序。
addEventListener 方法(推荐)
使用 addEventListener 方法绑定事件,这是最推荐的方式:
const button = document.getElementById("myButton");
button.addEventListener("click", function() {
console.log("第一个处理程序");
});
button.addEventListener("click", function() {
console.log("第二个处理程序");
});
button.addEventListener("click", handleClick);
function handleClick() {
console.log("命名函数处理程序");
}
addEventListener 的优势:
- 可以为同一事件绑定多个处理程序
- 可以控制事件在捕获阶段还是冒泡阶段触发
- 可以动态移除事件处理程序
addEventListener 语法
element.addEventListener(event, handler, options);
element.addEventListener(event, handler, useCapture);
参数说明:
event:事件名称(字符串),如 "click"、"mouseover"handler:事件处理函数useCapture:布尔值,true 表示在捕获阶段触发,false(默认)表示在冒泡阶段触发options:配置对象,可包含以下属性:capture:是否在捕获阶段触发once:是否只触发一次passive:是否永远不会调用preventDefault()
const button = document.querySelector("#myButton");
button.addEventListener("click", handleClick, {
once: true, // 只触发一次后自动移除
capture: false, // 冒泡阶段触发
passive: true // 不会调用 preventDefault
});
function handleClick(event) {
console.log("按钮被点击了");
}
移除事件监听
使用 removeEventListener 移除事件处理程序:
function handleClick() {
console.log("点击了");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);
注意:移除事件时,处理函数必须是同一个引用。匿名函数无法被移除:
button.addEventListener("click", function() {
console.log("点击了");
});
button.removeEventListener("click", function() {
console.log("点击了");
});
事件对象
当事件触发时,事件处理函数会接收一个事件对象(Event Object),包含事件的详细信息。
常用属性
button.addEventListener("click", function(event) {
console.log(event.type);
console.log(event.target);
console.log(event.currentTarget);
console.log(event.timeStamp);
console.log(event.clientX, event.clientY);
});
常用属性说明:
| 属性 | 说明 |
|---|---|
type | 事件类型,如 "click"、"mouseover" |
target | 触发事件的元素(事件的目标元素) |
currentTarget | 绑定事件处理程序的元素 |
timeStamp | 事件发生的时间戳 |
bubbles | 事件是否冒泡 |
cancelable | 事件是否可以取消默认行为 |
鼠标事件特有属性
document.addEventListener("click", function(event) {
console.log("相对于视口:", event.clientX, event.clientY);
console.log("相对于页面:", event.pageX, event.pageY);
console.log("相对于屏幕:", event.screenX, event.screenY);
console.log("相对于目标元素:", event.offsetX, event.offsetY);
console.log("按下的按钮:", event.button);
console.log("按下的按钮组合:", event.buttons);
});
button 属性值:
- 0:左键
- 1:中键(滚轮)
- 2:右键
键盘事件特有属性
document.addEventListener("keydown", function(event) {
console.log("按键:", event.key);
console.log("按键代码:", event.code);
console.log("键码:", event.keyCode);
console.log("是否按住 Ctrl:", event.ctrlKey);
console.log("是否按住 Shift:", event.shiftKey);
console.log("是否按住 Alt:", event.altKey);
console.log("是否按住 Meta:", event.metaKey);
});
常用按键值:
document.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
console.log("按下了回车键");
}
if (event.key === "Escape") {
console.log("按下了 ESC 键");
}
if (event.ctrlKey && event.key === "s") {
event.preventDefault();
console.log("按下了 Ctrl+S");
}
});
事件冒泡与捕获
事件冒泡
事件从目标元素开始,逐级向上传播到父元素:
<div id="outer" style="padding: 50px; background: lightblue;">
外层 div
<div id="inner" style="padding: 30px; background: lightgreen;">
内层 div
<button id="btn">按钮</button>
</div>
</div>
<script>
document.getElementById("outer").addEventListener("click", function() {
console.log("外层 div 被点击");
});
document.getElementById("inner").addEventListener("click", function() {
console.log("内层 div 被点击");
});
document.getElementById("btn").addEventListener("click", function() {
console.log("按钮被点击");
});
</script>
点击按钮时,输出顺序:
按钮被点击
内层 div 被点击
外层 div 被点击
事件捕获
事件从最外层元素开始,逐级向下传播到目标元素:
document.getElementById("outer").addEventListener("click", function() {
console.log("外层 div 被点击(捕获)");
}, true);
document.getElementById("inner").addEventListener("click", function() {
console.log("内层 div 被点击(捕获)");
}, true);
document.getElementById("btn").addEventListener("click", function() {
console.log("按钮被点击(捕获)");
}, true);
点击按钮时,输出顺序:
外层 div 被点击(捕获)
内层 div 被点击(捕获)
按钮被点击(捕获)
阻止事件冒泡
使用 stopPropagation() 阻止事件继续传播:
document.getElementById("btn").addEventListener("click", function(event) {
console.log("按钮被点击");
event.stopPropagation();
});
document.getElementById("inner").addEventListener("click", function() {
console.log("这不会执行");
});
stopImmediatePropagation
stopImmediatePropagation() 不仅阻止事件传播,还会阻止当前元素上其他监听器执行:
button.addEventListener("click", function(event) {
console.log("第一个处理程序");
event.stopImmediatePropagation();
});
button.addEventListener("click", function() {
console.log("这不会执行");
});
阻止默认行为
某些元素有默认行为,如链接跳转、表单提交。使用 preventDefault() 可以阻止这些默认行为。
阻止链接跳转
const link = document.querySelector("a");
link.addEventListener("click", function(event) {
event.preventDefault();
console.log("链接不会跳转");
});
阻止表单提交
const form = document.querySelector("form");
form.addEventListener("submit", function(event) {
event.preventDefault();
const formData = new FormData(form);
console.log("表单数据:", Object.fromEntries(formData));
});
阻止右键菜单
document.addEventListener("contextmenu", function(event) {
event.preventDefault();
console.log("右键菜单被阻止");
});
检查是否可取消
document.addEventListener("click", function(event) {
if (event.cancelable) {
event.preventDefault();
}
});
常见事件类型
鼠标事件
const box = document.querySelector(".box");
box.addEventListener("click", function(event) {
console.log("单击");
});
box.addEventListener("dblclick", function(event) {
console.log("双击");
});
box.addEventListener("mouseenter", function(event) {
console.log("鼠标进入(不冒泡)");
});
box.addEventListener("mouseleave", function(event) {
console.log("鼠标离开(不冒泡)");
});
box.addEventListener("mouseover", function(event) {
console.log("鼠标悬停(冒泡)");
});
box.addEventListener("mouseout", function(event) {
console.log("鼠标移出(冒泡)");
});
box.addEventListener("mousemove", function(event) {
console.log("鼠标移动", event.clientX, event.clientY);
});
box.addEventListener("mousedown", function(event) {
console.log("鼠标按下");
});
box.addEventListener("mouseup", function(event) {
console.log("鼠标释放");
});
mouseenter/mouseleave 与 mouseover/mouseout 的区别:
mouseenter/mouseleave:不会冒泡,只在元素边界触发mouseover/mouseout:会冒泡,子元素也会触发
键盘事件
const input = document.querySelector("input");
input.addEventListener("keydown", function(event) {
console.log("按键按下:", event.key);
});
input.addEventListener("keyup", function(event) {
console.log("按键释放:", event.key);
});
input.addEventListener("keypress", function(event) {
console.log("按键输入:", event.key);
});
事件触发顺序:keydown → keypress → keyup
注意:keypress 事件已被废弃,推荐使用 keydown 或 input 事件。
表单事件
const form = document.querySelector("form");
const input = document.querySelector("input");
form.addEventListener("submit", function(event) {
event.preventDefault();
console.log("表单提交");
});
input.addEventListener("input", function(event) {
console.log("输入值变化:", event.target.value);
});
input.addEventListener("change", function(event) {
console.log("值改变并失去焦点:", event.target.value);
});
input.addEventListener("focus", function(event) {
console.log("获得焦点");
});
input.addEventListener("blur", function(event) {
console.log("失去焦点");
});
input.addEventListener("select", function(event) {
console.log("文本被选中");
});
input 与 change 的区别:
input:每次输入都触发(实时)change:值改变且失去焦点后触发
窗口事件
window.addEventListener("load", function() {
console.log("页面完全加载(包括图片等资源)");
});
window.addEventListener("DOMContentLoaded", function() {
console.log("DOM 加载完成(不等待图片等资源)");
});
window.addEventListener("resize", function() {
console.log("窗口大小改变:", window.innerWidth, window.innerHeight);
});
window.addEventListener("scroll", function() {
console.log("页面滚动:", window.scrollY);
});
window.addEventListener("beforeunload", function(event) {
event.preventDefault();
event.returnValue = "确定要离开吗?";
});
load 与 DOMContentLoaded 的区别:
load:页面所有资源(图片、CSS、JS)都加载完成后触发DOMContentLoaded:DOM 解析完成后立即触发,不等待资源
事件委托
事件委托是利用事件冒泡机制,将事件处理程序绑定到父元素上,通过判断 event.target 来处理子元素的事件。
基本原理
<ul id="list">
<li>项目 1</li>
<li>项目 2</li>
<li>项目 3</li>
</ul>
<script>
const list = document.getElementById("list");
list.addEventListener("click", function(event) {
if (event.target.tagName === "LI") {
console.log("点击了:", event.target.textContent);
}
});
list.insertAdjacentHTML("beforeend", "<li>项目 4</li>");
</script>
事件委托的优势
- 减少事件绑定数量:只需要在父元素绑定一个事件处理程序
- 动态元素支持:新增的子元素自动拥有事件处理能力
- 内存效率高:减少内存占用
实际应用示例
<table id="userTable">
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>张三</td>
<td>25</td>
<td>
<button class="edit" data-id="1">编辑</button>
<button class="delete" data-id="1">删除</button>
</td>
</tr>
<tr>
<td>李四</td>
<td>30</td>
<td>
<button class="edit" data-id="2">编辑</button>
<button class="delete" data-id="2">删除</button>
</td>
</tr>
</tbody>
</table>
<script>
document.getElementById("userTable").addEventListener("click", function(event) {
const target = event.target;
if (target.classList.contains("edit")) {
const id = target.dataset.id;
console.log("编辑用户:", id);
}
if (target.classList.contains("delete")) {
const id = target.dataset.id;
if (confirm("确定删除吗?")) {
target.closest("tr").remove();
console.log("删除用户:", id);
}
}
});
</script>
使用 matches 方法
document.querySelector(".container").addEventListener("click", function(event) {
if (event.target.matches(".btn-primary")) {
console.log("主要按钮被点击");
}
if (event.target.matches(".btn-danger")) {
console.log("危险按钮被点击");
}
if (event.target.matches("a[href^='http']")) {
event.preventDefault();
console.log("外部链接被点击");
}
});
closest 方法
当目标元素嵌套较深时,使用 closest() 查找最近的匹配元素:
document.querySelector(".list").addEventListener("click", function(event) {
const item = event.target.closest(".list-item");
if (item) {
console.log("点击了列表项:", item.dataset.id);
}
});
自定义事件
JavaScript 允许创建和触发自定义事件。
创建自定义事件
const customEvent = new CustomEvent("myEvent", {
detail: {
message: "这是自定义数据",
time: new Date()
},
bubbles: true,
cancelable: true
});
const element = document.querySelector("#myElement");
element.addEventListener("myEvent", function(event) {
console.log("自定义事件触发了");
console.log("数据:", event.detail);
});
element.dispatchEvent(customEvent);
简单自定义事件
const event = new Event("build");
document.addEventListener("build", function(event) {
console.log("构建事件触发");
});
document.dispatchEvent(event);
实际应用示例
class Counter {
constructor(element) {
this.element = element;
this.count = 0;
this.render();
}
increment() {
this.count++;
this.render();
const event = new CustomEvent("countChange", {
detail: { count: this.count },
bubbles: true
});
this.element.dispatchEvent(event);
}
render() {
this.element.textContent = this.count;
}
}
const counterElement = document.querySelector("#counter");
const counter = new Counter(counterElement);
document.addEventListener("countChange", function(event) {
console.log("计数变化:", event.detail.count);
});
counter.increment();
性能优化
节流(Throttle)
节流限制事件在一定时间内只执行一次:
function throttle(func, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
func.apply(this, args);
lastTime = now;
}
};
}
window.addEventListener("scroll", throttle(function() {
console.log("滚动事件(节流)");
}, 200));
防抖(Debounce)
防抖在事件停止触发一段时间后才执行:
function debounce(func, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
const searchInput = document.querySelector("#search");
searchInput.addEventListener("input", debounce(function(event) {
console.log("搜索:", event.target.value);
}, 300));
passive 事件监听器
对于 scroll、touchstart 等事件,使用 passive: true 可以提高滚动性能:
window.addEventListener("scroll", function(event) {
console.log("滚动中...");
}, { passive: true });
小结
- 事件是用户或浏览器执行的动作,JavaScript 通过事件处理程序响应
- 事件流包括捕获阶段、目标阶段和冒泡阶段
addEventListener是推荐的事件绑定方式- 事件对象包含事件的详细信息
stopPropagation()阻止事件冒泡,preventDefault()阻止默认行为- 事件委托利用事件冒泡,将事件绑定到父元素
- 可以创建和触发自定义事件
- 使用节流和防抖优化高频事件
练习
- 实现一个可拖拽的元素
- 使用事件委托实现一个动态列表的增删改
- 实现一个键盘快捷键系统(如 Ctrl+S 保存)
- 使用防抖实现搜索输入框
- 创建一个自定义事件系统,实现组件间通信