主题
常用事件总结(Day15 1/4)
本页对应 Day15_DOM,共 6 段视频,约 1 小时 27 分。
学习目标
- 能够说明事件监听的三种方式及其解绑差异,正确使用
addEventListener/removeEventListener,并解释事件流、冒泡/捕获及回调函数中this的指向。 - 能够为元素绑定
click、dblclick、contextmenu事件,并验证双击时click与dblclick的触发关系及右击时系统菜单的行为。 - 能够使用
mousedown、mouseup、mousemove完成鼠标按键与移动监听,并通过事件对象button判断左中右键,使用offset/client/page/screen坐标获取鼠标位置并说明参照物差异。 - 能够独立实现元素拖拽案例:按下时记录偏移并绑定移动事件,移动时计算并设置元素位置,抬起时解除监听,并修复快速拖拽丢失问题。
- 能够说明将
mousemove绑定到document的原因,并在拖拽案例中实现视口边界限制,保证元素不超出可视区域。 - 能够对比
mouseover/mouseout与mouseenter/mouseleave的触发差异,解释后代元素冒泡影响,并在进入/离开需求中正确选择事件。
知识点
HTML DOM:表单与表格的专用操作
为什么需要:通用 document.createElement、appendChild 能创建任何元素,但面对表单、表格这类结构固定的元素,写起来啰嗦。HTML DOM 给它们提供了专用属性和方法,目的是让常见操作更直接。怎么想:先判断元素类型,能用专用方法就别绕通用路径;但专用方法只对特定元素有效。怎么做:form 元素可用 submit() 提交、reset() 重置;表单控件可用 blur() 失焦、focus() 获焦;select 可用 add()、remove() 增删选项;表格可在表格里加行、在行里加单元格。创建元素时,只有 Option、Image 等少数可直接 new,其他元素仍要老老实实用 document.createElement。易错点:不要把专用方法当成所有元素通用;表格加行、加单元格有层级顺序;new Option、new Image 是特例,不能推广到任意标签。实际中特别强调:“只有它俩能直接 new,其他元素还是得用通用方式。”
事件监听三种方式与选择
为什么需要:同一个交互可能被多人绑定,若绑定方式选不好,后写的会覆盖先写的,协作时容易互相影响。怎么想:第一种把事件写进 HTML 属性,最简陋、耦合最高;第二种把事件当元素对象属性,写法简洁但同元素同事件只能保留一个回调;第三种 addEventListener 是标准方式,可给同一元素同一事件绑定多个回调。怎么做:①把事件作为 HTML 标签属性;②元素对象.on事件名 = 函数;③元素对象.addEventListener(事件名, 回调函数, false)。前两种方式下,重复绑定相同事件,后面的会覆盖前面的;第三种不会。易错点:事件名一般全小写,而属性名、方法名常是驼峰;addEventListener 第三个参数默认 false,表示冒泡阶段触发,传 true 则改捕获阶段;解绑时回调函数必须是同一个引用。现场用“再监听一遍就覆盖”解释了属性赋值的本质。
解除监听:按绑定方式解
为什么需要:一次性事件、满足条件后失效、拖拽松手后停止,都需要解除监听。怎么想:先看当初怎么绑定的,再选对应解绑方式,不能混用。怎么做:前两种方式用 元素对象.on事件名 = null; 覆盖掉原回调;第三种用 元素对象.removeEventListener(事件名, 回调函数);。代码可记成:
js
元素对象.on事件名 = null;
元素对象.removeEventListener(事件名, 回调函数);易错点:addEventListener 解绑时不能临时再写一个匿名函数,否则引用不同,解不掉;第一种 HTML 属性写法从感官上没写函数,但属性值里的代码相当于包在函数里,仍可用 null 覆盖;第三种解绑只影响对应回调,不影响其他监听。调试拖拽案例时,mouseup 里必须把 document.onmousemove = null,否则松手后元素还“赖”着鼠标走。
事件流:捕获、目标、冒泡
为什么需要:解释“为什么点子元素,父元素也触发”“谁先触发”这类现象。怎么想:事件从最上层 window、document 层层往下捕获,找到真正发生动作、不可再分的元素,就是目标元素;再从目标往上冒泡。目标元素不一定监听事件,它只是动作发生的位置。怎么做:默认回调在冒泡阶段执行;addEventListener 第三参数传 true 可改到捕获阶段。易错点:目标阶段可看作捕获结束、冒泡开始;不是目标元素必须绑定事件;系统可选布尔参数默认值常是 false,不传参得到 undefined,也相当于 false。用红、橙、黄、绿四层举例:只给橙和黄监听,点绿目标时,黄先于橙触发,因为冒泡从目标往上走。
回调函数中的 this 与循环变量陷阱
为什么需要:事件回调由浏览器事件模块调用,我们看不到调用过程,不能靠“谁调用”推 this;循环绑定多个元素时又容易串。怎么想:一句话,this 指向监听事件的那个元素;循环变量 i 是外层同一个变量,回调里用它取不到对应元素。怎么做:回调里直接用 this 取当前元素;不要用循环变量 i 去取对应元素。易错点:第一种 HTML 属性写法没显式写函数,但属性值代码相当于包在函数里,this 仍是该元素;addEventListener 的回调里 this 也指向监听元素;for 循环给多个元素绑定后,回调里的 i 往往已经变成最后的值,改用 this 就不会混。提醒:“甭管用哪种方式,this 就是监听事件的那个元素。”
回顾:事件监听、解除与事件流
为什么需要:常用事件不是孤立记事件名,先要理解监听机制,否则后面拖拽、进入离开都会写错。怎么想:监听有三种方式——HTML 属性、元素属性、addEventListener;前两种同一元素同一事件只能保留一个回调,后写覆盖前写,标准方式可绑多个。怎么做:解除时,前两种把事件属性赋 null;addEventListener 用 removeEventListener,且回调不能匿名。事件流按捕获→目标→冒泡理解,默认在冒泡阶段触发,addEventListener 第三参数为 true 可改捕获。回调里的 this 指向监听事件的元素;循环里不要用全局 i 取元素,直接用 this。易错点:removeEventListener 找不到匿名函数;把“目标元素”误认为一定监听了事件;忘记默认冒泡。
js
box.addEventListener('click', fn);
box.removeEventListener('click', fn); // fn 不能是匿名函数单击、双击与右击
为什么需要:鼠标最基础交互要先分清单击、双击、右键菜单。怎么想:单击是 click,双击是 dblclick,右击不叫 rightclick,而叫 contextmenu(上下文菜单)。怎么做:给同一个元素分别绑定即可。易错点:事件名一般全小写,dblclick 不要拼错;双击时会先触发两次 click,再触发一次 dblclick;右击时自定义回调执行,但系统右键菜单仍会弹出,转写稿中并未做阻止默认菜单。
js
box.onclick = function () { console.log('单击'); };
box.ondblclick = function () { console.log('双击'); };
box.oncontextmenu = function () { console.log('右击'); };按下、抬起与事件对象
为什么需要:拖拽、按住改样式都离不开“按下”和“抬起”。怎么想:mousedown 是鼠标按键按下,mouseup 是抬起,左中右都算,不区分左右;回调第一个形参是事件对象,浏览器调用回调时会传入。怎么做:用事件对象的 button 判断按键:0 左键、1 中键滚轮、2 右键。按下改红、抬起恢复。易错点:其他鼠标功能键通常捕获不到;事件对象形参名可随便取;mouseup 里也能取 button。
js
box.onmousedown = function (e) {
box.style.background = '#900';
console.log(e.button); // 0 左 1 中 2 右
};
box.onmouseup = function () {
box.style.background = ''; // 清空行内样式,恢复原样式
};鼠标移动与坐标
为什么需要:移动事件能实时拿到鼠标位置,是做拖拽、跟随效果的基础。怎么想:mousemove 只要鼠标在元素上移动就触发,按不按键都行,移动一个像素也会触发。坐标要分清参照物:offsetX/offsetY 是鼠标在目标元素上的位置;clientX/clientY 是视口位置;pageX/pageY 是页面位置;screenX/screenY 是屏幕位置。怎么做:按需求选一对。易错点:offset 的目标元素不一定是监听元素,可能是其后代;页面滚动后 clientY 与 pageY 会不同;屏幕坐标一般用得少。
js
box.onmousemove = function (e) {
console.log(e.offsetX, e.offsetY); // 目标元素上
console.log(e.clientX, e.clientY); // 视口上
};拖拽案例:按下后让元素跟鼠标走
为什么需要:把按下、移动、抬起串起来,才能做出“按住拖拽”。怎么想:元素用固定定位,参照视口,所以移动时取 clientX/clientY;按下瞬间要记录鼠标在元素内的 offsetX/offsetY,移动时用 clientX - offsetX 得到元素新 left。为了鼠标移动太快也不离开元素,mousemove 要监听在 document 上,松手时解除。怎么做:按下时挂 document.onmousemove,松手时设 document.onmousemove = null,并做边界限制。易错点:忘记加 px;只监听元素自身,鼠标一快就丢事件;松手不解除会“赖着走”;left 和 top 要分别判断,不能一个 else if 混掉两轴。
js
box.onmousedown = function (e) {
let eleLeft = e.offsetX, eleTop = e.offsetY;
document.onmousemove = function (e) {
let left = e.clientX - eleLeft;
let top = e.clientY - eleTop;
if (left < 0) left = 0;
if (top < 0) top = 0;
let maxLeft = document.documentElement.clientWidth - box.offsetWidth;
let maxTop = document.documentElement.clientHeight - box.offsetHeight;
if (left > maxLeft) left = maxLeft;
if (top > maxTop) top = maxTop;
box.style.left = left + 'px';
box.style.top = top + 'px';
};
};
document.onmouseup = function () {
document.onmousemove = null;
box.style.background = '';
};进入与离开:over/out 与 enter/leave
为什么需要:鼠标进入、离开元素很常用,但两套事件行为不同。怎么想:mouseover/mouseout 出现早,进入后代元素也会触发,因为会冒泡,所以在元素内部游走时会反复触发;mouseenter/mouseleave 后来增加,用来代替它们,后代元素不会冒泡触发,只有彻底进入、离开监听元素才触发。怎么做:一般优先用 mouseenter/mouseleave。易错点:mouseover/mouseout 在内部移动时频繁触发,不一定符合用户习惯;mouseenter/mouseleave 从事件流原理看“不冒泡”,但更符合交互体验。
课堂画面






代码示例
1. 鼠标单击、双击、右击,以及按键按下/抬起
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>鼠标事件</title>
<style>
#box {
width: 400px;
height: 250px;
padding: 20px;
background-color: #099;
}
#box p {
background-color: #ccc;
min-height: 50px;
}
</style>
</head>
<body>
<div id="box">
<p>1</p>
<p>2</p>
</div>
<script>
var box = document.querySelector('#box');
/* ---------- 单击 / 双击 / 右击 ---------- */
box.onclick = function () {
console.log('box 被单击了');
};
box.ondblclick = function () {
console.log('box 被双击了');
};
box.oncontextmenu = function () {
console.log('box 被右击了');
};
/* ---------- 按键按下 / 抬起 ---------- */
box.onmousedown = function (event) {
box.style.background = '#900';
console.log('鼠标按键按下,button = ' + event.button);
};
box.onmouseup = function (event) {
box.style.background = ''; // 设为空,覆盖行内样式,回退到 CSS 中的 #099
console.log('鼠标按键抬起,button = ' + event.button);
};
</script>
</body>
</html>关键行与易错点
- 事件名全部小写:
dblclick不要写成dbclick/ddlclick;右击叫contextmenu(上下文菜单事件),不叫rightclick。 - 双击时会触发 1 次
dblclick+ 2 次click,这是正常的。 mousedown/mouseup不分左右键,左键、滚轮、右键都能触发;oncontextmenu只触发一次,系统右键菜单也照常弹出,两者互不影响。- 回调函数里的
event是浏览器事件模块自动传入的实参,形参名随便起(叫e、ev都行),不用可以不写,但浏览器一定会传。 event.button:0左键,1滚轮,2右键。box.style.background = ''是清空行内样式,从而恢复 CSS 里的原色;写成别的值就覆盖不掉了。
2. 元素拖拽(限制在视口内)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>元素拖拽</title>
<style>
body { margin: 0; height: 1000px; } /* 撑出滚动条,验证拖拽范围 */
#box {
position: fixed; /* 参照视口,和 clientX/clientY 一致 */
left: 100px;
top: 60px;
width: 150px;
height: 150px;
background-color: #099;
cursor: pointer;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
(function () {
var box = document.querySelector('#box');
var eleLeft = 0; // 鼠标在元素上的 X
var eleTop = 0; // 鼠标在元素上的 Y
/* 1. 按下:记录相对位置 + 开始监听移动 */
box.onmousedown = function (event) {
box.style.background = '#900';
eleLeft = event.offsetX;
eleTop = event.offsetY;
/* 关键:监听 document 而不是 box */
document.onmousemove = function (event) {
// 鼠标在视口上的位置 - 鼠标在元素上的位置 = 元素该去的位置
var left = event.clientX - eleLeft;
var top = event.clientY - eleTop;
var maxLeft = document.documentElement.clientWidth - box.offsetWidth;
var maxTop = document.documentElement.clientHeight - box.offsetHeight;
if (left < 0) {
left = 0;
} else if (left > maxLeft) {
left = maxLeft;
}
if (top < 0) {
top = 0;
} else if (top > maxTop) {
top = maxTop;
}
box.style.left = left + 'px';
box.style.top = top + 'px';
};
};
/* 2. 抬起:恢复颜色 + 解除移动监听 */
box.onmouseup = function () {
box.style.background = '';
document.onmousemove = null;
};
})();
</script>
</body>
</html>关键行与易错点
- 元素位置只能通过 CSS 设置(
style.left/style.top),之前学的获取位置方法是只读的。position: fixed参照视口,所以配套用clientX/clientY。 mousemove必须写在mousedown里,否则不按鼠标元素也会跟着跑。- 监听对象是
document而不是box:鼠标拖太快会离开元素,box上的mousemove就不触发了;document上任何位置都能触发,拖再快也不丢。 offsetX/offsetY必须在按下的瞬间取一次并存到外层变量里,否则拖动过程中数值一直变,鼠标和元素会错位。- 内层回调函数可以直接用外层的
eleLeft/eleTop(作用域链),两个函数里的event形参互不冲突。 - 赋值时必须拼上
'px':box.style.left = 100无效,box.style.left = 100 + 'px'才生效。 mouseup里一定要document.onmousemove = null解绑,否则松手后元素还赖着鼠标走。- 边界判断:
left和top必须各判断各的,不能把 top 的判断接到 left 的else if后面,否则 left 一旦命中,top 就不再判断了。
3. mouseover/mouseout 与 mouseenter/mouseleave 的对比
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>鼠标进入和离开</title>
<style>
#box {
width: 400px;
height: 250px;
padding: 20px;
background-color: #099;
}
#box p {
background-color: #ccc;
min-height: 50px;
}
</style>
</head>
<body>
<div id="box">
<p>1</p>
<p>2</p>
</div>
<script>
var box = document.querySelector('#box');
/* ---------- 不推荐:会在后代元素之间游走时反复触发 ---------- */
// box.onmouseover = function () {
// console.log('mouseover 触发了');
// };
// box.onmouseout = function () {
// console.log('mouseout 触发了');
// };
/* ---------- 推荐:只在整个元素真正进出时触发 ---------- */
box.onmouseenter = function () {
console.log('mouseenter 触发了');
};
box.onmouseleave = function () {
console.log('mouseleave 触发了');
};
</script>
</body>
</html>关键行与易错点
mouseover/mouseout是早期事件;mouseenter/mouseleave是后加的,就是用来代替前两者的,有相关需求优先用 enter/leave。- 现象:鼠标在
#box内部的两个<p>之间移动,mouseover/mouseout会不停反复触发——因为后代元素上发生的动作会冒泡到#box。 mouseenter/mouseleave不会因后代元素冒泡而触发,只有彻底进入 / 彻底离开#box才触发一次,更符合用户习惯。- 注意
mouseenter别拼成mouseover、mouseleave别拼成mouseout,名字容易混;mouseup也别把u、s写反。 - 单纯"移入变色、移出复原"这类效果,优先用 CSS 的
:hover,更简单。
常见错误
同一元素同一事件绑定了多个回调,只有最后一个生效
- 现象:多次写
box.onclick = fn1; box.onclick = fn2;,最终只执行fn2。 - 原因:把事件当成元素属性赋值,后一次会覆盖前一次。
- 修法:需要多个回调时统一用
addEventListener绑定。
- 现象:多次写
removeEventListener解除监听无效- 现象:调用了移除方法,但事件仍然触发。
- 原因:绑定和移除传入的不是同一个函数引用,或捕获阶段参数不一致。
- 修法:把回调保存为具名函数/变量,绑定与移除使用同一引用,并保持第三个参数一致。
拖拽松手后元素仍然跟随鼠标
- 现象:
mouseup后背景色恢复了,但元素继续跟着鼠标动。 - 原因:
mousemove监听没有解除,之前添加的监听一直存在。 - 修法:在
mouseup中解除同一个元素上的mousemove,且绑定对象与解除对象必须一致。
- 现象:
拖拽速度一快,元素就“丢”了
- 现象:慢慢拖正常,快速移动时元素跟不上鼠标。
- 原因:
mousemove绑在box上,鼠标快速移出元素后事件不再触发。 - 修法:拖拽过程中的
mousemove改绑到document或window,mouseup也在同一层级解除。
拖拽开始瞬间元素位置跳变
- 现象:鼠标按在元素中间拖动,元素左上角突然跑到鼠标位置。
- 原因:直接用
clientX/clientY设置left/top,没有减去鼠标按下时在元素内的偏移。 - 修法:
mousedown时记录offsetX/offsetY,移动时用clientX - offsetX、clientY - offsetY计算。
在嵌套结构中用
mouseover/mouseout导致频繁触发- 现象:鼠标在父元素内部子元素之间移动,进入离开事件反复触发。
- 原因:子元素上的事件会冒泡到父元素。
- 修法:需要“真正进入/离开父元素”时优先用
mouseenter/mouseleave。
设置元素位置后没有效果或样式异常
- 现象:
box.style.left = event.clientX后元素不动或表现异常。 - 原因:CSS 位置属性需要单位,且元素需有合适定位方式。
- 修法:赋值时拼接
'px',并确认元素使用fixed/absolute等定位。
- 现象:
offsetX/offsetY被误认为一定相对监听元素- 现象:在带子元素的容器上移动,得到的坐标相对子元素变化。
- 原因:
offsetX/offsetY相对的是事件目标元素,目标元素可能是后代元素。 - 修法:明确目标元素;若必须相对监听元素,用
getBoundingClientRect()配合clientX/clientY计算。
练习
- 拖拽边界限制:实现一个固定定位方块,只能在本视口内拖动;要求左、上不小于
0,右、下不超过视口宽高减元素宽高。 - 坐标面板:在方块上监听
mousemove,实时显示offsetX/offsetY、clientX/clientY、pageX/pageY、screenX/screenY,并说明四组坐标参照物差异。 - 进入离开对比:写一个父容器内含多个子元素,分别用
mouseover/mouseout与mouseenter/mouseleave记录触发次数,观察并解释差异。 - 一次性事件:给按钮绑定点击事件,要求第一次点击后自动解除监听;分别用
removeEventListener和{ once: true }实现一次。 - 按键区分:在元素上监听
mousedown,用事件对象区分左键、滚轮键、右键;同时监听contextmenu并阻止默认菜单。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 回顾 | 13:29 |
| 2 | 鼠标事件-单击双击右击 | 04:22 |
| 3 | 鼠标事件-按键按下抬起和鼠标移动 | 28:07 |
| 4 | 元素拖拽案例01 | 21:28 |
| 5 | 元素拖拽案例02 | 10:58 |
| 6 | 鼠标事件-鼠标进入和离开元素 | 08:41 |
小结
- 事件绑定优先使用
addEventListener,便于同一事件挂多个回调,也便于后续精确解绑。 - 解绑必须满足“同元素、同事件、同函数引用、同阶段参数”,否则移除无效。
- 拖拽的核心组合是
mousedown + mousemove + mouseup,移动与抬起监听应放在document/window上更稳。 - 拖拽位置计算要使用“鼠标视口坐标 − 按下时元素内偏移”,设置 CSS 位置时必须补单位。
- 拖拽边界限制中,
left/top的最小值与最大值要分别判断,左右与上下也要分别处理。 - 进入/离开元素优先考虑
mouseenter/mouseleave,可避免子元素冒泡造成的频繁触发。 - 事件对象是获取按键、坐标等状态的主要入口;
button的0/1/2分别对应左/中/右。 - 事件回调中的
this指向监听事件的元素,但箭头函数不按此规则绑定,需根据场景选择函数写法。