浏览器事件简介
- 事件 是某事发生的信号。所有的 DOM 节点都生成这样的信号(但事件不仅限于 DOM)。
- 鼠标事件
click
—— 当鼠标点击一个元素时(触摸屏设备会在点击时生成)。contextmenu
—— 当鼠标右键点击一个元素时。mouseover
/mouseout
—— 当鼠标指针移入/离开一个元素时。mousedown
/mouseup
—— 当在元素上按下/释放鼠标按钮时。mousemove
—— 当鼠标移动时。
- 键盘事件
keydown
和keyup
—— 当按下和松开一个按键时。
- 表单(form)元素事件
submit
—— 当访问者提交了一个<form>
时。focus
—— 当访问者聚焦于一个元素时,例如聚焦于一个<input>
。
- Document 事件
DOMContentLoaded
—— 当 HTML 的加载和处理均完成,DOM 被完全构建完成时。
- CSS 事件
transitionend
—— 当一个 CSS 动画完成时。
- 鼠标事件
- 事件处理程序
- 为了对事件作出响应,我们可以分配一个 处理程序(handler)—— 一个在事件发生时运行的函数。
- 处理程序是在发生用户行为(action)时运行 JavaScript 代码的一种方式。
- HTML 特性
- 在鼠标点击时,
onclick
中的代码就会运行。 - ,在
onclick
中,我们使用单引号,因为特性本身使用的是双引号。如果我们忘记了代码是在特性中的,而使用了双引号,像这样:onclick="alert("Click!")"
,那么它就无法正确运行。
- 在鼠标点击时,
- DOM 属性
- 我们可以使用 DOM 属性(property)
on<event>
来分配处理程序。
- 我们可以使用 DOM 属性(property)
- 访问元素:this
- 处理程序中的
this
的值是对应的元素。就是处理程序所在的那个元素。
- 处理程序中的
- 可能出现的错误
- 函数应该是以
sayThanks
的形式进行赋值,而不是sayThanks()
。 - 不要对处理程序使用
setAttribute
。 - DOM 属性是大小写敏感的。
- 函数应该是以
- addEventListener
- 我们不能为一个事件分配多个处理程序。
- element.addEventListener(event, handler[, options]);
- 移除需要相同的函数
- 要移除处理程序,我们需要传入与分配的函数完全相同的函数。
- 对于某些事件,只能通过 addEventListener 设置处理程序
- 事件对象
- 当事件发生时,浏览器会创建一个
event
对象,将详细信息放入其中,并将其作为参数传递给处理程序。 - event.type:事件类型
- event.currentTarget:处理事件的元素。
- event.clientX / event.clientY:指针事件(pointer event)的指针的窗口相对坐标。
event
对象在 HTML 处理程序中也可用
- 当事件发生时,浏览器会创建一个
- 对象处理程序:handleEvent
- 当事件发生时,就会调用该对象的
handleEvent
方法。 - 请注意,我们需要使用
addEventListener
来显式设置事件,以指明要监听的事件。这里的menu
对象只监听mousedown
和mouseup
,而没有任何其他类型的事件。
- 当事件发生时,就会调用该对象的
冒泡和捕获
- 冒泡
- 当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。
- ![[1746858166632.png]]
- 几乎所有事件都会冒泡。
focus
事件不会冒泡。
- event.target
- 父元素上的处理程序始终可以获取事件实际发生位置的详细信息。引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过
event.target
访问。 event.target与
this(=
event.currentTarget)之间的区别
event.target
—— 是引发事件的“目标”元素,它在冒泡过程中不会发生变化。
this
—— 是“当前”元素,其中有一个当前正在运行的处理程序。
form.onclick
处理程序中:this
(=event.currentTarget
)是<form>
元素,因为处理程序在它上面运行。
event.target
是表单中实际被点击的元素。
event.target
可能会等于this
—— 当点击事件发生在<form>
元素上时,就会发生这种情况。
- 父元素上的处理程序始终可以获取事件实际发生位置的详细信息。引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过
- 停止冒泡
- 冒泡事件从目标元素开始向上冒泡。通常,它会一直上升到
<html>
,然后再到document
对象,有些事件甚至会到达window
,它们会调用路径上所有的处理程序。 - 停止冒泡的方法是
event.stopPropagation()
。 - 不要在没有需要的情况下停止冒泡!
- 有时
event.stopPropagation()
会产生隐藏的陷阱,以后可能会成为问题。- 我们创建了一个嵌套菜单,每个子菜单各自处理对自己的元素的点击事件,并调用
stopPropagation
,以便不会触发外部菜单。 - 之后,我们决定捕获在整个窗口上的点击,以追踪用户的行为(用户点击的位置)。有些分析系统会这样做。通常,代码会使用
document.addEventListener('click'…)
来捕获所有的点击。 - 我们的分析不适用于被
stopPropagation
所阻止点击的区域。太伤心了,我们有一个“死区”。
- 我们的分析不适用于被
- 我们创建了一个嵌套菜单,每个子菜单各自处理对自己的元素的点击事件,并调用
- 冒泡事件从目标元素开始向上冒泡。通常,它会一直上升到
- 捕获
- 事件处理的另一个阶段被称为“捕获(capturing)”。它很少被用在实际开发中,但有时是有用的。
- DOM 事件标准描述了事件传播的 3 个阶段
- 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
- 目标阶段(Target phase)—— 事件到达目标元素。
- 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。
- 使用
on<event>
属性或使用 HTML 特性(attribute)或使用两个参数的addEventListener(event, handler)
添加的处理程序,对捕获一无所知,它们仅在第二阶段和第三阶段运行。
JavaScriptelem.addEventListener(..., {capture: true}) // 或者,用 {capture: true} 的别名 "true" elem.addEventListener(..., true)
capture
选项有两个可能的值:
- 如果为
false
(默认值),则在冒泡阶段设置处理程序。 - 如果为
true
,则在捕获阶段设置处理程序。 PS:请注意,虽然形式上有 3 个阶段,但第 2 阶段(“目标阶段”:事件到达元素)没有被单独处理:捕获阶段和冒泡阶段的处理程序都在该阶段被触发。 4. 有一个属性event.eventPhase
,它告诉我们捕获事件的阶段数。但它很少被使用,因为我们通常是从处理程序中了解到它 5. 要移除处理程序,removeEventListener
需要同一阶段 1. 如果我们addEventListener(..., true)
,那么我们应该在removeEventListener(..., true)
中提到同一阶段,以正确删除处理程序。 6. 同一元素的同一阶段的监听器按其设置顺序运行 7. 如果我们在同一阶段有多个事件处理程序,并通过addEventListener
分配给了相同的元素,则它们的运行顺序与创建顺序相同
elem.addEventListener("click", e => alert(1)); // 会先被触发
elem.addEventListener("click", e => alert(2));
事件委托
捕获和冒泡允许我们实现最强大的事件处理模式之一,即 事件委托 模式。这个想法是,如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上。 在点击时高亮显示被点击的单元格
//html
<table>
<tr>
<th colspan="3">
<em>Bagua</em> Chart: Direction, Element, Color, Meaning
</th>
</tr>
<tr>
<td class="nw">
<strong>Northwest</strong><br />Metal<br />Silver<br />Elders
</td>
<td class="n">...</td>
<td class="ne">...</td>
</tr>
<tr>
...2 more lines of this kind...
</tr>
<tr>
...2 more lines of this kind...
</tr>
</table>
//JS
<script>
let selectedTd;
table.onclick = function (event) {
let target = event.target; // 在哪里点击的?
if (target.tagName != "TD") return; // 不在 TD 上?那么我们就不会在意
highlight(target); // 高亮显示它
};
function highlight(td) {
if (selectedTd) {
// 移除现有的高亮显示,如果有的话
selectedTd.classList.remove("highlight");
}
selectedTd = td;
selectedTd.classList.add("highlight"); // 高亮显示新的 td
}
</script>
委托示例:标记中的行为
- 例如,我们想要编写一个有“保存”、“加载”和“搜索”等按钮的菜单。并且,这里有一个具有
save
、load
和search
等方法的对象。如何匹配它们? - 我们可以为整个菜单添加一个处理程序,并为具有方法调用的按钮添加
data-action
特性(attribute)
html<html> <div id="menu"> <button data-action="save">Save</button> <button data-action="load">Load</button> <button data-action="search">Search</button> </div> </html> <script> class Menu { constructor(elem) { this._elem = elem; elem.onclick = this.onClick.bind(this); // (*) } save() { alert("saving"); } load() { alert("loading"); } search() { alert("searching"); } onClick(event) { let action = event.target.dataset.action; if (action) { this[action](); } } } new Menu(menu); </script> // 注意:this.onClick 在 (*) 行中被绑定到了 this。 //这很重要,因为否则内部的 this 将引用 DOM 元素(elem), //而不是 Menu 对象,那样的话,this[action] 将不是我们所需要的。
- 例如,我们想要编写一个有“保存”、“加载”和“搜索”等按钮的菜单。并且,这里有一个具有
事件委托的好处(根据以上案例)
- 我们不需要编写代码来为每个按钮分配一个处理程序。只需要创建一个方法并将其放入标记(markup)中即可。
- HTML 结构非常灵活,我们可以随时添加/移除按钮
“行为”模式
- 我们还可以使用事件委托将“行为(behavior)”以 声明方式 添加到具有特殊特性(attribute)和类的元素中。
- 行为模式分为两个部分
- 我们将自定义特性添加到描述其行为的元素。
- 用文档范围级的处理程序追踪事件,如果事件发生在具有特定特性的元素上 —— 则执行行为(action)。
行为:计数器
html<html> Counter: <input type="button" value="1" data-counter /> One more counter: <input type="button" value="2" data-counter /> </html> <script> document.addEventListener("click", function (event) { if (event.target.dataset.counter != undefined) { // 如果这个特性存在... event.target.value++; } }); </script>
- 对于文档级的处理程序 —— 始终使用的是
addEventListener
- 当我们将事件处理程序分配给
document
对象时,我们应该始终使用addEventListener
, 而不是document.on<event>
,因为后者会引起冲突:新的处理程序会覆盖旧的处理程序。
- 对于文档级的处理程序 —— 始终使用的是
行为:切换器
//点击一个具有 `data-toggle-id` 特性的元素将显示/隐藏具有给定 `id` 的元素
<button data-toggle-id="subscribe-mail">
Show the subscription form
</button>
<form id="subscribe-mail" hidden>
Your mail: <input type="email">
</form>
<script>
document.addEventListener('click', function(event) {
let id = event.target.dataset.toggleId;
if (!id) return;
let elem = document.getElementById(id);
elem.hidden = !elem.hidden;
});
</script>
浏览器默认行为
许多事件会自动触发浏览器执行某些行为。例如:
mousedown
—— 开始选择(移动鼠标进行选择)。- 在
<input type="checkbox">
上的click
—— 选中/取消选中的input
。 submit
—— 点击<input type="submit">
或者在表单字段中按下 Enter 键会触发该事件,之后浏览器将提交表单。keydown
—— 按下一个按键会导致将字符添加到字段,或者触发其他行为。contextmenu
—— 事件发生在鼠标右键单击时,触发的行为是显示浏览器上下文菜单。- ……还有更多……
- 阻止浏览器行为 2. 有两种方式来告诉浏览器我们不希望它执行默认行为
- 主流的方式是使用
event
对象。有一个event.preventDefault()
方法。 - 如果处理程序是使用
on<event>
(而不是addEventListener
)分配的,那返回false
也同样有效。 - 从处理程序返回
false
是一个例外- 事件处理程序返回的值通常会被忽略。唯一的例外是从使用
on<event>
分配的处理程序中返回的return false
。在所有其他情况下,return
值都会被忽略。并且,返回true
没有意义。
- 事件处理程序返回的值通常会被忽略。唯一的例外是从使用
- 后续事件
- 某些事件会相互转化。如果我们阻止了第一个事件,那就没有第二个事件了
- 例如:在
<input>
字段上的mousedown
会导致在其中获得焦点,以及focus
事件。如果我们阻止mousedown
事件,在这就没有焦点了。
- 主流的方式是使用
- 处理程序选项 “passive” 6.
addEventListener
的可选项passive: true
向浏览器发出信号,表明处理程序将不会调用preventDefault()
。 7. 这样做的原因:- 移动设备上会发生一些事件,例如
touchmove
(当用户在屏幕上移动手指时),默认情况下会导致滚动,但是可以使用处理程序的preventDefault()
来阻止滚动。 - 因此,当浏览器检测到此类事件时,它必须首先处理所有处理程序,然后如果没有任何地方调用
preventDefault
,则页面可以继续滚动。但这可能会导致 UI 中不必要的延迟和“抖动”。 passive: true
选项告诉浏览器,处理程序不会取消滚动。然后浏览器立即滚动页面以提供最大程度的流畅体验,并通过某种方式处理事件。- 对于某些浏览器(Firefox,Chrome),默认情况下,
touchstart
和touchmove
事件的passive
为true
。
- 移动设备上会发生一些事件,例如
- event.defaultPrevented 9. 如果默认行为被阻止,那么
event.defaultPrevented
属性为true
,否则为false
。 10. 有时我们可以使用event.defaultPrevented
来代替,来通知其他事件处理程序,该事件已经被处理。 11. 一个实际例子html以上代码问题是,当我们点击//默认情况下,浏览器在 `contextmenu` 事件(单击鼠标右键)时,显示带有标准选项的上下文菜单。我们可以阻止它并显示我们自定义的菜单 <html> <button>Right-click shows browser context menu</button> <button oncontextmenu="alert('Draw our menu'); return false"> Right-click shows our context menu </button> //显示最近的上下文菜单 <p>Right-click here for the document context menu</p> <button id="elem">Right-click here for the button context menu</button> </html> <script> elem.oncontextmenu = function(event) { event.preventDefault(); alert("Button context menu"); }; document.oncontextmenu = function(event) { event.preventDefault(); alert("Document context menu"); }; </script>
elem
时,我们会得到两个菜单:按钮级和文档级(事件冒泡)的菜单。 解决方法:“当我们在按钮处理程序中处理鼠标右键单击事件时,我们阻止其冒泡”
<html>
<p>Right-click for the document menu</p>
<button id="elem">
Right-click for the button menu (fixed with event.stopPropagation)
</button>
</html>
<script>
elem.oncontextmenu = function (event) {
event.preventDefault();
event.stopPropagation(); //阻止冒泡
alert("Button context menu");
};
document.oncontextmenu = function (event) {
event.preventDefault();
alert("Document context menu");
};
</script>
以上解决方法阻止了任何外部代码对右键点击信息的访问,包括收集统计信息的计数器等。这是非常不明智的。 另一个替代方案是,检查 document
处理程序是否阻止了浏览器的默认行为?如果阻止了,那么该事件已经得到了处理,我们无需再对此事件做出反应。
<html>
<p>
Right-click for the document menu (added a check for event.defaultPrevented)
</p>
<button id="elem">Right-click for the button menu</button>
</html>
<script>
elem.oncontextmenu = function (event) {
event.preventDefault();
alert("Button context menu");
};
document.oncontextmenu = function (event) {
if (event.defaultPrevented) return;
event.preventDefault();
alert("Document context menu");
};
</script>
- event.stopPropagation() 和 event.preventDefault() 2. 正如我们所看到的,
event.stopPropagation()
和event.preventDefault()
(也被认为是return false
)是两个不同的东西。它们之间毫无关联。 - 嵌套的上下文菜单结构 4. 还有其他实现嵌套上下文菜单的方式。其中之一是拥有一个具有
document.oncontextmenu
处理程序的全局对象,以及使我们能够在其中存储其他处理程序的方法。 5. 该对象将捕获任何右键单击,浏览存储的处理程序并运行适当的处理程序。 6. 但是,每段需要上下文菜单的代码都应该了解该对象,并使用它的帮助,而不是使用自己的contextmenu
处理程序。
创建自定义事件
我们不仅可以分配事件处理程序,还可以从 JavaScript 生成事件。自定义事件可用于创建“图形组件”。例如,我们自己的基于 JavaScript 的菜单的根元素可能会触发 open
(打开菜单),select
(有一项被选中)等事件来告诉菜单发生了什么。另一个代码可能会监听事件,并观察菜单发生了什么。我们不仅可以生成出于自身目的而创建的全新事件,还可以生成例如 click
和 mousedown
等内建事件。这可能会有助于自动化测试。
- 事件构造器
- 内建事件类形成一个层次结构(hierarchy),类似于 DOM 元素类。根是内建的 Event 类。
- let event = new Event(type[, options]);
- type:事件类型,例如:
"click"
- options —— 具有两个可选属性的对象:
bubbles: true/false
—— 如果为true
,那么事件会冒泡。cancelable: true/false
—— 如果为true
,那么“默认行为”就会被阻止。稍后我们会看到对于自定义事件,它意味着什么。- 默认情况下都是false
- type:事件类型,例如:
- dispatchEvent
- 事件对象被创建后,我们应该使用
elem.dispatchEvent(event)
调用在元素上“运行”它。 - 然后,处理程序会对它做出反应,就好像它是一个常规的浏览器事件一样。如果事件是用
bubbles
标志创建的,那么它会冒泡。 - event.isTrusted
- 对于来自真实用户操作的事件,
event.isTrusted
属性为true
,对于脚本生成的事件,event.isTrusted
属性为false
。
- 对于来自真实用户操作的事件,
- 事件对象被创建后,我们应该使用
- 冒泡示例
<h1 id="elem">Hello from the script!</h1>
<script>
// 在 document 上捕获...
document.addEventListener("hello", function(event) { // (1)
alert("Hello from " + event.target.tagName); // Hello from H1
});
// ...在 elem 上 dispatch!
let event = new Event("hello", {bubbles: true}); // (2)
elem.dispatchEvent(event);
// 在 document 上的处理程序将被激活,并显示消息。
</script>
//注意
1. 我们应该对我们的自定义事件使用 `addEventListener`,因为 `on<event>` 仅存在于内建事件中,`document.onhello` 则无法运行。
2. 必须设置 `bubbles:true`,否则事件不会向上冒泡。
3. 内建事件(`click`)和自定义事件(`hello`)的冒泡机制相同。自定义事件也有捕获阶段和冒泡阶段。
- MouseEvent,KeyboardEvent 及其他
- 这是一个摘自于 UI 事件规范 的一个简短的 UI 事件类列表:
UIEvent
FocusEvent
MouseEvent
WheelEvent
KeyboardEvent
- …
- 如果我们想要创建这样的事件,我们应该使用它们而不是
new Event
。例如,new MouseEvent("click")
。 - 正确的构造器允许为该类型的事件指定标准属性。
- 这是一个摘自于 UI 事件规范 的一个简短的 UI 事件类列表:
let event = new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: 100,
clientY: 100
});
alert(event.clientX); // 100
//通用的 Event 构造器不允许这样做。
- 自定义事件
- 对于我们自己的全新事件类型,例如
"hello"
,我们应该使用new CustomEvent
。从技术上讲,CustomEvent 和Event
一样。除了一点不同。在第二个参数(对象)中,我们可以为我们想要与事件一起传递的任何自定义信息添加一个附加的属性detail
。
- 对于我们自己的全新事件类型,例如
<h1 id="elem">Hello for John!</h1>
<script>
// 事件附带给处理程序的其他详细信息
elem.addEventListener("hello", function(event) {
alert(event.detail.name);
});
elem.dispatchEvent(new CustomEvent("hello", {
detail: { name: "John" }
}));
</script>
detail 属性可以有任何数据。从技术上讲,我们可以不用,因为我们可以在创建后将任何属性分配给常规的 new Event 对象中。但是 CustomEvent 提供了特殊的 detail 字段,以避免与其他事件属性的冲突。 6. event.preventDefault() 1. 许多浏览器事件都有“默认行为”。对于新的,自定义的事件,绝对没有默认的浏览器行为,但是分派(dispatch)此类事件的代码可能有自己的计划,触发该事件之后应该做什么。 2. 通过调用 event.preventDefault()
,事件处理程序可以发出一个信号,指出这些行为应该被取消。在这种情况下,elem.dispatchEvent(event)
的调用会返回 false
。那么分派(dispatch)该事件的代码就会知道不应该再继续。 3. 任何处理程序都可以使用 rabbit.addEventListener('hide',...)
来监听该事件,并在需要时使用 event.preventDefault()
来取消该行为。 4. 事件必须具有 cancelable: true
标志,否则 event.preventDefault()
调用将会被忽略。 7. 事件中的事件是同步的 1. 通常事件是在队列中处理的。也就是说:如果浏览器正在处理 onclick
,这时发生了一个新的事件,例如鼠标移动了,那么它的处理程序会被排入队列,相应的 mousemove
处理程序将在 onclick
事件处理完成后被调用。 2. 一个事件是在另一个事件中发起的。例如使用 dispatchEvent
。这类事件将会被立即处理,即在新的事件处理程序被调用之后,恢复到当前的事件处理程序。
<button id="menu">Menu (click me)</button>
<script>
menu.onclick = function() {
alert(1);
menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
}));
alert(2);
};
// 在 1 和 2 之间触发
document.addEventListener('menu-open', () => alert('nested'));
</script>
//输出顺序为:1 → nested → 2。
想让 onclick
不受 menu-open
或者其它嵌套事件的影响,优先被处理完毕。 将 dispatchEvent
(或另一个触发事件的调用)放在 onclick
末尾,或者最好将其包装到零延迟的 setTimeout
中
<button id="menu">Menu (click me)</button>
<script>
menu.onclick = function() {
alert(1);
setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
})));
alert(2);
};
document.addEventListener('menu-open', () => alert('nested'));
</script>
现在,dispatchEvent 在当前代码执行完成之后异步运行,包括 menu.onclick,因此,事件处理程序是完全独立的。
//输出顺序变成:1 → 2 → nested。