Skip to content

常用事件总结(Day15 1/4) ​

本页对应 Day15_DOM,共 6 段视频,约 1 小时 27 分。

学习目标 ​

  1. 能够说明事件监听的三种方式及其解绑差异,正确使用 addEventListener/removeEventListener,并解释事件流、冒泡/捕获及回调函数中 this 的指向。
  2. 能够为元素绑定 click、dblclick、contextmenu 事件,并验证双击时 click 与 dblclick 的触发关系及右击时系统菜单的行为。
  3. 能够使用 mousedown、mouseup、mousemove 完成鼠标按键与移动监听,并通过事件对象 button 判断左中右键,使用 offset/client/page/screen 坐标获取鼠标位置并说明参照物差异。
  4. 能够独立实现元素拖拽案例:按下时记录偏移并绑定移动事件,移动时计算并设置元素位置,抬起时解除监听,并修复快速拖拽丢失问题。
  5. 能够说明将 mousemove 绑定到 document 的原因,并在拖拽案例中实现视口边界限制,保证元素不超出可视区域。
  6. 能够对比 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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

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,更简单。

常见错误 ​

  1. 同一元素同一事件绑定了多个回调,只有最后一个生效

    • 现象:多次写 box.onclick = fn1; box.onclick = fn2;,最终只执行 fn2。
    • 原因:把事件当成元素属性赋值,后一次会覆盖前一次。
    • 修法:需要多个回调时统一用 addEventListener 绑定。
  2. removeEventListener 解除监听无效

    • 现象:调用了移除方法,但事件仍然触发。
    • 原因:绑定和移除传入的不是同一个函数引用,或捕获阶段参数不一致。
    • 修法:把回调保存为具名函数/变量,绑定与移除使用同一引用,并保持第三个参数一致。
  3. 拖拽松手后元素仍然跟随鼠标

    • 现象:mouseup 后背景色恢复了,但元素继续跟着鼠标动。
    • 原因:mousemove 监听没有解除,之前添加的监听一直存在。
    • 修法:在 mouseup 中解除同一个元素上的 mousemove,且绑定对象与解除对象必须一致。
  4. 拖拽速度一快,元素就“丢”了

    • 现象:慢慢拖正常,快速移动时元素跟不上鼠标。
    • 原因:mousemove 绑在 box 上,鼠标快速移出元素后事件不再触发。
    • 修法:拖拽过程中的 mousemove 改绑到 document 或 window,mouseup 也在同一层级解除。
  5. 拖拽开始瞬间元素位置跳变

    • 现象:鼠标按在元素中间拖动,元素左上角突然跑到鼠标位置。
    • 原因:直接用 clientX/clientY 设置 left/top,没有减去鼠标按下时在元素内的偏移。
    • 修法:mousedown 时记录 offsetX/offsetY,移动时用 clientX - offsetX、clientY - offsetY 计算。
  6. 在嵌套结构中用 mouseover/mouseout 导致频繁触发

    • 现象:鼠标在父元素内部子元素之间移动,进入离开事件反复触发。
    • 原因:子元素上的事件会冒泡到父元素。
    • 修法:需要“真正进入/离开父元素”时优先用 mouseenter/mouseleave。
  7. 设置元素位置后没有效果或样式异常

    • 现象:box.style.left = event.clientX 后元素不动或表现异常。
    • 原因:CSS 位置属性需要单位,且元素需有合适定位方式。
    • 修法:赋值时拼接 'px',并确认元素使用 fixed/absolute 等定位。
  8. offsetX/offsetY 被误认为一定相对监听元素

    • 现象:在带子元素的容器上移动,得到的坐标相对子元素变化。
    • 原因:offsetX/offsetY 相对的是事件目标元素,目标元素可能是后代元素。
    • 修法:明确目标元素;若必须相对监听元素,用 getBoundingClientRect() 配合 clientX/clientY 计算。

练习 ​

  1. 拖拽边界限制:实现一个固定定位方块,只能在本视口内拖动;要求左、上不小于 0,右、下不超过视口宽高减元素宽高。
  2. 坐标面板:在方块上监听 mousemove,实时显示 offsetX/offsetY、clientX/clientY、pageX/pageY、screenX/screenY,并说明四组坐标参照物差异。
  3. 进入离开对比:写一个父容器内含多个子元素,分别用 mouseover/mouseout 与 mouseenter/mouseleave 记录触发次数,观察并解释差异。
  4. 一次性事件:给按钮绑定点击事件,要求第一次点击后自动解除监听;分别用 removeEventListener 和 { once: true } 实现一次。
  5. 按键区分:在元素上监听 mousedown,用事件对象区分左键、滚轮键、右键;同时监听 contextmenu 并阻止默认菜单。

原视频对照 ​

#视频时长
1回顾13:29
2鼠标事件-单击双击右击04:22
3鼠标事件-按键按下抬起和鼠标移动28:07
4元素拖拽案例0121:28
5元素拖拽案例0210:58
6鼠标事件-鼠标进入和离开元素08:41

小结 ​

  1. 事件绑定优先使用 addEventListener,便于同一事件挂多个回调,也便于后续精确解绑。
  2. 解绑必须满足“同元素、同事件、同函数引用、同阶段参数”,否则移除无效。
  3. 拖拽的核心组合是 mousedown + mousemove + mouseup,移动与抬起监听应放在 document/window 上更稳。
  4. 拖拽位置计算要使用“鼠标视口坐标 − 按下时元素内偏移”,设置 CSS 位置时必须补单位。
  5. 拖拽边界限制中,left/top 的最小值与最大值要分别判断,左右与上下也要分别处理。
  6. 进入/离开元素优先考虑 mouseenter/mouseleave,可避免子元素冒泡造成的频繁触发。
  7. 事件对象是获取按键、坐标等状态的主要入口;button 的 0/1/2 分别对应左/中/右。
  8. 事件回调中的 this 指向监听事件的元素,但箭头函数不按此规则绑定,需根据场景选择函数写法。