事件是某事发生的信号,所有的 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
- 添加处理程序:
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
- 设置为
signal
:AbortSignal
,该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
:capturing2
:target3
:bubbling
event.cancelable
:当前事件是否支持取消,浏览器默认行为event.defaultPrevented
:当前事件是否被阻止浏览器默认行为
事件传播流程
- 捕获阶段:事件发生时,从
window
开始,自顶向下地捕获事件发生的目标元素 - 目标阶段:事件到达目标元素
- 冒泡阶段:目标元素自底向上地冒泡事件
- 通常会一直上升到
<html>
,然后再到document
,有些事件甚至会到达window
- 几乎所有事件都会冒泡,除了
focus
、blur
、scroll
、wheel
、mouseenter
、mouseleave
等事件不会冒泡
- 通常会一直上升到
停止传播
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
提示:你被封禁了。
答案
- 方式一:给整个浏览器窗口添加一个透明遮罩,给遮罩添加点击处理逻辑
- 方式二:给
window
添加捕获阶段的点击处理逻辑,阻止事件传播window.addEventListener('click', event => { if (user.banned) { alert('你被封禁了') event.stopProgagtion() } }, true)