javascript中Document对象的常用事件


DOM 事件很多,但日常开发翻来覆去用的就那几类。这篇按鼠标、拖拽、键盘、表单、焦点、文档/窗口六大类整理,先把事件绑定和事件流这两个基础概念讲清,再逐类过一遍,标注哪些已经废弃、哪些有坑。

一、先说清楚:on 前缀是怎么回事

很多老教程(包括本文的早期版本)把事件写成 onclickonmouseover,其实事件本名是 clickmouseoveron 前缀只是”事件处理属性”的命名方式。绑定事件有三种写法:

<!-- 1. HTML 属性(不推荐,逻辑和结构耦合) -->
<button onclick="handleClick()">点我</button>
// 2. DOM 属性(同一事件只能绑一个处理函数,后绑的覆盖先绑的)
btn.onclick = function () {
    console.log("clicked");
};

// 3. addEventListener(推荐:可绑多个、可控制捕获/冒泡、可移除)
btn.addEventListener("click", handleClick);
btn.removeEventListener("click", handleClick);

结论:日常一律用 addEventListener,事件名不带 on。

二、事件流:捕获 → 目标 → 冒泡

一次点击并不是”直接命中”目标元素,而是完整走三个阶段:

window → document → html → body → div(捕获阶段,从外向内)
                              ↓
                          目标元素(目标阶段)
                              ↓
div → body → html → document → window(冒泡阶段,从内向外)

addEventListener 第三个参数决定在哪个阶段触发:

el.addEventListener("click", fn); // 默认 false,冒泡阶段触发
el.addEventListener("click", fn, true); // 捕获阶段触发

两个常用控制方法:

  • event.stopPropagation():阻止事件继续传播(不再往上冒)。
  • event.preventDefault():阻止默认行为(如 a 标签跳转、表单提交),不影响传播。

理解了冒泡,才能理解后面 mouseovermouseenter 的区别,也是事件委托的原理基础。

三、鼠标事件

事件名称 描述
click 当用户在元素上按下并释放鼠标按钮时触发
dblclick 当用户在元素上快速连续点击两次鼠标按钮时触发
mousedown 当用户在元素上按下鼠标按钮(还未释放)时触发
mouseup 当用户在元素上释放鼠标按钮时触发
mousemove 当鼠标指针在元素上移动时触发
mouseover 当鼠标指针移动到元素上方或其子元素上方时触发
mouseout 当鼠标指针移出元素或其子元素时触发
mouseenter 当鼠标指针移动到元素内部时触发(不会冒泡)
mouseleave 当鼠标指针移出元素时触发(不会冒泡)
contextmenu 当用户右键点击元素时触发(在显示上下文菜单前)
wheel 当用户滚动鼠标滚轮时触发

几个要点:

1. mouseover/mouseout 与 mouseenter/mouseleave 的区别(高频面试题):

  • mouseovermouseout 会冒泡:鼠标在父元素内部、进入或离开子元素时也会触发,做悬停效果时容易疯狂重复触发。
  • mouseentermouseleave 不冒泡:只在鼠标穿过被绑定元素本身的边界时触发一次,子元素之间移动不触发。
// 父元素里有多个子元素时,体验一下差别
parent.addEventListener("mouseover", () => console.log("over")); // 划过每个子元素都打印
parent.addEventListener("mouseenter", () => console.log("enter")); // 只在进入 parent 时打印一次

悬停显示/隐藏这类需求,优先用 mouseenter/mouseleave

2. 触发顺序:一次完整单击依次触发 mousedown → mouseup → click;双击则是两轮 mousedown/mouseup/click 后再触发 dblclick

3. 自定义右键菜单:先屏蔽默认菜单再显示自己的。

el.addEventListener("contextmenu", e => {
    e.preventDefault(); // 屏蔽浏览器默认右键菜单
    showMyMenu(e.clientX, e.clientY);
});

4. 事件对象常用坐标clientX/clientY(相对视口)、pageX/pageY(相对文档,含滚动)、offsetX/offsetY(相对目标元素)。

四、拖拽事件

事件名称 描述 触发时机
dragstart 拖拽开始 用户开始拖动元素时
dragenter 进入目标元素 被拖拽元素进入目标元素边界时
dragover 在目标元素上移动 被拖拽元素在目标元素上方移动
dragend 拖拽结束 拖拽操作结束时(释放鼠标或取消操作)
drop 元素放置 被拖拽元素在目标元素上释放

此外还有 drag(拖动过程中在被拖元素上持续触发)和 dragleave(离开目标元素)。

两个必知的坑:

1. 想让元素可拖,先加 draggable="true"(img 和 a 标签默认可拖)。

2. drop 不触发?因为没阻止 dragover 的默认行为。 浏览器默认不允许放置,必须在 dragover(保险起见 dragenter 也加上)里 preventDefault()

// 被拖元素:用 dataTransfer 携带数据
source.addEventListener("dragstart", e => {
    e.dataTransfer.setData("text/plain", e.target.id);
});

// 放置目标:不 preventDefault,drop 永远不会触发
target.addEventListener("dragover", e => e.preventDefault());

target.addEventListener("drop", e => {
    e.preventDefault();
    const id = e.dataTransfer.getData("text/plain");
    e.target.appendChild(document.getElementById(id));
});

dataTransfer 是拖拽事件专属的数据中转站:setData/getData 传数据,e.dataTransfer.files 拿到拖入的文件(文件上传拖拽区就是这么做的)。

五、键盘事件

事件名称 描述
keydown 按下按键时触发(含 Alt、Shift 等),按住不放会连续触发
keyup 释放按键时触发一次

keypress 已被标准废弃,不要再用;判断按键也不要用 keyCode(同样废弃),统一用 event.key

input.addEventListener("keydown", e => {
    if (e.key === "Enter") submit();
    if (e.key === "Escape") close();
    if (e.ctrlKey && e.key === "s") {
        // 组合键:ctrlKey/shiftKey/altKey/metaKey
        e.preventDefault(); // 拦截浏览器默认的"保存网页"
        save();
    }
});

六、表单事件

事件名称 描述
input 输入内容每变化一次就触发(打一个字一次)
change 值改变且失去焦点后触发;select/checkbox/radio 选择即触发
submit 表单提交时在 <form> 上触发
reset 点击重置按钮时在 <form> 上触发
select 用户在输入框中选中文本时触发

input 和 change 的区别是这组的核心:实时搜索、字数统计用 input;只关心最终值(如校验)用 change

// 拦截表单默认提交刷新页面,改走 ajax
form.addEventListener("submit", e => {
    e.preventDefault();
    ajaxSubmit(new FormData(form));
});

七、焦点事件

事件名称 描述 是否冒泡
focus 元素获得焦点时触发
blur 元素失去焦点时触发
focusin 同 focus,但会冒泡
focusout 同 blur,但会冒泡

focus/blur 不冒泡,没法在父元素上做事件委托;需要委托时(比如一个表单里几十个输入框统一处理)用 focusin/focusout

form.addEventListener("focusin", e => e.target.classList.add("active"));
form.addEventListener("focusout", e => e.target.classList.remove("active"));

八、文档与窗口事件

事件名称 描述
DOMContentLoaded HTML 解析完、DOM 树构建完成即触发(不等图片、样式)
load 页面所有资源(图片、样式、iframe)加载完成后在 window 上触发;也用于 img 等单个资源加载完成
scroll 元素或窗口滚动时触发(注意加节流)
resize 窗口大小改变时触发(注意加防抖)
error 资源加载失败或脚本运行出错时触发
abort 资源加载被中断时触发
visibilitychange 页面可见性变化(切 tab、最小化)时在 document 上触发

DOMContentLoaded vs load:想尽早操作 DOM 用前者,需要拿图片尺寸等资源信息才用后者。老代码里的 window.onload 大多其实只需要 DOMContentLoaded

unload 已废弃(移动端和 bfcache 场景根本不触发),页面离开前的逻辑(如埋点上报)用 visibilitychangepagehide 代替。

document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "hidden") {
        navigator.sendBeacon("/log", data); // 页面隐藏时上报,比 unload 可靠
    }
});

scrollresize 触发频率极高,处理函数里有计算或 DOM 操作的话,记得配合防抖节流

九、最后:数量多了怎么办——事件委托

列表有 1000 个 li,不需要绑 1000 个监听器。利用冒泡,把事件绑在父元素上,通过 e.target 判断真正点的是谁:

ul.addEventListener("click", e => {
    const li = e.target.closest("li");
    if (li) console.log("点击了", li.dataset.id);
});

省内存、动态新增的子元素自动生效。展开原理见我另一篇:JS事件委托原理


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
  目录