事件是某事发生的信号,所有的 DOM 节点都生成这样的信号(但事件不仅限于 DOM)

DOM Level 规范迭代

DOM Level 0

DOM 没有被 W3C 定为标准之前

DOM Level 1

DOM 成为 W3C 的标准后,称为 DOM Level 1

DOM Level 1由两个模块组成:

  • DOM 核心(DOM Core):规定的是如何映射基于 XML 的文档结构,以便简化对文档中任意部分的访问和操作
  • DOM HTML:在 DOM 核心的基础上加以扩展,添加了针对 HTML 的对象和方法

DOM Level 2

在 DOM Level 1 的基础上进行了扩展。为节点添加了更多方法和属性等

添加新的模块,包括:视图、事件、范围、遍历、样式等

DOM Level 3

进一步扩展了 DOM,增加了 XPath 模块、加载和保存(DOM Load and Save)模块等,开始支持 XML1.0 规范

事件监听

attribute、property

DOM Level 0 事件

处理程序可以设置在 HTML 中名为 on<event> 的 attribute 中:

<!-- 直接写 JS 逻辑 -->
<input value="Click me" onclick="alert('Click!')" type="button">
 
<!-- 指向一个 JS 函数 -->
<input value="Click me" onclick="handleClick()" type="button">
<script>
function handleClick() {
  alert('Click!')
}
</script>

如果一个处理程序是通过 HTML 特性(attribute)分配的,那么随后浏览器读取它,并从特性的内容创建一个新函数,并将这个函数写入 DOM 属性(property)

因此,这种方法实际上与前一种方法相同:

<input id="elem" type="button" value="Click me">
<script>
  elem.onclick = function() {
    alert('Thank you')
  }
</script>

要移除一个处理程序 —— 赋值 elem.onclick = null

问题

因为只有一个 onclick 属性,所以无法分配更多事件处理程序:

<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
  elem.onclick = function() { // 覆盖了现有的处理程序
    alert('After')
  }
</script>

处理程序只会在冒泡(+ 目标)阶段执行

注意

this

处理程序中的 this 指向对应的元素,就是处理程序所在的那个元素

函数是否需要加括号

attribute 需要加:

<input value="Click me" onclick="handleClick()" type="button">
<script>
function handleClick() {
  alert('Click!')
}
</script>

当浏览器读取 HTML 特性(attribute)时,浏览器将会使用特性中的内容创建一个处理程序:

button.onclick = function() {
  handleClick()
 
  // 不加括号的话就变成了
  handleClick
  // 函数不会调用
}

property 不加:

<input value="Click me" type="button">
<script>
function handleClick() {
  alert('Click!')
}
 
button.onclick = handleClick
 
// 加括号的话就变成了
button.onclick = handleClick()
// 实际上获得的是函数执行的结果,即 undefined(因为这个函数没有返回值),此代码不会工作
</script>

不要使用 setAttribute

这样的调用会失效:

// 点击 <body> 将产生 error,
// 因为特性总是字符串的,函数变成了一个字符串
document.body.setAttribute('onclick', function() { alert(1) })

大小写敏感

  • attribute 大小写不敏感:<div onclick="..."><div OnClick="..."> 都可以,推荐全小写
  • property 大小写敏感:是 elem.onclick,而不是 elem.ONCLICK,否则代码不会工作

addEventListener

DOM Level 3 事件

  • 添加处理程序:EventTarget.addEventListener(event: string, listener: function, options?)
    • EventTarget:事件目标,可能是:
      • element
      • document
      • window
      • 任何支持事件的对象,如 XMLHttpRequest
    • event:事件名,如 click,注意没有 on 且全小写
    • listener:事件处理程序,回调函数或是一个包含 handleEvent 方法的对象
    • options:可以是一个对象,也可以是 boolean
      • 对象
        • once: boolean:默认 false,如果为 true,会在被触发后自动删除监听器
        • capture: boolean
          • false:默认值,冒泡阶段回调
          • true:捕获阶段回调
        • passive: boolean:默认 false
          • 设置为 true 时,表示 listener 永远不会调用 preventDefault()
          • 如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告
          • passive 设为 true 可以启用性能优化,并可大幅改善应用性能,因 preventDefault 的调用导致浏览器必须等待监听函数执行完成,造成阻塞
          • 处理程序选项:passive
        • signalAbortSignal,该 AbortSignal 的 abort() 方法被调用时,监听器会被移除
      • boolean:同 capture
  • 移除处理程序:EventTarget.removeEventListener(event: string, listener: function, options?)
    • options:可以是一个对象,也可以是 boolean
      • 对象:{ capture: boolean }
      • boolean:同 capture
      • 捕获(capture: true)和冒泡(capture: false)的事件监听器互不相关,需要分别移除。移除捕获监听器不会影响非捕获版本的相同监听器,反之亦然。

addEventListener() 的工作原理是将函数或对象添加到调用它的 EventTarget 上的指定事件类型的事件侦听器列表中

  • 如果要绑定的函数或对象已经被添加到列表中,该函数或对象不会被再次添加
  • 相同的函数或对象:地址值相同
  • 同一元素同一阶段有多个事件处理程序,它们的运行顺序与绑定顺序相同

对于某些事件,只能通过 addEventListener 设置处理程序,如:

// 永远不会运行
document.onDOMContentLoaded = function() {
  alert("DOM built")
}
 
// 正确方式
document.addEventListener("DOMContentLoaded", function() {
  alert("DOM built")
});

优点

  • 允许为一个事件添加多个监听器
  • 更精细的手段来控制 listener 的触发阶段(捕获或冒泡)
  • 它对任何事件都有效,而不仅仅是 HTML 或 SVG 元素

事件对象

当事件发生时,会创建一个 event 对象,将详细信息放入其中(如:鼠标指针坐标、键盘按键信息),并将其作为参数传递给处理程序:

<input type="button" onclick="alert(event.type)" value="Event type">
 
<input type="button" onclick="foo(event)" value="Event type">
 
<script>
foo(e) {}
 
elem.onclick = function(e) {}
 
elem.addEventListener('click', function(e) {})
 
elem.addEventListener('click', { handleEvent(e) {} })
</script>

event 对象的一些属性:

  • type:事件类型,如 "click"
  • event.currentTarget:EventTarget(如:绑定处理器的元素),这与 this 相同
    • 当处理程序是一个箭头函数,或者它的 this 被修改,就可以从 event.currentTarget 获取
  • event.target:事件发生的目标(即:目标阶段)
    • event.currentTarget 是当前绑定处理器的目标;event.target 是事件发生的目标
  • event.eventPhase:当前阶段,事件传播流程
    • 1:capturing
    • 2:target
    • 3:bubbling
  • event.cancelable:当前事件是否支持取消,浏览器默认行为
  • event.defaultPrevented:当前事件是否被阻止浏览器默认行为

事件传播流程

DOM Level 3 事件

  1. 捕获阶段:事件发生时,从 window 开始,自顶向下地捕获事件发生的目标元素
  2. 目标阶段:事件到达目标元素
  3. 冒泡阶段:目标元素自底向上地冒泡事件
    • 通常会一直上升到 <html>,然后再到 document ,有些事件甚至会到达 window
    • 几乎所有事件都会冒泡,除了 focusblurscrollwheelmouseentermouseleave 等事件不会冒泡

停止传播

  • event.stopPropagation():阻止继续捕获或冒泡
  • event.stopImmediatePropagation()
    • 阻止继续捕获或冒泡
    • 阻止监听同一事件的其他事件监听器被调用

尽量不要使用停止传播

有时停止传播会产生隐藏的陷阱,产生“死区”

通常,没有真正的必要去阻止冒泡。一项看似需要阻止冒泡的任务,可以通过其他方法解决:

  • 使用 event.defaultPrevented 来代替,来通知其他事件处理程序,该事件已经被处理
  • 将数据写入一个处理程序中的 event 对象,并在另一个处理程序中读取该数据,这样就可以向父处理程序传递有关下层处理程序的信息
  • 使用自定义事件

事件委托

如果有许多以类似方式处理的元素,就不必为每个元素分配一个处理程序,而是将单个处理程序放在它们的共同祖先上

在处理程序中,通过 event.target 以查看事件实际发生的位置并进行处理

好处:

  • 简化初始化并优化性能(节省内存):无需添加许多处理程序
  • HTML 结构灵活,可以随时添加/移除子元素而无需考虑处理程序(即使通过 JS 动态加入的子元素也能触发父级委托的处理程序)

局限性:

  • 事件必须冒泡(低级别的处理程序不应该使用 event.stopPropagation()),而有些事件不会冒泡
  • 可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应(可以忽略不计)

浏览器默认行为

许多事件会自动触发浏览器执行某些行为。例如:

  • 点击一个链接 —— 触发导航(navigation)到该 URL
  • 点击表单的提交按钮 —— 触发提交到服务器的行为
  • 在文本上按下鼠标按钮并移动 —— 选中文本

阻止浏览器默认行为

使用 JS 处理一个事件,通常不希望发生相应的浏览器行为,而是想要实现其他行为进行替代。

有两种方式来告诉浏览器不希望它执行默认行为:

  • 主流的方式是使用 event 对象的 event.preventDefault() 方法
  • 如果处理程序是使用 on<event>(而不是 addEventListener)分配的,那返回 false 也同样有效
<a href="https://www.baidu.com/" onclick="event.preventDefault()">此时点击不会跳转</a>
 
<a href="https://www.baidu.com/" onclick="return false">此时点击不会跳转</a>
 
<a href="https://www.baidu.com/" id="linkDemo">此时点击不会跳转</a>
<script>
linkDemo.onclick = function() { return false }
</script>

但是注意:

<a href="https://www.baidu.com/" onclick="demo()">没有阻止浏览器默认行为</a>
 
<script>
function demo() { return false }
</script>

这样并没有阻止浏览器默认行为,因为上面代码相当于:

function demo() { return false }
a.onclick = function() { demo() }

需要这样写:

<a href="https://www.baidu.com/" onclick="return demo()">此时点击不会跳转</a>
 
<script>
function demo() { return false }
</script>

后续事件

某些事件会相互转化。如果阻止了第一个事件,那就没有第二个事件了

如:阻止了鼠标按下(mousedown)的浏览器的默认事件,会导致 input 不能获取到焦点(focus

这是因为浏览器行为在 mousedown 上被取消。如果用另一种方式进行输入,则仍然可以进行聚焦。如使用 Tab 键从第一个输入切换到第二个输入,但鼠标点击则不行。

不可取消事件

对于不可取消的事件(如:通过 EventTarget.dispatchEvent() 分派的且没有指定 cancelable: true 的事件),调用 preventDefault() 将没有任何效果

可以使用 event.cancelable 来检查该事件是否支持取消

处理程序选项:passive

addEventListener 的可选项 passive: true 向浏览器发出信号,表明处理程序将不会调用 preventDefault()

为什么需要这样做?

移动设备上会发生一些事件,例如 touchmove(当用户在屏幕上移动手指时),默认情况下会导致滚动,但是可以使用处理程序的 preventDefault() 来阻止滚动

因此,当浏览器检测到此类事件时,它必须首先处理所有处理程序,然后如果没有任何地方调用 preventDefault,则页面可以继续滚动。但这可能会导致 UI 中不必要的延迟和“抖动”。

passive: true 选项告诉浏览器,处理程序不会取消滚动。然后浏览器立即滚动页面以提供最大程度的流畅体验,并通过某种方式处理事件

对于某些浏览器(Firefox,Chrome),默认情况下,touchstart 和 touchmove 事件的 passive 为 true

题目

一个历史页面,上面有若干按钮的点击逻辑,每个按钮都有自己的 click 事件

新需求来了:给用户信息添加了一个属性 banned = true,此用户点击页面上的任何按钮或者元素,都不可响应原来的函数。而是直接 alert 提示:你被封禁了。