🥘 JavaScript 浏览器事件
# 浏览器事件简介
DOM 事件是在某个时刻触发的信号。
常见的事件列表:
鼠标事件:
click
:鼠标点击一个元素时(或者触摸屏点击元素)。contextmenu
:当鼠标右键点击一个元素时。mouseover
/mouseout
: 当鼠标指针移入 / 离开一个元素时。mousedown
/mouseup
:当在元素上按下 / 释放鼠标按钮时。mousemove
:当鼠标移动时。
键盘事件:
keydown
和keyup
:当按下和松开一个按键时。
表单事件:
submit
:当访提交了一个<form>
时。focus
:当访问者聚焦于一个表单元素时,例如聚焦于一个<input>
。
文档事件:
DOMContentLoaded
:当 HTML 的加载和处理均完成,DOM 被完全构建完成时。
CSS 事件:
transitionend
:当一个 CSS 动画完成时。
# 事件处理
要对事件响应,可以分配一个处理事件的程序:一个事件发生时运行的函数。
# 对于 HTML 特性
🌰 例子 / 点击按钮事件:
<input value="Click me" onclick="alert('Click!')" type="button">
🌰 例子 / 事件处理函数:
<script>
function countRabbits() {
for(let i=1; i<=3; i++) {
alert("Rabbit number " + i);
}
}
</script>
<input type="button" onclick="countRabbits()" value="Count rabbits!">
2
3
4
5
6
7
8
9
由于 HTML 中标签特性 大小写不敏感,所以事件特性命名可以随意,但是一般小写,例如 onclick
。
# 对于 DOM 属性
使用 DOM 属性 on<event>
分配事件处理程序。
🌰 例子:
<input id="elem" type="button" value="Click me">
<script>
let elem = document.querySelector("#elem")
elem.onclick = function() {
alert('Thank you');
};
</script>
2
3
4
5
6
7
如果一个处理程序是通过 HTML 特性分配的,那么随后浏览器读取它,并从特性的内容创建一个新函数,并将这个函数写入 DOM 属性。
注意
注意 DOM 属性是大小写敏感的。
初始化的顺序:
🌰 例子:
<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
elem.onclick = function() { // 覆盖了现有的处理程序
alert('After'); // 只会显示此内容
};
</script>
2
3
4
5
6
这个示例中,使用 JavaScript 添加了一个处理程序,覆盖了现有的处理程序。
所以如果要溢出这个处理程序,赋值 elem.onclick = null
。
# 元素 this
处理程序中的 this
的值是对应的元素。就是处理程序所在的那个元素。
🌰 例子:
<button onclick="alert(this.innerHTML)">Click me</button>
::: demo[vanilla]
<button onclick="alert(this.innerHTML)">Click me</button>
:::
# 注意问题
在脚本中,使用现存的函数作为事件的处理程序时,赋值时应该使用 函数名不带
()
的形式。🌰 例子:
// 正确 button.onclick = sayThanks; // 错误 button.onclick = sayThanks();
1
2
3
4
5如果添加了
()
,那么就变成了 函数的调用,赋值的是 函数运行之后的结果(没有返回值则为undefined
),并没有赋值任何的事件处理handler
。在 HTML 特性中,分配事件处理程序需要加
()
。🌰 例子:
<input type="button" id="button" onclick="sayThanks()">
1因为给特性赋值,相当于给 属性赋值:
button.onclick = function() { sayThanks(); // <-- 特性(attribute)中的内容变到了这里 };
1
2
3所以这里需要 事件处理程序的执行。
不要对处理程序使用
setAttribute
:🌰 例子:
document.body.setAttribute('onclick', function() { alert(1) }); // 无效
1
# 事件监听 addEventListener
要为事件 分配多个处理程序时,使用 addEventListener
和 removeEventListener
管理处理程序。
对元素添加处理程序:
element.addEventListener(event, handler[, options]);
1event
:事件名称。(例如click
)handler
:处理程序。options
:附加可选对象:once
:如果为true
,那么会在被触发后自动删除监听器。一次性事件处理。capture
:事件处理的阶段passive
:如果为true
,那么处理程序将不会调用preventDefault()
(浏览器默认行为)。
对元素移除处理程序:
element.removeEventListener(event, handler[, options]);
1移除的程序必须相同。所以存储在变量中。
对元素添加多个处理程序:
多次调用addEventListener
:<input id="elem" type="button" value="Click me"/> <script> function handler1() { alert('Thanks!'); }; function handler2() { alert('Thanks again!'); } elem.onclick = () => alert("Hello"); elem.addEventListener("click", handler1); // Thanks! elem.addEventListener("click", handler2); // Thanks again! </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15可以 同时 使用 DOM 属性和
addEventListener
来设置处理程序。通常我们只使用其中一种方式。
对于某些事件只能使用
addEventListener
添加处理程序:🌰 例子 /
DOMContentLoaded
事件,该事件在文档加载完成并且 DOM 构建完成时触发。:document.addEventListener("DOMContentLoaded", function() { alert("DOM built"); });
1
2
3
# 事件对象
当事件发生时,浏览器会创建一个 event
对象,将详细信息放入其中,并将其作为参数传递给处理程序。
🌰 例子 / 从 event
对象获取鼠标指针的坐标:
<input type="button" value="Click me" id="elem">
<script>
elem.onclick = function(event) {
// 显示事件类型、元素和点击的坐标
alert(event.type + " at " + event.currentTarget);
alert("Coordinates: " + event.clientX + ":" + event.clientY);
};
</script>
2
3
4
5
6
7
8
事件对象的一般属性:
event.type
: 事件的类型。event.currentTarget
:处理事件的元素。与this
相同,除非处理程序是一个箭头函数,或者它的this
被绑定到了其他东西上,之后可以从event.currentTarget
获取元素了。event.clientX
/event.clientY
:指针事件的指针的窗口相对坐标。
# 对象处理程序
处理事件的程序除了可以分配函数,还可以将一个 对象 分配为事件处理程序。回调用对象中的 handleEvent
方法。
🌰 例子:
<button id="elem">Click me</button>
<script>
let obj = {
handleEvent(event) {
alert(event.type + " at " + event.currentTarget);
}
};
elem.addEventListener('click', obj);
</script>
2
3
4
5
6
7
8
9
10
11
🌰 例子 / 使用一个类:
<button id="elem">Click me</button>
<script>
class Menu {
handleEvent(event) {
switch(event.type) {
case 'mousedown':
elem.innerHTML = "Mouse button pressed";
break;
case 'mouseup':
elem.innerHTML += "...and released.";
break;
}
}
}
let menu = new Menu();
elem.addEventListener('mousedown', menu);
elem.addEventListener('mouseup', menu);
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
🌰 例子 / handleEvent
调用其他特定于事件的方法:
<button id="elem">Click me</button>
<script>
class Menu {
handleEvent(event) {
// mousedown -> onMousedown
let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
this[method](event);
}
onMousedown() {
elem.innerHTML = "Mouse button pressed";
}
onMouseup() {
elem.innerHTML += "...and released.";
}
}
let menu = new Menu();
elem.addEventListener('mousedown', menu);
elem.addEventListener('mouseup', menu);
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
将事件处理程序明确地分离了出来,这样更容易进行代码编写和后续维护。
# 冒泡
🌰 例子 / 引入:
<div onclick="alert('The handler!')">
<em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>
2
3
事件处理程序分配给标签
<div>
。但是点击其中嵌套的标签(<em>
或<code>
) 都会触发该事件处理程序。
冒泡的原理(Bubbling):当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。
🌰 例子:
::: demo[vanilla]
<style>
#example * {
margin: 10px;
border: 1px solid blue;
}
</style>
<html>
<form id="example" onclick="alert('form')"> FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
:::
点击内部的
<p>
会首先运行onclick
:
- 在该
<p>
上的。- 然后是外部
<div>
上的。- 然后是外部
<form>
上的。- 以此类推,直到最后的
document
对象。这个过程就是冒泡。
** 几乎所有事件都会冒泡。** 例外: focus
事件不会冒泡。
# 目标元素 event.target
父元素上的处理程序始终可以获取事件实际发生位置的详细信息。引发事件的那个嵌套层级最深的元素被称为目标元素 ,通过 event.target
访问。
this
与 event.target
的区别:
event.target
:引发事件的目标元素。冒泡过程不会变化。this
:当前元素。随着冒泡过程,是当前正在运行的处理程序的元素。
🌰 例子:
这个例子中,处理程序
form.onclick
,那么它可以捕获表单内的所有点击。所以无论点击发生在哪个元素,都会冒泡到这个处理程序,所以此时:
event.target
:实际被点击的元素;this
:<form>
元素,因为处理程序在它上面运行。(
event.target
可以等于this
,在点击发生在<form>
时)
# 停止冒泡
用于停止冒泡的方法是 event.stopPropagation()
。
事件的冒泡一旦发生,通常会一直上升到
<html>
再到document
,甚至到达window
,回调用路径上所有的处理程序。但是任意处理程序都可以决定事件已经被完全处理,并停止冒泡。
🌰 例子:
<body onclick="alert(`the bubbling doesn't reach here`)">
<button onclick="event.stopPropagation()">Click me</button>
</body>
2
3
这里的按钮的
body.onclick
事件会停止工作。
event.stopImmediatePropagation()
可以用于 停止冒泡,并且阻止当前元素上的处理程序运行,(意味着,其他处理程序不会执行)。
注意
非必要,不阻止。阻止冒泡后可能会出现的问题:
- 当创建了一个嵌套菜单,每个子菜单各自处理对自己的元素的点击事件,并调用
stopPropagation
,以便不会触发外部菜单;当在外部要捕获窗口的 用户的点击时,阻止冒泡后,这个区域不能被捕获到。
通常,没有真正的必要去阻止冒泡。如果真的要阻止冒泡,通常使用其他方法 如自定义事件;还可以数据写入一个处理程序中的 event
对象,并在另一个处理程序中读取该数据,这样就可以向父处理程序传递有关下层处理程序的信息。
# 捕获
DOM 描述事件传播的三个阶段:
- 捕获阶段:事件(从 window)走进元素
- 目标阶段:事件到达目标元素;
- 冒泡阶段:事件从元素上开始冒泡。
捕获阶段一般很少用到,所以通常看不见。
为了在捕获阶段捕获事件,需要将处理程序的 capture
选项设置为 true
( addEventListener
中的选项参数):
- 为
false
(默认值),则在冒泡阶段设置处理程序。 - 为
true
,则在捕获阶段设置处理程序。
虽然形式上有三个阶段,但实际上第二阶段没有被单独处理,捕获阶段和冒泡阶段的处理程序都在该阶段被触发。
🌰 例子:
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form>FORM
<div>DIV
<p>P</p>
</div>
</form>
<script>
for(let elem of document.querySelectorAll('*')) {
elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
例子中,为文档中的 每个 元素都设置了点击处理程序,点击
<p>
时:
HTML
→BODY
→FORM
→DIV
(捕获阶段第一个监听器):P
(目标阶段,触发两次,因为设置了两个监听器:捕获和冒泡)DIV
→FORM
→BODY
→HTML
(冒泡阶段,第二个监听器)。
event.eventPhase
可以获得事件的阶段数。但它很少被使用,因为通常是从处理程序中了解到它。
提示
要移除处理程序,
removeEventListener
需要同一阶段。如果addEventListener(..., true)
,那么应该在removeEventListener(..., true)
中提到同一阶段,以正确删除处理程序。同一元素的同一阶段的监听器按其设置顺序运行。如果在同一阶段有多个事件处理程序,并通过
addEventListener
分配给了相同的元素,则它们的运行顺序与创建顺序相同:elem.addEventListener("click", e => alert(1)); // 会先被触发 elem.addEventListener("click", e => alert(2));
1
2
# 事件委托
事件的捕获与冒泡阶段 允许了 事件的委托模式。
思路:如果有类似的方式处理的元素,那么就不用为每一个元素都分配一个处理程序,而是要将单个处理程序放在共同的祖先。
🌰 例子:
要实现如上的 在点击时高亮显示该单元格。无论有多少个单元格,都可以分配处理的程序。
可以在 table
元素上设置一个 捕获所有点击 的处理程序(利用事件的捕获阶段),这样不用担心表格有多少个单元格,也可以随时动态增加 / 移除 <td>
元素,点击高粱依然有效:
let selectedTd;
table.onclick = function(event) {
let target = event.target
if(target.tagName != 'TD') {
return // 不处理没有点在td上的事件
}
highlisht(target)
}
function highlight(td) {
if (selectedTd) { // 移除现有的高亮显示,如果有的话
selectedTd.classList.remove('highlight');
}
selectedTd = td;
selectedTd.classList.add('highlight'); // 高亮显示新的 td
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
但由于
<td>
内部还有许多嵌套的元素标签,所以如果点击例如<strong>
元素时,strong
元素称为event.target
,这样也要考虑在td
的范围内,改进为:table.onclick = function(event) { let td = event.target.closest('td'); // (1) if (!td) return; // (2) if (!table.contains(td)) return; // (3) highlight(td); // (4) };
1
2
3
4
5
6
7
8
9使用元素的
closet
属性,找到最近的祖先元素。
# 事件委托给 标记的行为
🌰 例子 / 如果要编写一个有 “保存”、“加载” 和 “搜索” 等按钮的菜单。并且,这里有一个具有 save
、 load
和 search
等方法的对象。
要匹配它们对应到的事件,如果每次单独分配处理程序过于麻烦。可以考虑给整个菜单添加一个处理程序,并且为具有方法调用的按钮添加 data-action
特性:
<button data-action="save">Click to Save</button>
完整代码:
注意:
this.onclick.bind(this)
要绑定Menu
对象的this
上下文对象,否则在处理程序内部 引用的是element
对象而不是Menu
对象。
提示
使用事件委托的好处:
- 不需要编写代码给每一个按钮分配单独的处理程序。只需要创建一个处理程序获取该元素的标记,以分配对应的行为;
- 并且可以随时灵活移除 / 增加按钮。
# 行为模式
事件委托将行为以 声明的方式 添加到具有特殊特性和类的元素中。
行为模式的两个部分:
- 将自定义特性添加到描述其行为的元素。
- 用文档范围级的处理程序追踪事件,如果事件发生在具有特定特性的元素上,则执行行为。
🌰 例子 / 计数器:
可以根据需要使用
data-counter
特性到相应的元素上。使用事件委托可以根据该行为特性处理事件。
提示
对于文档级的处理程序, 始终使用的是 addEventListener
:
将事件处理程序分配给 document
对象时,应该始终使用 addEventListener
,而不是 document.on<event>
,因为后者会引起冲突:新的处理程序会覆盖旧的处理程序。
对于实际项目来说。在 document
上有许多由代码的不同部分设置的处理程序,这是很正常的。
🌰 例子 / 显示隐藏切换器 toggle:
因为添加了对
toggleId
的行为的事件处理程序。所以现在向元素添加功能,只需要添加一个行为模式的特性data-toggle-id
, 无需为每个这样的元素编写 JavaScript。只需要使用行为。文档级处理程序使其适用于页面的任意元素。
提示
事件委托的使用,通常用于为许多相似的元素添加相同的处理,但不仅限于此。
好处:
- 简化初始化并节省内存:无需添加许多处理程序。
- 更少的代码:添加或移除元素时,无需添加 / 移除处理程序。
局限:
- 首先,事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用
event.stopPropagation()
。 - 其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣。但是,通常负载可以忽略不计,所以我们不考虑它。
# 浏览器事件默认行为
常见的默认行为:
- 点击一个链接,触发导航到该 URL;
- 点击表单的提交按钮,触发提交到服务器的行为;
- 在文本上按下鼠标按钮并移动,选中文本;
# 阻止浏览器的默认行为
两种方法:
- 使用
event
对象。有一个event.preventDefault()
方法。 - 使用
on<event>
(而不是addEventListener
)分配的,那返回false
也同样有效。
🌰 例子:
事件处理程序返回的值通常会被忽略。唯一的例外是从使用
on<event>
分配的处理程序中返回的return false
。在所有其他情况下,
return
值都会被忽略。并且,返回true
没有意义。
🌰 例子 / 处理导航菜单的默认行为:
<script>
let menu = document.querySelector("#menu")
menu.onclick = function(event) {
if(event.target.nodeName != 'A') return;
let href = event.target.getAttribute('href')
alert(href);
return false;
}
</script>
2
3
4
5
6
7
8
9
10
11
# passive
addEventListener
的可选项 passive: true
向浏览器发出信号,表明处理程序将不会调用 preventDefault()
。
移动设备上会发生一些事件,例如
touchmove
(当用户在屏幕上移动手指时),默认情况下会导致滚动,但是可以使用处理程序的preventDefault()
来阻止滚动。因此,当浏览器检测到此类事件时,它必须首先处理所有处理程序,然后如果没有任何地方调用
preventDefault
,则页面可以继续滚动。但这可能会导致 UI 中不必要的延迟和 “抖动”。
passive: true
选项告诉浏览器,处理程序不会取消滚动。然后浏览器立即滚动页面以提供最大程度的流畅体验,并通过某种方式处理事件。某些浏览器默认情况下,
touchstart
和touchmove
事件的passive
为true
。
# 阻止事件冒泡
可以使用 event.defaultPrevented
来代替 event.stopPropagation()
,来通知其他事件处理程序,该事件已经被处理。
🌰 例子:
默认情况下,浏览器在 contextmenu
事件(单击鼠标右键)时,显示带有标准选项的上下文菜单。可以阻止它并显示我们自定义的菜单:
除了对于特定的元素,还可以实现文档范围内的:
<p>Right-click here for the document context menu</p>
<button id="elem">Right-click here for the button context menu</button>
<script>
elem.oncontextmenu = function(event) {
event.preventDefault();
alert("Button context menu");
};
document.oncontextmenu = function(event) {
event.preventDefault();
alert("Document context menu");
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这个例子中,点击
elem
时,会得到两个菜单:按钮级和文档级(事件冒泡)的菜单。解决方案,阻止事件冒泡:
<script> elem.oncontextmenu = function(event) { event.preventDefault(); event.stopPropagation(); alert("Button context menu"); }; document.oncontextmenu = function(event) { event.preventDefault(); alert("Document context menu"); }; </script>
1
2
3
4
5
6
7
8
9
10
11
12但是阻止事件冒泡存在代价,现在 右键点击信息 相关的事件,都是被取消的。
采用另一个解决方案,检查是否已经被阻止了默认行为即可,如果已经阻止就不用处理:
<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>
2
3
4
5
6
7
8
9
10
11
12
13
现在,如果有嵌套的元素,并且每个元素都有自己的上下文菜单,那么这也是可以运行的。只需确保检查每个
contextmenu
处理程序中的event.defaultPrevented
。
# 自定义事件
自定义事件可用于创建图形组件。例如,基于 JavaScript 的菜单的根元素可能会触发 open
(打开菜单), select
(有一项被选中)等事件来告诉菜单发生了什么。另一个代码可能会监听事件,并观察菜单发生了什么。
不仅可以出于自身目的而创建的全新事件,还可以生成例如 click
和 mousedown
等内建事件。这可能会有助于自动化测试。
# 事件的构造器
let evnet = new Event(type[, options])
type
:事件类型;options
:两个可选属性对象配置:bubbles: true/false
:如果为true
,那么事件会冒泡。cancelable: true/false
:如果为true
,那么默认行为就会被阻止。
默认情况下,两个配置都为
false
,即事件不冒泡并且不阻止默认行为。
# 调用自定义事件
使用 elem.dispatchEvent(event)
调用在元素上运行 event
。然后,处理程序会对它做出反应,就好像它是一个常规的浏览器事件一样。
🌰 例子:
<button id="elem" onclick="alert('Click!');">Autoclick</button>
<script>
let elem = document.querySelector("#elem")
let event = new Event("click");
elem.dispatchEvent(event);
</script>
2
3
4
5
6
7
这个例子中,
click
事件是用 JavaScript 初始化创建的。处理程序工作方式和点击按钮的方式相同。
# 区分自定义事件
使用 event.isTrusted
区分真实用户事件和 通过脚本生成的自定义事件:
- 对于来自真实用户操作的事件,
event.isTrusted
属性为true
; - 对于脚本生成的事件,
event.isTrusted
属性为false
。
# 自定义事件的冒泡
🌰 例子 / 创建一个自定义的冒泡事件,并在 document
上捕获:
<h1 id="elem">
Hello from the script
</h1>
<script>
// 在 document 捕获 hello 事件
document.addEventListener("hello", function(event) {
alert("Hello from" + event.target.tagName)
})
let event = new Event("hello", {bubbls: true});
elem.dispatch(event)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
- 应该对自定义事件使用
addEventListener
,因为on<event>
仅存在于内建事件中,document.onhello
则无法运行。- 必须设置
bubbles:true
,否则事件不会向上冒泡。
内建事件( click
)和自定义事件( hello
)的冒泡机制相同。自定义事件也有捕获阶段和冒泡阶段。
# 自定义事件的类型
通常的 UI 事件类有:
UIEvent
FocusEvent
MouseEvent
WheelEvent
KeyboardEvent
- …
相关的 UI 事件规定 https://www.w3.org/TR/uievents
要创建这样类型的事件,使用它们而不是 new Event
,正确的构造器允许为该类型的事件指定标准属性。
🌰 例子:
let event = new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: 100,
clientY: 100
});
alert(event.clientX);
2
3
4
5
6
7
8
对于通用的 Event
构造器,不允许这样:
let event = new Event("click", {
bubbles: true, // 构造器 Event 中只有 bubbles 和 cancelable 可以工作
cancelable: true,
clientX: 100,
clientY: 100
});
alert(event.clientX); // undefined,未知的属性被忽略了!
2
3
4
5
6
7
8
技术上,可以通过在创建后直接分配
event.clientX=100
来解决这个问题。所以,这是一个方便和遵守规则的问题。浏览器生成的事件始终具有正确的类型。
# 自定义事件的事件类型
要全新自定义事件类型,使用 new CustomEvent
。
从技术上讲,CustomEvent (opens new window) 和
Event
一样。除了一点不同。在第二个参数(对象)中,可以为想要与事件一起传递的 任何自定义信息添加一个附加的属性detail
。
事件类描述了它是「什么类型的事件」,如果事件是自定义的,那么应该使用
CustomEvent
来明确它是什么。
🌰 例子:
<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>
2
3
4
5
6
7
8
9
10
11
12
detail
属性可以有任何数据。
- 通常在创建后将任何属性分配给常规的
new Event
对象中。- 但是
CustomEvent
提供了特殊的detail
字段,以避免与其他事件属性的冲突。
# 自定义事件的默认行为
对于新的自定义的事件,绝对没有默认的浏览器行为,但是分派此类事件的代码可能有自己的计划,触发该事件之后应该做什么。
🌰 例子 /
<pre id="rabbit">
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
</pre>
<button onclick="hide()">Hide()</button>
2
3
4
5
6
7
8
<script>
function hide() {
let event = new CustomEvent("hide", {
cancelable: true // 没有这个标志,preventDefault 将不起作用
});
if (!rabbit.dispatchEvent(event)) {
alert('The action was prevented by a handler');
} else {
rabbit.hidden = true;
}
}
rabbit.addEventListener('hide', function(event) {
if (confirm("Call preventDefault?")) {
event.preventDefault();
}
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 事件的同步事件
通常事件是在队列中处理的。也就是说:如果浏览器正在处理 onclick
,这时发生了一个新的事件,例如鼠标移动了,那么它的处理程序会被排入队列,相应的 mousemove
处理程序将在 onclick
事件处理完成后被调用。
例外:当一个事件是在另一个事件中发起的。例如使用 dispatchEvent
。这类事件将会被立即处理,即在新的事件处理程序被调用之后,恢复到当前的事件处理程序。
🌰 例子 / menu-open
事件是在 onclick
事件执行过程中被调用的。它会被立即执行,而不必等待 onclick
处理程序结束:
<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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
嵌套事件
menu-open
会在document
上被捕获。嵌套事件的传播和处理先被完成,然后处理过程才会返回到外部代码(onclick
)。这不只是与
dispatchEvent
有关,还有其他情况。如果一个事件处理程序调用了触发其他事件的方法 —— 它们同样也会被以嵌套的方式同步处理。如果想让
onclick
不受menu-open
或者其它嵌套事件的影响,优先被处理完毕。那么可以将dispatchEvent
(或另一个触发事件的调用)放在onclick
末尾,或者最好将其包装到零延迟的setTimeout
中:<script> menu.onclick = function() { alert(1); setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", { bubbles: true }))); alert(2); }; document.addEventListener('menu-open', () => alert('nested')); </script>
1
2
3
4
5
6
7
8
9
10
11
12
13现在,
dispatchEvent
在当前代码执行完成之后异步运行,包括menu.onclick
,因此,事件处理程序是完全独立的。