跳到主要内容

JavaScript 事件处理

事件是用户或浏览器执行的某种动作,如点击、提交表单、页面加载等。JavaScript 通过事件处理程序来响应这些动作,实现与用户的交互。

事件基础

什么是事件

事件是浏览器或用户执行的某种动作,JavaScript 可以"监听"这些事件并执行相应的代码。常见的事件包括:

  • 鼠标事件:click、dblclick、mouseenter、mouseleave、mousemove
  • 键盘事件:keydown、keyup、keypress
  • 表单事件:submit、change、input、focus、blur
  • 窗口事件:load、resize、scroll、unload

事件流

当事件发生时,事件会在 DOM 树中传播,这个过程称为事件流。事件流分为三个阶段:

  1. 捕获阶段:事件从 window 向下传播到目标元素
  2. 目标阶段:事件到达目标元素
  3. 冒泡阶段:事件从目标元素向上传播到 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 事件已被废弃,推荐使用 keydowninput 事件。

表单事件

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>

事件委托的优势

  1. 减少事件绑定数量:只需要在父元素绑定一个事件处理程序
  2. 动态元素支持:新增的子元素自动拥有事件处理能力
  3. 内存效率高:减少内存占用

实际应用示例

<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 事件监听器

对于 scrolltouchstart 等事件,使用 passive: true 可以提高滚动性能:

window.addEventListener("scroll", function(event) {
console.log("滚动中...");
}, { passive: true });

小结

  1. 事件是用户或浏览器执行的动作,JavaScript 通过事件处理程序响应
  2. 事件流包括捕获阶段、目标阶段和冒泡阶段
  3. addEventListener 是推荐的事件绑定方式
  4. 事件对象包含事件的详细信息
  5. stopPropagation() 阻止事件冒泡,preventDefault() 阻止默认行为
  6. 事件委托利用事件冒泡,将事件绑定到父元素
  7. 可以创建和触发自定义事件
  8. 使用节流和防抖优化高频事件

练习

  1. 实现一个可拖拽的元素
  2. 使用事件委托实现一个动态列表的增删改
  3. 实现一个键盘快捷键系统(如 Ctrl+S 保存)
  4. 使用防抖实现搜索输入框
  5. 创建一个自定义事件系统,实现组件间通信