Skip to content

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

本页对应 Day15_DOM,共 6 段视频,约 59 分钟。

学习目标 ​

  • 能够改造无缝滚动案例,使用 mouseenter 与 mouseleave 配合定时器变量,实现鼠标悬停停止滚动、离开后继续滚动。
  • 能够为页面监听滚轮事件,区分并兼容 mousewheel 与 DOMMouseScroll,通过事件对象判断滚轮向上或向下滚动。
  • 能够为输入框或 document 监听 keydown、keyup 事件,并使用事件对象获取按键的 keyCode/which 或 key 信息。
  • 能够说明 keypress 与 keydown 在触发按键范围和能否区分字母大小写方面的区别,并通过示例验证。
  • 能够使用 keyup 事件实时获取输入框内容,并将内容同步显示到指定元素中。
  • 能够说明 load 与 DOMContentLoaded 的触发时机差异,并使用 addEventListener 监听文档加载事件。

知识点 ​

回顾主线:先把 DOM 与事件串起来 ​

本节是一次回顾,笔记先列出两条主线:HTML DOM 中的表单、表格;事件中的监听方式、解除监听、事件流和回调函数里的 this。为什么需要回顾?因为后面的无缝滚动、滚轮、键盘、文档加载事件,都会反复用到“先获取元素,再监听事件,再在回调里操作元素”这套流程。怎么想?把事件题拆成四问:监听谁、监听什么事件、回调里拿什么信息、什么时候解除或兼容。怎么做?笔记给出的监听方式有三种:

js
// ① HTML 标签属性
// <button onclick="fn()">按钮</button>

// ② 元素对象.on事件名
btn.onclick = function () {};

// ③ addEventListener
btn.addEventListener('click', function () {});

解除监听时,第一种和第二种用 元素对象.on事件名 = null;,第三种用 元素对象.removeEventListener()。事件流分为捕获阶段、目标阶段、冒泡阶段;事件回调函数中的 this 指向监听事件的元素。易错点是:第三种方式不能再用 onclick = null 的思路解除;this 要看监听者是谁,不要想当然。

无缝滚动:鼠标移入停止,移出继续 ​

为什么需要?无缝滚动一直在跑,用户鼠标放上去时往往想看清楚内容,所以要让定时器暂停,移出后继续。怎么想?鼠标进入元素时清除定时器,鼠标离开时重新开启定时器。怎么做?转写稿里先把定时器用变量存起来,再给 book 绑定 mouseenter 和 mouseleave。关键是把定时器回调函数独立成有名字的函数,避免重复写匿名函数:

js
var interId = setInterval(run, 30);

function run() {
  // 原来的滚动逻辑
}

book.onmouseenter = function () {
  clearInterval(interId);
};

book.onmouseleave = function () {
  interId = setInterval(run, 30);
};

易错点:离开时重新开启定时器,变量仍然是同一个 interId,不要再写 var,否则下次清除不到;回调函数传名字 run,不要写成 run(),否则会立即执行。

滚轮事件:兼容与方向判断 ​

为什么需要?滚轮能控制页面滚动,也能用来操控元素。怎么想?滚轮事件一般监听给 window。Chrome、Safari、Edge 用 mousewheel,火狐用 DOMMouseScroll,而且后者只能通过 addEventListener 监听。怎么做?mousewheel 用事件对象的 wheelDelta 判断方向:向下为负,向上为正;火狐的 DOMMouseScroll 用 detail,方向相反。转写稿最后把两个回调合并成一个:

js
function wheelFunc(event) {
  if (event.wheelDelta) {
    if (event.wheelDelta > 0) {
      console.log('向上滚动');
    } else {
      console.log('向下滚动');
    }
  } else if (event.detail) {
    if (event.detail < 0) {
      console.log('向上滚动');
    } else {
      console.log('向下滚动');
    }
  }
}

window.onmousewheel = wheelFunc;
window.addEventListener('DOMMouseScroll', wheelFunc);

易错点:wheelDelta 和 detail 的正负方向相反;带大小写的新事件名通常只能用 addEventListener。

键盘事件:keydown/keyup 与监听对象 ​

为什么需要?键盘事件用来获取用户按了哪个键、何时输入完成。怎么想?键盘事件不能随便监听给普通 div,因为普通元素没有焦点;表单控件获取焦点后可以监听,document 也可以监听。怎么做?

js
inputBooks.onkeydown = function (event) {
  console.log(event.keyCode, event.which, event.key);
};

document.onkeyup = function (event) {
  console.log('键盘按键抬起');
};

keyCode 和 which 得到按键对应的 ASCII 码数字,key 得到按键名字。易错点:按住不放时 keydown 会连续触发;在输入框上按键,事件还会冒泡到 document,所以两边都可能触发。

keypress 与 keydown 的区别 ​

为什么需要?两者都像“按下”,但用途不同。怎么想?keydown 关心“按了哪个键”,keypress 关心“输入了什么字符”。怎么做?keydown 对控制键也能触发,keypress 对 Ctrl、Shift、方向键等控制键不触发,只对可输入字符触发。keydown 的 keyCode 不区分大小写,按 A 得到大写 A 的码;keypress 能区分输入的大小写。易错点:不要拿 keypress 判断所有按键;要判断物理按键用 keydown,要判断输入字符可用 keypress。

keyup 实时获取输入框内容 ​

为什么需要?搜索框、银行卡号回显都要实时读取输入内容。怎么想?改输入框内容必须靠键盘,而 keyup 在按键抬起时触发,此时字符已经进入输入框,所以比 keydown 更合适。怎么做?

js
inputBooks.onkeyup = function () {
  resBox.innerHTML = inputBooks.value;
};

易错点:用 keydown 时内容还没完全输入,可能少一个字符;获取元素时别忘了 #,转写稿里就出现过 querySelector 漏写井号导致取不到元素的问题。

文档加载事件:load 与 DOMContentLoaded ​

为什么需要?脚本写在前面时,元素还没解析出来,直接获取会失败。怎么想?把操作放进加载完成后的回调里。load 等页面所有内容,包括外部文件;DOMContentLoaded 只等文档元素加载完毕。怎么做?

js
window.addEventListener('load', function () {
  console.log('load 完毕');
});

window.addEventListener('DOMContentLoaded', function () {
  console.log('DOMContentLoaded 完毕');
});

易错点:DOMContentLoaded 事件名有大小写,通常只能用 addEventListener;一般 DOM 操作推荐 DOMContentLoaded,不必等外部图片等资源。

无缝滚动:鼠标悬停暂停、离开继续 ​

为什么需要:无缝滚动一直在跑,用户想停下来看清内容时却没有办法;用鼠标进入/离开控制定时器,交互更自然。怎么想:滚动由 setInterval 驱动,进入时 clearInterval,离开时重新 setInterval;但回调重复会难维护,所以把滚动回调独立成 run。怎么做:

js
let timer = setInterval(run, 20);
function run() { /* 滚动逻辑 */ }
box.onmouseenter = function () { clearInterval(timer); };
box.onmouseleave = function () { timer = setInterval(run, 20); };

易错点:转写稿里特别强调,timer 要始终是同一个变量,不能在离开事件里重新 var,否则下次清除不到最新定时器;清除后的定时器已经废了,要继续只能再开一个。调试时先刷新看滚动,再鼠标放上去看是否停、移开是否继续。

滚轮事件:mousewheel 与 DOMMouseScroll ​

为什么需要:想知道滚轮向上还是向下,才能做滚动控制。怎么想:Chrome、Safari、Edge 用 mousewheel,Firefox 用 DOMMouseScroll;事件名含大小写,必须用 addEventListener 监听。怎么做:

js
function wheelFunc(e) {
  e = e || window.event;
  if (e.wheelDelta) {
    console.log(e.wheelDelta > 0 ? '向上' : '向下');
  } else if (e.detail) {
    console.log(e.detail < 0 ? '向上' : '向下');
  }
}
window.addEventListener('mousewheel', wheelFunc);
window.addEventListener('DOMMouseScroll', wheelFunc);

易错点:wheelDelta 向下为负、向上为正;Firefox 的 detail 相反,向下为正、向上为负。转写稿里调试时先只输出事件,发现能触发但感受不到滚动,才补多个 p 让页面出现滚动条;再输出 wheelDelta 观察正负。

键盘事件:keydown 与 keyup ​

为什么需要:键盘按下和抬起是输入交互的基础。怎么想:键盘事件不能随便绑给普通 div,它不会触发;表单控件要获得焦点,document 则打开页面后按键盘就能触发。怎么做:

js
document.addEventListener('keydown', function (e) {
  console.log('按下', e.keyCode, e.which, e.key);
});
document.addEventListener('keyup', function (e) {
  console.log('抬起', e.key);
});

易错点:按住不放时 keydown 会连续触发,keyup 在松手时触发;keyCode、which 得到数字编码,key 得到按键名字。转写稿中给 input 绑定后必须先聚焦,按键才触发;给 document 绑定时,输入框按键还会冒泡到文档。

keypress 与 keydown 的区别 ​

为什么需要:都叫“按下”,但用途不同。怎么想:keydown 管所有按键;keypress 只管能输入字符的按键,控制键如 Ctrl、Shift、方向键不触发。怎么做:用同一个页面分别监听并输出:

js
document.addEventListener('keydown', e => console.log('keydown', e.keyCode));
document.addEventListener('keypress', e => console.log('keypress', e.keyCode));

易错点:keydown 不区分字母大小写,按 A 键得到的是大写 A 的编码;keypress 能识别实际输入字符的大小写。转写稿调试时按 Ctrl、方向键,keypress 不触发;按字母、数字、空格、回车才触发。

keyup 实时获取输入框内容 ​

为什么需要:搜索提示、银行卡号回显都要求边输入边更新。怎么想:用 keyup 而不是 keydown,因为按下时字符还没进入输入框,取 value 会少一个;抬起时输入已完成。怎么做:

js
const inputBox = document.querySelector('#bankCard');
const resBox = document.querySelector('#res');
inputBox.addEventListener('keyup', function () {
  resBox.innerHTML = inputBox.value;
});

易错点:选择器 id 要加 #,转写稿调试时漏了 # 导致获取不到元素;删除内容也会触发 keyup,所以回显会同步变短。用 keydown 对比会发现回显总慢半拍。

文档事件:load 与 DOMContentLoaded ​

为什么需要:脚本写在前面时可能获取不到后面的元素,需要等文档加载。怎么想:load 等页面所有资源,包括外部图片;DOMContentLoaded 只等 DOM 元素解析完,不等外部资源。二者事件名含大小写,必须用 addEventListener。怎么做:

js
window.addEventListener('DOMContentLoaded', function () {
  console.log('DOMContentLoaded');
});
window.addEventListener('load', function () {
  console.log('load');
});

易错点:转写稿用无法访问的外部图片调试,DOMContentLoaded 先触发,load 要等图片请求超时后才触发;日常更推荐 DOMContentLoaded。beforeunload、unload 现代浏览器会阻止,不展开演示。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:无缝滚动——鼠标悬停停止、离开继续 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>无缝滚动-鼠标悬停停止</title>
<style>
  #book {
    width: 400px;
    height: 40px;
    overflow: hidden;
    border: 1px solid #999;
    margin: 100px auto;
  }
  #book ul {
    list-style: none;
    margin: 0;
    padding: 0;
    width: 800px; /* 两份内容拼起来,保证滚动时不断档 */
  }
  #book li {
    float: left;
    width: 100px;
    height: 40px;
    line-height: 40px;
    text-align: center;
  }
</style>
</head>
<body>
<div id="book">
  <ul>
    <li>1</li><li>2</li><li>3</li><li>4</li>
    <li>1</li><li>2</li><li>3</li><li>4</li>
  </ul>
</div>
<script>
  var book = document.getElementById('book');
  var ul = book.getElementsByTagName('ul')[0];

  // 全局变量:自始至终只用这一个,用来保存定时器 id
  var intervalId = null;

  // 把定时器的回调函数独立出来,起个名字,方便复用
  function run() {
    var left = parseInt(ul.style.marginLeft || 0);
    left -= 2;                       // 每次向左滚 2px
    if (left <= -400) {              // 滚过第一组内容(4 * 100)
      left = 0;                      // 瞬间复位,形成"无缝"效果
    }
    ul.style.marginLeft = left + 'px';
  }

  // 开启定时器,把 id 存进变量
  intervalId = setInterval(run, 20);

  // 鼠标进入元素:停止定时器
  book.onmouseenter = function () {
    clearInterval(intervalId);
  };

  // 鼠标离开元素:重新开启一个定时器,仍然赋值给同一个变量
  book.onmouseleave = function () {
    intervalId = setInterval(run, 20);
  };
</script>
</body>
</html>

要点解释

  • intervalId 必须是全局变量(var 写在函数外)。如果在回调里再 var intervalId,那 clearInterval 清掉的就永远是 undefined,定时器根本停不下来。
  • clearInterval 之后原来的定时器就"废了",想继续只能重新 setInterval 开一个新的,所以 mouseleave 里必须再写一次开启代码。
  • 所以先把回调函数提成具名的 run,再传 setInterval(run, 20)——不能写 run(),加括号就变成立即调用、把返回值当回调了。
  • ul.style.marginLeft 取出来是带 px 的字符串,必须 parseInt 转数字再运算;第一次取值是 '',所以用 || 0 兜底。
  • 鼠标进出用 mouseenter / mouseleave,不要用 mouseover / mouseout(后者在子元素之间移动时也会触发,会误停误开)。

示例二:滚轮事件(兼容 Chrome/Edge 与 Firefox) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>滚轮事件</title>
<style>
  p {
    width: 400px;
    padding: 20px;
    background: #ccc;
  }
</style>
</head>
<body>
<p>段落1</p><p>段落2</p><p>段落3</p><p>段落4</p>
<p>段落5</p><p>段落6</p><p>段落7</p><p>段落8</p>
<script>
  // 定义滚轮事件的回调函数,两种浏览器共用这一个
  function wheelFunc(event) {
    event = event || window.event;

    // 有 wheelDelta 属性 —— Chrome / Safari / Edge
    if (event.wheelDelta) {
      if (event.wheelDelta > 0) {
        console.log('滚轮向上滚动');
      } else {
        console.log('滚轮向下滚动');
      }
    }
    // 有 detail 属性 —— Firefox(正负号和 wheelDelta 相反!)
    else if (event.detail) {
      if (event.detail < 0) {
        console.log('滚轮向上滚动');
      } else {
        console.log('滚轮向下滚动');
      }
    }
  }

  // Chrome / Safari / Edge 支持的事件
  window.addEventListener('mousewheel', wheelFunc);
  // Firefox 支持的事件
  window.addEventListener('DOMMouseScroll', wheelFunc);
</script>
</body>
</html>

要点解释

  • 滚轮事件一般监听给 window,页面有没有滚动条都会触发。
  • 两个事件名:mousewheel(Chrome/Safari/Edge)和 DOMMouseScroll(Firefox),互相不干扰,可以同时监听,同一个回调函数里靠属性判断来源。
  • DOMMouseScroll、DOMContentLoaded 这类带大小写的事件名,只能通过 addEventListener 监听,onxxx 和标签属性写法都无效。
  • 方向判断的正负号完全相反:wheelDelta 正数向上、负数向下;detail 负数向上、正数向下。这是最容易写错的地方。
  • 判断时用 if (event.wheelDelta) ... else if (event.detail),而不是先判断浏览器,这样代码更通用。

示例三:keyup 实时获取输入框内容 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>keyup 实时获取输入框内容</title>
<style>
  input {
    width: 300px;
    padding: 10px;
    border: 1px solid #999;
    outline: none;
  }
  #res {
    font-size: 1.5em;   /* 回显的字要大一些 */
    padding: 10px 20px;
  }
</style>
</head>
<body>
<div>
  请输入银行卡号:<input type="text" id="bankCard">
</div>
<div id="res"></div>
<script>
  (function () {
    var inputBox = document.querySelector('#bankCard');
    var resBox = document.querySelector('#res');

    // 给输入框监听按键抬起事件
    inputBox.onkeyup = function () {
      // 抬起的那一刻内容已经写进输入框了,取到的才是完整的
      resBox.innerHTML = inputBox.value;
    };
  })();
</script>
</body>
</html>

要点解释

  • 用 keyup 而不是 keydown:keydown 触发时字符还没写入输入框,取到的是"上一个字",回显总会少一位;keyup 是按键抬起,内容已输入完毕,inputBox.value 才是完整值。
  • 删除内容也会触发 keyup,所以删一个字回显也会同步变化,这就是"实时"的来源。
  • querySelector('#bankCard') 的 # 不能漏,漏了会拿到 null,后面绑定事件直接报错。
  • 键盘事件能监听给表单控件(有焦点后才触发)或 document(页面打开后按键盘就触发);监听给普通 div 是永远不会触发的,因为它拿不到焦点。给 document 监听时,输入框里按键还会冒泡到 document,两处都会触发。

常见错误 ​

1. 定时器 ID 用 var 重新声明,或每次进入回调都新建局部变量

  • 现象:鼠标移入后滚动停不下来;或移出后越滚越快。
  • 原因:var 重复声明会让"同一个变量"的假设失效,局部变量则每次都是新的引用,clearInterval 清的不是正在跑的那个。
  • 修法:定时器 ID 只用一个外层/全局变量承载,clearInterval(timerId) 之后立刻 timerId = setInterval(...) 覆盖,全程只有一个变量。

2. 恢复滚动时直接 setInterval,没先清旧的

  • 现象:反复移入移出几次后速度成倍增加。
  • 原因:clearInterval 只清掉"当前 ID 指向的那一个",若开启前没清,旧定时器仍在跑。
  • 修法:把"清除 + 重开"封装成一个函数(start() / stop()),start() 内部第一行必须 stop()。

3. 把回调函数写成 setInterval(roll(), 20)

  • 现象:定时器完全不动,或只执行一次。
  • 原因:加了小括号是立即调用,把返回值(undefined)传给了 setInterval。
  • 修法:传函数引用 setInterval(roll, 20);提取具名函数同时解决了"移入移出两处重复写同一段回调"的问题。

4. 滚轮事件只写一套,或统一用 on... 绑定

  • 现象:Chrome 正常,Firefox 毫无反应;或反过来。
  • 原因:mousewheel/wheel 与 DOMMouseScroll 分属不同浏览器;后者事件名含大写,只能 addEventListener,且方向值用 detail、符号与 wheelDelta 相反。
  • 修法:两个事件名都 addEventListener,指向同一个回调;回调内先判断 'wheelDelta' in e(Chrome/Edge)否则走 'detail' in e(Firefox),并注意方向判断要反过来写。

5. 键盘事件绑在 div 等普通元素上

  • 现象:代码不报错,但怎么按都不触发。
  • 原因:键盘事件只对"能获得焦点"的元素有意义,div 拿不到焦点。
  • 修法:绑到表单控件(需先聚焦)或直接绑到 document;注意绑 document 时,在输入框里按键会因冒泡而"看起来触发两次",需分清是哪一个元素在监听。

6. keydown 里读输入框 value 总是慢一个字符

  • 现象:输入 "12",显示的是 "1"。
  • 原因:keydown 触发时字符尚未写入输入框。
  • 修法:改用 keyup(或用 input 事件),此时内容已就绪。

7. 用 load 包裹所有初始化逻辑

  • 现象:页面明明能看,脚本却迟迟不执行;某张外链图片挂掉后整页功能全废。
  • 原因:load 要等所有外部资源(图片、iframe、外链)结束,失败资源还要等超时。
  • 修法:只依赖 DOM 结构时用 DOMContentLoaded(同样只能 addEventListener);确需图片尺寸等资源信息时才用 load。

练习 ​

1.(定时器) 把无缝滚动升级为"悬停暂停、离开继续",要求:滚动回调提取为具名函数;停止与开启各自封装成 stop() / start();反复移入移出 10 次速度不变。写完后回答:如果把保存 ID 的变量改成函数内部的 var,会出现什么现象?

2.(滚轮) 写一个兼容 Chrome 与 Firefox 的滚轮回调:向上滚页面显示数字 +1,向下滚 -1。要求两个浏览器事件共用一个回调,且回调内部通过属性存在性判断浏览器。追问:为什么这里不能用 window.onmousewheel = fn 的方式做兼容?

3.(键盘) 做一个"银行卡号回显":输入框内容实时同步到下方大字区域。先用 keydown 实现一遍,再用 keyup 实现一遍,对比两者结果差异并解释原因。进阶:只允许输入数字,其他键按下时阻止默认行为。

4.(键盘作用域) 在 input 和 document 上同时监听 keydown,在输入框内按字母 A,观察输出条数。解释为什么是两条,并给出"只让 document 那一条生效"或"只让 input 那一条生效"的两种改法。

5.(文档事件) 页面中放一张指向不可达地址的图片,分别用 addEventListener 监听 load 与 DOMContentLoaded,记录两者触发顺序与时间差。说明:为什么实际开发中更推荐把"获取元素、绑定事件"的初始化代码放进后者?

原视频对照 ​

#视频时长
1无缝滚动-鼠标悬停滚动停止03:59
2鼠标事件-滚轮事件14:11
3键盘事件-按键按下和抬起15:25
4键盘事件-keypress和keydown区别06:54
5keyup实时获取输入框内容06:23
6文档事件12:54

小结 ​

  1. 一个定时器一个变量:clearInterval 清除的是"ID 当前指向的那个定时器",变量被重新声明或换新,就等于把旧定时器弄丢了,它会在后台永远跑下去。
  2. "暂停/继续"的正确姿势是"清除 + 重建",且重建前必先清除;把 start/stop 抽成函数,比在 mouseenter/mouseleave 里各写一遍回调可靠得多。
  3. 回调传引用,不传调用:fn 与 fn() 的区别,是所有异步 API(定时器、事件监听)的通用红线。
  4. 新式事件名(含大写字母)只能 addEventListener,on 系列和 HTML 属性写法对它无效——DOMMouseScroll、DOMContentLoaded 都属此类。
  5. 跨浏览器兼容的常见套路:多个事件名挂同一个回调,在回调内做能力检测(属性是否存在),而不是在外部写两套逻辑;同时留意同义属性可能符号相反。
  6. 键盘事件的有效范围由"能否获得焦点"决定:表单控件需先聚焦,document/window 天然覆盖全页;给 document 绑定时要意识到输入框内的按键会冒泡上来。
  7. keydown 看"按了哪个键"(物理键、不区分大小写),keyup 看"输入了什么"(内容已落库);凡是要读 value 的场景,优先 keyup。
  8. DOM 就绪用 DOMContentLoaded,资源就绪才用 load;前者更快、更抗外部资源故障,是现代写法的默认选择。