事件处理

事件处理定义方式

addEventListener

简介那一章我们就学过,使用 DOM 对象的 addEventListener 方法 ,可以告知浏览器,当某个事件发生时,执行哪个函数进行处理

比如

// 鼠标点击事件
element.addEventListener("click", handleFunc )

// 键盘事件
element.addEventListener("keydown", handleFunc )

前面的教程给了一个 处理 鼠标点击 事件 click 的例子,

我们还可以改为 处理 键盘按键按下 事件 keydown

文本输入框中,键盘按键 Ctrl + 回车 ,就会执行script里面的函数,把薪资在 2万 以上、以下的人员名单分别打印出来

如下


<p>请输入员工薪资记录</p>
  
<textarea id="salary" rows="10" cols="50">     
薛蟠     45600 25
薛宝钗   25776 23
</textarea>
  
<div>
  <br><br>
  <span id="result" style='color:blue'>
  分类结果</span> <br><br>
  <pre>
  </pre>
<div>

  
<button onclick='location.reload()'>重置</button>

<script>    
function salaryStats(event){
  // 如果ctrl键按下,并且按下了回车键
  if (event.ctrlKey && event.key==='Enter') {    
    document.querySelector('pre').innerText  = `处理结果省略...`
  }
}

// 注册事件回调函数
document.querySelector('#salary').addEventListener("keydown", salaryStats );
</script>    

DOM对象事件属性

除了使用 addEventListener 指定 事件处理函数,

还可以使用 元素对应 DOM 对象的事件属性 指定 事件处理函数

事件属性名是 on开头,后面加事件名称

比如: onkeydownonclick 等等

// 键盘事件
element.onkeydown = handleFunc 

// 鼠标点击事件
element.onclick = handleFunc 

上例中,可以改为这样

// 注册事件回调函数,改为使用 onkeydown
document.querySelector('#salary').onkeydown = salaryStats

效果是一样的

而且,这种写法,更适合 属性后面直接跟匿名函数,像这样

document.querySelector('#salary').onkeydown = function (event){
  if (event.ctrlKey && event.key==='Enter') {    
      document.querySelector('pre').innerText  = `处理结果省略...`
  }
} 

或者箭头函数,像这样

document.querySelector('#salary').onkeydown = event =>{
  if (event.ctrlKey && event.key==='Enter') {    
      document.querySelector('pre').innerText  = `处理结果省略...`
  }
} 

html 内联定义 - 不推荐

还可以直接在 元素属性中设置事件处理 ,比如


<body>
    
  <p>请输入员工薪资记录</p>
  
  <textarea id="salary" rows="10" cols="50" 
    onkeydown="salaryStats(event)">    
薛蟠     45600 25
薛宝钗   25776 23
薛宝琴   14346 18
王熙凤   30460 25
王子腾   55660 45
  </textarea>
  
  <div>
    <br><br>
    <span id="result" style='color:blue'>
    分类结果</span> <br><br>
    <pre>
    </pre>
  <div>

  <script>    
  function salaryStats(event){
    if (event.ctrlKey && event.key==='Enter') {
      document.querySelector('pre').innerText  = `处理结果省略...`
    }
  }
  </script>
    
</body>

其中 textarea 里面的 onkeydown="salaryStats(event)" 就定义了当 keydown事件发生时,执行 salaryStats(event)


但是 很多人 不推荐这种写法,认为这样破坏了html界面和js代码的分离,不方便维护

事件针对的元素

大家应该可以理解,针对哪个元素dom对象调用 addEventListener 方法, 就是在这个元素的范围内注册事件处理函数。

非这个元素内发生的 注册事件 ,不会触发调用。

比如,上面的例子中,如果焦点在textarea输入框外 ,按 Ctrl + 回车 ,不会触发调用。


可以在整个DOM范围内注册事件监听, 对整个网页都是有效的,如下

document.addEventListener("keydown", salaryStats);

代码在html的位置

注意,上例中 <script> 元素包含的代码 是放在 body 的最后的,

如果放在head里面,像这样


<!DOCTYPE html>
<html>

<head>
  
  <script>    
  document.querySelector('#salary').onkeydown = event =>{
    if (event.ctrlKey && event.key==='Enter') {    
        document.querySelector('pre').innerText  = `处理结果省略...`
    }
  }   
  </script>
      
</head>

<body>    
  <span style='color:blue'>
  请输入员工薪资记录</span> <br><br>  
  <textarea id="salary" rows="20" cols="80">   
薛蟠     45600 25
薛宝钗   25776 23
  </textarea>
  
  <div>
    <br><br>
    <span id="result" style='color:blue'>
    分类结果</span> <br><br>
    <pre>
    </pre>
  <div>
  
</body>
</html>

运行就会报错

Cannot set properties of null (setting 'onkeydown')

因为 body中的script代码,执行的顺序 就是其 在html文档中的顺序。

在head中的js代码在网页内容(也就是body中的内容)渲染前,会被先执行。

这样 DOM 里面内容还没有创建, 还没有body节点,更加没有id为salary的节点对象。

document.querySelector('#salary') 值为 null

所以会报错


但是很多人喜欢把js内嵌的代码都集中放在head里面,

怎么办呢?

看下一节内容

页面加载后才执行

页面资源完成全部加载,包括页面HTML所有DOM对象产生,界面渲染完成,引用的外部js、css、图片加载完成 等等,会发出load事件

我们经常需要在页面资源完成全部内容的加载,立即执行一段代码

可以这样写

window.addEventListener('load', (event) => {
  // 执行代码
});

参数event 就是 load事件对象

如果不需要处理该对象,可以忽略,像这样

window.addEventListener('load', () => {
  // 执行代码
});

也可以使用 window对象的onload属性

window.onload = () => {
  // 执行代码
};

这样,我们就可以解决前面的问题了

<!DOCTYPE html>
<html>

<head>
  
  <script>  
  window.onload = () => {
    document.querySelector('#salary').onkeydown = event =>{
      if (event.ctrlKey && event.key==='Enter') {    
          document.querySelector('pre').innerText  = `处理结果省略...`
      }
    }   
  };    
  </script>
      
</head>

<body>
    
  <span style='color:blue'>
  请输入员工薪资记录</span> <br><br>
  
  <textarea id="salary" rows="20" cols="80">
薛蟠     4560 25
薛宝钗   35776 23
薛宝琴   14346 18
王熙凤   24460 25
王子腾   55660 45
  </textarea>
  
  <div>
    <br><br>
    <span id="result" style='color:blue'>
    分类结果</span> <br><br>
    <pre>
    </pre>
  <div>
  
</body>
</html>

这样,虽然键盘事件处理代码是放在head中,

但不是立即执行,而是等页面加载完成后再执行,

document.querySelector('#salary') 自然就可以找到该元素了。

事件对象和类型

我们前面代码

document.querySelector('#salary').onkeydown = event =>{
  // 处理代码
}   

这个里面的 event 参数对应的就是,事件发生时,浏览器传入回调我们的函数时,传入的 事件对象

不同的用户操作触发的事件对应的事件对象的类型不同

比如我们上面的是键盘事件,对应的就是 键盘事件(KeyboardEvent) 对象类型

如果是鼠标按钮点击操作,对应的就是 鼠标事件(MouseEvent) 对象类型


不同类型的对象,其属性、方法 不同

比如 上例中,我们是 键盘事件,传入的是键盘事件对象,它的有属性

  • ctrlKey

    如果事件发生时,ctrl键按下,值为true,否则为false

  • altKey

    如果事件发生时,alt键按下,值为true,否则为false

  • Shift

    如果事件发生时,shift键按下,值为true,否则为false

  • key

    返回 事件发生时,按下按键的字符串表示,比如

    Enter 对应回车键

    1、2、3、4 对应数字键 1、2、3、4

    a、b、c、d 对应字母键 a、b、c、d

    A、B、C、D 对应字母键 A、B、C、D

    等等

    大家可以通过 在代码中加上

    console.log(event.key)
    

    查看你的按键对应的到底是什么key属性的值

KeyboardEvent具体属性方法,可以参考MDN文档


事件对象类型 有很多,除了 键盘事件、鼠标按钮事件 外,还有 滚轮事件(WheelEvent)、拖拽事件(DragEvent)、游戏触控板事件(GamepadEvent) 等等。

还有的事件不是用户操作触发的,比如 页面加载完成事件、网址hash更改事件(HashChangeEvent)、websocket网络消息事件、 存储事件 等等


详细的事件分类,可以点击这里查看MDN文档

大家可以在需要使用 某种事件对象时,查阅该文档。

事件处理顺序

当我们操作界面元素触发事件时,比如点击下图中 深蓝色的 td,

image

td对象被点击事件,同时也是所有的上层元素被点击的事件。

这个道理就像: 一个南京人奥运夺冠事件,也就是一个江苏人奥运夺冠事件,也就是一个中国人奥运夺冠事件。


那么 如果我们代码 针对td和其上层元素都注册了点击处理函数,执行次序究竟是怎样的呢?


现代浏览器基本是这样做的:

点击下图中 深蓝色的 td,导致

  • 浏览器创建一个 click 事件对象

  • 这个事件对象会先从 浏览器DOM 顶层的 window 对象一直 传递下去,直到 触发事件的对象的父对象 ,这个过程称之为 capture Phase(捕获阶段)

    这个路径上,如果有任何DOM对象注册了点击处理事件,就会按照从上到下的先后顺序,依次被调用

  • 然后,这个事件对象 到达触发事件的td对象,这个过程称之为 target phase(目标阶段)

  • 然后,这个事件对象 再从触发事件的td对象,一直传递到顶层的window对象,这个过程称之为 bubbling Phase(冒泡阶段)

    注意, click 事件是会 冒泡传播 的, 但是也有些类型的事件(比如,blur、focus)是不会冒泡的,到target 位置就结束了。

    不bubbling的事件具体是哪些,可以点击参考这里


要声明注册的处理函数是在 捕获阶段 触发执行 ,应该这样

element.addEventListener("click", e => {这里是处理代码}, true)

第3个参数如果是boolean 并且设置为true ,就表示是 Capture Phase触发行。


要声明注册的处理函数是在 非捕获阶段(target phase 和 bubbling Phase)触发执行 ,应该没有第3个参数,或者第3个参数为false,如下

element.addEventListener("click", e => {这里是处理代码})

// 或者这样
element.addEventListener("click", e => {这里是处理代码}, false)



看下面的代码


<div id='outer' style='width:10rem;height:10rem;border:1px solid black'>  
  外层
  <div id='inner' style='width:6rem;height:6rem;border:1px solid black'>
  内层
  </div> 
</div> 
<br><br>

<script>    
  document.querySelector('#inner')
    .addEventListener("click", e => alert('处理 inner'))
  document.querySelector('#outer')
    .addEventListener("click", e => alert('处理 outer'))
  document.querySelector('body')
    .addEventListener("click", e => alert('处理 body'))  
</script>

点击内层元素,就会发现alert次序是

处理 inner
处理 outer
处理 body



如果改为


<div id='outer' style='width:10rem;height:10rem;border:1px solid black'>  
  外层
  <div id='inner' style='width:6rem;height:6rem;border:1px solid black'>
  内层
  </div> 
</div> 
<br><br>

<script>    
  document.querySelector('#inner')
    .addEventListener("click", e => alert('处理 inner'),true)
  document.querySelector('#outer')
    .addEventListener("click", e => alert('处理 outer'),true)
  document.querySelector('body')
    .addEventListener("click", e => alert('处理 body'),true)
</script>

点击内层元素,就会发现日志结果是

处理 body
处理 outer
处理 inner



如果改为


<div id='outer' style='width:10rem;height:10rem;border:1px solid black'>  
  外层
  <div id='inner' style='width:6rem;height:6rem;border:1px solid black'>
  内层
  </div> 
</div> 
<br><br>

<script>    
  document.querySelector('#inner')
    .addEventListener("click", e => alert('处理 inner'))
  document.querySelector('#outer')
    .addEventListener("click", e => alert('处理 outer'))
  document.querySelector('body')
    .addEventListener("click", e => alert('处理 body'),true)
</script>

点击内层元素,就会发现次序

处理 body
处理 inner
处理 outer

事件对象 target属性 / this

当我们实习事件处理函数的时候,传入的参数对象就是 触发的事件对象,这里我们用变量名 e 指代它

这个事件对象的属性中 有两个要注意的:

e.target 指代了 真正触发事件的那个DOM对象

e.currentTarget 指代了当前 正在处理事件的DOM对象, 也就是当前处理函数 注册对应那个对象


看一个例子


<body>
  <div id='outer' style='width:10rem;height:10rem;border:1px solid black'>  
    外层
    <div id='inner' style='width:6rem;height:6rem;border:1px solid black'>
    内层
    </div>  
  </div>

  <br>
  <button onclick='location.reload()'>重置</button>

  <script>  
  function changeColor(e) {
    e.target.style.backgroundColor  = 'green'
    e.currentTarget.style.backgroundColor  = 'gray'
  } 
  
  document.querySelector('#outer')
    .addEventListener("click", changeColor )    
  </script>

</body>


如果你点击内层框,会发现

内层框(真正触发事件的元素,也就是e.target )变为绿色 green

外层框(注册监听事件的元素,也就是e.currentTarget )变为灰色 gray


注意:注册监听事件的元素的内部元素触发的该事件,也会上升到


如果你点击外层框,

由于外层框 既是注册监听事件的元素,也是 真正触发事件的元素

所以两行代码都指代它,那么后面一行代码的效果会最终生效,就是外层框变成了灰色。

内层框没有被涉及到,颜色保持透明,显示的是外层框的颜色。


在处理函数中,也可以使用 this ,等价于 event.currentTarget

比如,上面的代码可以等价改为

function changeColor(e) {
  e.target.style.backgroundColor  = 'green'
  this.style.backgroundColor  = 'gray'
}