JavaScript 事件
网页之所以"能交互",靠的就是事件机制——用户点击、输入、滚动、提交,浏览器都会产生"事件"。JS 通过监听这些事件,在事件发生时执行对应的逻辑。这一章是前端开发的核心。
1. 事件基础
事件三要素:
- 事件类型:如
click、input、keydown(注意不带on)。 - 目标元素:谁触发的,如按钮、输入框。
- 监听函数:事件发生时调用的函数。
// 给元素添加事件监听
const btn = document.querySelector("#btn");
// 参数 1:事件类型(不带 on),参数 2:回调函数
btn.addEventListener("click", () => {
console.log("按钮被点击了");
});
// 回调函数会收到一个事件对象 e
btn.addEventListener("click", (e) => {
console.log(e.type); // "click"(事件类型)
console.log(e.target); // 触发事件的元素(可能是 btn 或它的子元素)
console.log(e.currentTarget); // 绑定监听的元素(就是 btn)
});
// 移除监听(必须传同一函数引用,匿名函数移除不了)
function onClick() { console.log("点击"); }
btn.addEventListener("click", onClick);
btn.removeEventListener("click", onClick);
// ⚠️ 老写法(不推荐):覆盖式赋值,只能绑一个
btn.onclick = () => console.log("老写法");现代写法一律用 addEventListener——它可以绑多个监听(老写法 onclick= 只能绑一个,会覆盖),而且能精确控制(冒泡/捕获阶段、是否一次性)。
2. 常见事件类型
// 常见事件类型
// 鼠标事件
element.addEventListener("click", fn); // 单击
element.addEventListener("dblclick", fn); // 双击
element.addEventListener("mouseenter", fn);// 鼠标进入
element.addEventListener("mouseleave", fn);// 鼠标离开
element.addEventListener("mousemove", fn); // 鼠标移动(频繁触发!)
// 键盘事件
document.addEventListener("keydown", (e) => {
console.log(e.key); // "a" / "Enter" / "ArrowUp"
console.log(e.code); // "KeyA" / "Enter"
});
// 表单事件
input.addEventListener("input", fn); // 输入内容变化(实时)
input.addEventListener("change", fn); // 失去焦点且内容变化
input.addEventListener("focus", fn); // 获得焦点
input.addEventListener("blur", fn); // 失去焦点
form.addEventListener("submit", fn); // 表单提交
// 窗口事件
window.addEventListener("scroll", fn); // 滚动
window.addEventListener("resize", fn); // 窗口尺寸变化
window.addEventListener("load", fn); // 页面加载完成input vs change:input 是每次按键都触发,change 是失去焦点且值变化才触发。实时搜索用 input,提交校验用 change。
3. 事件对象 e
监听函数会收到一个事件对象(通常命名为 e 或 event),包含事件的所有信息:
// 事件对象 e 的常用属性和方法
btn.addEventListener("click", (e) => {
// 阻止默认行为(如表单提交刷新页面、链接跳转)
e.preventDefault();
// 阻止事件冒泡
e.stopPropagation();
// 鼠标坐标
console.log(e.clientX, e.clientY); // 相对视口
console.log(e.pageX, e.pageY); // 相对页面(含滚动)
// 是否按了修饰键
console.log(e.ctrlKey, e.shiftKey, e.altKey, e.metaKey);
// 键盘事件的按键信息
// console.log(e.key, e.code, e.keyCode); // keyCode 已废弃
});
// 实战:阻止链接跳转
document.querySelector("a").addEventListener("click", (e) => {
e.preventDefault();
console.log("链接被点了,但不跳转");
});
// 实战:Enter 键提交
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
submit();
}
});两个最常用的方法:
e.preventDefault():阻止默认行为——如表单提交刷新页面、链接跳转、右键菜单。e.stopPropagation():阻止事件冒泡——不希望事件传递到父元素时用。
4. 事件冒泡与捕获
这是事件传播的核心机制。点击嵌套元素时,事件会按三个阶段传播:
- 捕获阶段(capture):从
window往下到目标元素的祖先(window → document → html → body → ... → 父元素)。 - 目标阶段(target):到达真正被点击的元素。
- 冒泡阶段(bubble):从目标元素的祖先往上传,直到
window。
<!-- 假设 DOM 结构 -->
<!-- <div id="outer"><div id="inner"><button id="btn">点我</button></div></div> -->
<script>
// 默认:事件冒泡(从内到外)
// 点击 btn 时,事件依次触发:btn → inner → outer → document → window
document.querySelector("#outer").addEventListener("click", () => {
console.log("outer");
});
document.querySelector("#inner").addEventListener("click", () => {
console.log("inner");
});
document.querySelector("#btn").addEventListener("click", () => {
console.log("btn");
});
// 点击按钮,输出:btn → inner → outer
// 第三个参数 true:改为"捕获阶段"监听(从外到内)
document.querySelector("#outer").addEventListener("click",
() => console.log("outer 捕获"), true);
// 捕获阶段:outer → inner → btn;冒泡阶段:btn → inner → outer
// 阻止冒泡
document.querySelector("#btn").addEventListener("click", (e) => {
e.stopPropagation(); // 后续的 outer、inner 不再触发
console.log("只有 btn 触发");
});
</script>默认监听在冒泡阶段触发,所以子元素的事件先执行,父元素后执行。如果想改成捕获阶段监听,把 addEventListener 的第三个参数设为 true。
实际开发中,90% 的需求用默认冒泡就够了,理解冒泡主要是为了事件委托。
5. 事件委托(超实用)
事件委托利用事件冒泡——把监听加在父元素上,通过 e.target 判断真正触发的子元素。这样一个监听就能处理任意多个子元素,而且动态新增的子元素自动支持:
<!-- 列表项的点击事件,用事件委托更高效 -->
<ul id="list">
<li data-id="1">第一项</li>
<li data-id="2">第二项</li>
<!-- 动态添加的项也要能点击 -->
</ul>
<script>
// ❌ 笨办法:给每个 li 都加监听
document.querySelectorAll("#list li").forEach(li => {
li.addEventListener("click", () => console.log(li.dataset.id));
});
// 缺点 1:li 多了内存占用大
// 缺点 2:动态添加的新 li 没有事件
// ✅ 事件委托:监听父元素,通过 e.target 判断
document.querySelector("#list").addEventListener("click", (e) => {
if (e.target.tagName === "LI") {
console.log("点击了:", e.target.dataset.id);
}
});
// 优点:1 个监听搞定;动态新增的 li 自动支持
</script>事件委托是前端面试必考、实战必用的模式。场景:
- 动态列表/表格(用户分页加载、上拉加载)。
- 大量相似元素(日历格子、棋盘格)。
- 需要"未来"添加的元素也响应事件。
6. 表单事件实战
// 表单事件实战
const form = document.querySelector("#loginForm");
form.addEventListener("submit", (e) => {
e.preventDefault(); // 必加!阻止页面刷新
const username = form.username.value;
const password = form.password.value;
if (!username || !password) {
showError("用户名密码不能为空");
return;
}
// 发送请求...
console.log("提交:", username);
});
// 实时校验输入
const emailInput = document.querySelector("#email");
emailInput.addEventListener("input", (e) => {
const value = e.target.value;
const valid = /\S+@\S+/.test(value);
e.target.classList.toggle("invalid", !valid);
});
// 全局键盘快捷键
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault(); // 阻止浏览器保存网页
saveDocument();
}
});处理表单的金科玉律:提交事件里必须 e.preventDefault(),否则浏览器默认行为会刷新页面,JS 验证全部失效。
7. 自定义事件
// 创建并触发自定义事件(组件通信常用)
const myEvent = new CustomEvent("userAdded", {
detail: { name: "小明", age: 20 }
});
document.addEventListener("userAdded", (e) => {
console.log("收到事件:", e.detail); // { name: "小明", age: 20 }
});
// 在某个时刻触发
document.dispatchEvent(myEvent);
// 实战:封装"对话框打开/关闭"事件
function openDialog() {
// ...显示对话框
document.dispatchEvent(new CustomEvent("dialog:open", { detail: { id: 1 } }));
}自定义事件在组件解耦时极其有用——组件 A 触发事件,组件 B 监听,互不依赖。现代框架有更完善的方案(React 的 props/state、Vue 的 emit),但原理一致。
8. 实战:图片轮播(简化版)
<div class="slider">
<button class="prev">上一张</button>
<img class="pic" src="1.jpg" />
<button class="next">下一张</button>
</div>
<script>
const img = document.querySelector(".pic");
const total = 5;
let current = 1;
function show(n) {
current = (n - 1 + total) % total + 1; // 循环:第 0 张回到最后一张
img.src = current + ".jpg";
}
// 事件委托:容器上监听
document.querySelector(".slider").addEventListener("click", (e) => {
if (e.target.classList.contains("prev")) show(current - 1);
if (e.target.classList.contains("next")) show(current + 1);
});
// 键盘左右键
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowLeft") show(current - 1);
if (e.key === "ArrowRight") show(current + 1);
});
</script>这个例子融合了事件类型、事件对象、事件委托、键盘事件——把这些知识点串起来用。
9. 常见陷阱
- 循环里加事件忘用 let:用
var会让所有回调共享同一个循环变量。 - 移除监听失败:匿名函数无法移除,必须用具名函数。
- 忘了 preventDefault:表单提交、a 标签点击,默认行为会破坏 SPA 体验。
- 滚动事件触发太频繁:用节流(throttle)或
requestAnimationFrame。
小结
- 用
addEventListener绑定监听,可绑多个,推荐用法。 - 事件对象
e有target(触发元素)、preventDefault、stopPropagation。 - 事件传播分捕获、目标、冒泡三阶段,默认在冒泡阶段触发。
- 事件委托:监听父元素,通过
e.target判断子元素,动态新增也支持。
下一篇我们看 BOM(浏览器对象模型)——操作整个浏览器窗口、地址栏、历史记录、定时器。
← 上一篇 JavaScript DOM 操作
下一篇 JavaScript BOM →