JavaScript 事件

网页之所以"能交互",靠的就是事件机制——用户点击、输入、滚动、提交,浏览器都会产生"事件"。JS 通过监听这些事件,在事件发生时执行对应的逻辑。这一章是前端开发的核心。

1. 事件基础

事件三要素:

// 给元素添加事件监听
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

监听函数会收到一个事件对象(通常命名为 eevent),包含事件的所有信息:

// 事件对象 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();
  }
});

两个最常用的方法:

4. 事件冒泡与捕获

这是事件传播的核心机制。点击嵌套元素时,事件会按三个阶段传播:

  1. 捕获阶段(capture):从 window 往下到目标元素的祖先(window → document → html → body → ... → 父元素)。
  2. 目标阶段(target):到达真正被点击的元素。
  3. 冒泡阶段(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. 常见陷阱

小结

下一篇我们看 BOM(浏览器对象模型)——操作整个浏览器窗口、地址栏、历史记录、定时器。

← 上一篇 JavaScript DOM 操作

下一篇 JavaScript BOM

✈️💬