浏览器事件简介
事件 是某事发生的信号。所有的 DOM 节点都生成这样的信号(但事件不仅限于 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 特性
处理程序可以设置在 HTML 中名为 on<event>
的特性(attribute)中。
例如,要为一个 input
分配一个 click
处理程序,我们可以使用 onclick
<input value="Click me" onclick="alert('Click!')" type="button">
在鼠标点击时,onclick
中的代码就会运行。
请注意,在 onclick
中,我们使用单引号,因为特性本身使用的是双引号。如果我们忘记了代码是在特性中的,而使用了双引号,像这样:onclick="alert("Click!")"
,那么它就无法正确运行。
HTML 特性不是编写大量代码的好位置,因此我们最好创建一个 JavaScript 函数,然后在 HTML 特性中调用这个函数。
在这里点击会运行 countRabbits()
:
<script>
function countRabbits() {
for(let i=1; i<=3; i++) {
alert("Rabbit number " + i);
}
}
</script>
<input type="button" onclick="countRabbits()" value="Count rabbits!">
我们知道,HTML 特性名是大小写不敏感的,所以 ONCLICK
和 onClick
以及 onCLICK
都一样可以运行。但是特性通常是小写的:onclick
。
DOM 属性
我们可以使用 DOM 属性(property)on<event>
来分配处理程序。
例如 elem.onclick
如果一个处理程序是通过 HTML 特性(attribute)分配的,那么随后浏览器读取它,并从特性的内容创建一个新函数,并将这个函数写入 DOM 属性(property)。
因此,这种方法实际上与前一种方法相同。
因为这里只有一个 onclick
属性,所以我们无法分配更多事件处理程序。
要移除一个处理程序 —— 赋值 elem.onclick = null
。
访问元素 this
处理程序中的 this
的值是对应的元素。就是处理程序所在的那个元素。
可能出现的错误
如果你刚开始写事件 —— 请注意一些细微之处。
我们可以将一个现存的函数用作处理程序
但要注意:函数应该是以 sayThanks
的形式进行赋值,而不是 sayThanks()
。
如果我们添加了括号,那么 sayThanks()
就变成了一个函数调用。所以,最后一行代码实际上获得的是函数执行的 结果,即 undefined
(因为这个函数没有返回值)。此代码不会工作。
……但在标记(markup)中,我们确实需要括号
<input type="button" id="button" onclick="sayThanks()">
这个区别很容易解释。当浏览器读取 HTML 特性(attribute)时,浏览器将会使用 特性中的内容 创建一个处理程序。
所以,标记(markup)会生成下面这个属性:
button.onclick = function() {
sayThanks(); // <-- 特性(attribute)中的内容变到了这里
};
不要对处理程序使用 setAttribute
。
这样的调用会失效:
// 点击 <body> 将产生 error,
// 因为特性总是字符串的,函数变成了一个字符串
document.body.setAttribute('onclick', function() { alert(1) });
DOM 属性是大小写敏感的。
将处理程序分配给 elem.onclick
,而不是 elem.ONCLICK
,因为 DOM 属性是大小写敏感的。
addEventListener
上述分配处理程序的方式的根本问题是 —— 我们不能为一个事件分配多个处理程序。
假设,在我们点击了一个按钮时,我们代码中的一部分想要高亮显示这个按钮,另一部分则想要显示一条消息。
我们想为此事件分配两个处理程序。但是,新的 DOM 属性将覆盖现有的 DOM 属性
Web 标准的开发者很早就了解到了这一点,并提出了一种使用特殊方法 addEventListener
和 removeEventListener
来管理处理程序的替代方法。它们没有这样的问题。
添加处理程序的语法:
element.addEventListener(event, handler[, options]);
event
事件名,例如:"click"
。
handler
处理程序。
options
具有以下属性的附加可选对象:
once
:如果为true
,那么会在被触发后自动删除监听器。capture
:事件处理的阶段,我们稍后将在 冒泡和捕获 一章中介绍。由于历史原因,options
也可以是false/true
,它与{capture: false/true}
相同。passive
:如果为true
,那么处理程序将不会调用preventDefault()
,我们稍后将在 浏览器默认行为 一章中介绍。
要移除处理程序,可以使用removeEventListener
:
element.removeEventListener(event, handler[, options]);
移除需要相同的函数
要移除处理程序,我们需要传入与分配的函数完全相同的函数。
这不起作用:
elem.addEventListener( "click" , () => alert('Thanks!'));
// ....
elem.removeEventListener( "click", () => alert('Thanks!'));
处理程序不会被移除,因为
removeEventListener
获取了另一个函数 —— 使用相同的代码,但这并不起作用,因为它是一个不同的函数对象。
下面是正确方法:
function handler() {
alert( 'Thanks!' );
}
input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);
请注意 —— 如果我们不将函数存储在一个变量中,那么我们就无法移除它。由 addEventListener
分配的处理程序将无法被“读回”。
多次调用 addEventListener
允许添加多个处理程序
正如我们在上面这个例子中所看到的,我们可以 同时 使用 DOM 属性和 addEventListener
来设置处理程序。但通常我们只使用其中一种方式。
对于某些事件,只能通过
addEventListener
设置处理程序
有些事件无法通过 DOM 属性进行分配。只能使用addEventListener
。
例如,DOMContentLoaded
事件,该事件在文档加载完成并且 DOM 构建完成时触发。
// 永远不会运行
document.onDOMContentLoaded = function() {
alert("DOM built");
};
// 这种方式可以运行
document.addEventListener("DOMContentLoaded", function() {
alert("DOM built");
});
所以
addEventListener
更通用。虽然这样的事件是特例而不是规则。
事件对象
为了正确处理事件,我们需要更深入地了解发生了什么。不仅仅是 “click” 或 “keydown”,还包括鼠标指针的坐标是什么?按下了哪个键?等等。
当事件发生时,浏览器会创建一个 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>
event
对象的一些属性:
event.type
事件类型,这里是 "click"
。
event.currentTarget
处理事件的元素。这与 this
相同,除非处理程序是一个箭头函数,或者它的 this
被绑定到了其他东西上,之后我们就可以从 event.currentTarget
获取元素了。
event.clientX / event.clientY
指针事件(pointer event)的指针的窗口相对坐标。
还有很多属性。其中很多都取决于事件类型:键盘事件具有一组属性,指针事件具有另一组属性,稍后我们将详细讨论不同事件,那时我们再对其进行详细研究。
event
对象在 HTML 处理程序中也可用
如果我们在 HTML 中分配了一个处理程序,那么我们也可以使用event
对象,像这样:
<input type="button" onclick="alert(event.type)" value="Event type">
这是可能的,因为当浏览器读取特性(attribute)时,它会创建像这样的处理程序:
function(event) { alert(event.type) }
。也就是说:它的第一个参数是"event"
,而主体取自于该特性(attribute)。
对象处理程序:handleEvent
我们不仅可以分配函数,还可以使用 addEventListener
将一个对象分配为事件处理程序。当事件发生时,就会调用该对象的 handleEvent
方法。
<button id="elem">Click me</button>
<script>
let obj = {
handleEvent(event) {
alert(event.type + " at " + event.currentTarget);
}
};
elem.addEventListener('click', obj);
</script>
正如我们所看到的,当 addEventListener
接收一个对象作为处理程序时,在事件发生时,它就会调用 obj.handleEvent(event)
来处理事件。
我们也可以对此使用一个类:
<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>
这里,同一个对象处理两个事件。请注意,我们需要使用 addEventListener
来显式设置事件,以指明要监听的事件。这里的 menu
对象只监听 mousedown
和 mouseup
,而没有任何其他类型的事件。
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>
冒泡和捕获
点击子元素会执行所有其父元素的处理函数。
冒泡
冒泡(bubbling)原理很简单。
当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。
假设我们有 3 层嵌套 FORM > DIV > P
,它们各自拥有一个处理程序
点击内部的 <p>
会首先运行 onclick
:
- 在该
<p>
上的。 - 然后是外部
<div>
上的。 - 然后是外部
<form>
上的。 - 以此类推,直到最后的
document
对象。
因此,如果我们点击<p>
,那么我们将看到 3 个 alert:p
→div
→form
。
这个过程被称为“冒泡(bubbling)”,因为事件从内部元素“冒泡”到所有父级,就像在水里的气泡一样。
几乎所有事件都会冒泡。
这句话中的关键词是“几乎”。
例如,focus
事件不会冒泡。同样,我们以后还会遇到其他例子。但这仍然是例外,而不是规则,大多数事件的确都是冒泡的。
event.target
父元素上的处理程序始终可以获取事件实际发生位置的详细信息。
引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过 event.target
访问。
注意与 this
(=event.currentTarget
)之间的区别:
event.target
—— 是引发事件的“目标”元素,它在冒泡过程中不会发生变化。this
—— 是“当前”元素,其中有一个当前正在运行的处理程序。
例如,如果我们有一个处理程序form.onclick
,那么它可以“捕获”表单内的所有点击。无论点击发生在哪里,它都会冒泡到<form>
并运行处理程序。
在form.onclick
处理程序中:this
(=event.currentTarget
)是<form>
元素,因为处理程序在它上面运行。event.target
是表单中实际被点击的元素。
event.target
可能会等于this
—— 当点击事件发生在<form>
元素上时,就会发生这种情况。
停止冒泡/捕获
冒泡事件从目标元素开始向上冒泡。通常,它会一直上升到 <html>
,然后再到 document
对象,有些事件甚至会到达 window
,它们会调用路径上所有的处理程序。
但是任意处理程序都可以决定事件已经被完全处理,并停止冒泡。
用于停止冒泡的方法是 event.stopPropagation()
。
event.stopImmediatePropagation()
如果一个元素在一个事件上有多个处理程序,即使其中一个停止冒泡,其他处理程序仍会执行。
换句话说,event.stopPropagation()
停止向上移动,但是当前元素上的其他处理程序都会继续运行。
有一个event.stopImmediatePropagation()
方法,可以用于停止冒泡,并阻止当前元素上的处理程序运行。使用该方法之后,其他处理程序就不会被执行。
不要在没有需要的情况下停止冒泡!
冒泡很方便。不要在没有真实需求时阻止它:除非是显而易见的,并且在架构上经过深思熟虑的。
有时event.stopPropagation()
会产生隐藏的陷阱,以后可能会成为问题。
例如:
- 我们创建了一个嵌套菜单,每个子菜单各自处理对自己的元素的点击事件,并调用
stopPropagation
,以便不会触发外部菜单。- 之后,我们决定捕获在整个窗口上的点击,以追踪用户的行为(用户点击的位置)。有些分析系统会这样做。通常,代码会使用
document.addEventListener('click'…)
来捕获所有的点击。- 我们的分析不适用于被
stopPropagation
所阻止点击的区域。太伤心了,我们有一个“死区”。
通常,没有真正的必要去阻止冒泡。一项看似需要阻止冒泡的任务,可以通过其他方法解决。其中之一就是使用自定义事件,稍后我们会介绍它们。此外,我们还可以将我们的数据写入一个处理程序中的event
对象,并在另一个处理程序中读取该数据,这样我们就可以向父处理程序传递有关下层处理程序的信息。
捕获
事件处理的另一个阶段被称为“捕获(capturing)”。它很少被用在实际开发中,但有时是有用的。
DOM 事件标准描述了事件传播的 3 个阶段:
- 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
- 目标阶段(Target phase)—— 事件到达目标元素。
- 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。
也就是说:点击<td>
,事件首先通过祖先链向下到达元素(捕获阶段),然后到达目标(目标阶段),最后上升(冒泡阶段),在途中调用处理程序。
之前,我们只讨论了冒泡,因为捕获阶段很少被使用。通常我们看不到它。
使用on<event>
属性或使用 HTML 特性(attribute)或使用两个参数的addEventListener(event, handler)
添加的处理程序,对捕获一无所知,它们仅在第二阶段和第三阶段运行。
为了在捕获阶段捕获事件,我们需要将处理程序的capture
选项设置为true
elem.addEventListener(event, hanlder, {capture: true})
// 或者,用 {capture: true} 的别名 "true"
elem.addEventListener(..., true)
capture
选项有两个可能的值:
- 如果为
false
(默认值),则在冒泡阶段设置处理程序。 - 如果为
true
,则在捕获阶段设置处理程序。
请注意,虽然形式上有 3 个阶段,但第 2 阶段(“目标阶段”:事件到达元素)没有被单独处理:捕获阶段和冒泡阶段的处理程序都在该阶段被触发。
有一个属性event.eventPhase
,它告诉我们捕获事件的阶段数。但它很少被使用,因为我们通常是从处理程序中了解到它。
要移除处理程序,
removeEventListener
需要同一阶段
如果我们addEventListener(..., true)
,那么我们应该在removeEventListener(..., true)
中提到同一阶段,以正确删除处理程序。
同一元素的同一阶段的监听器按其设置顺序运行
如果我们在同一阶段有多个事件处理程序,并通过addEventListener
分配给了相同的元素,则它们的运行顺序与创建顺序相同
事件委托
捕获和冒泡允许我们实现最强大的事件处理模式之一,即 事件委托 模式。
这个想法是,如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上。
在处理程序中,我们获取 event.target
以查看事件实际发生的位置并进行处理。
委托示例,标记中的行为
事件委托还有其他用途。(译注:本节标题中的“标记中的行为”即 action in markup)
例如,我们想要编写一个有“保存”、“加载”和“搜索”等按钮的菜单。并且,这里有一个具有 save
、load
和 search
等方法的对象。如何匹配它们?
第一个想法可能是为每个按钮分配一个单独的处理程序。但是有一个更优雅的解决方案。我们可以为整个菜单添加一个处理程序,并为具有方法调用的按钮添加 data-action
特性(attribute)
<div id="menu">
<button data-action="save">Save</button>
<button data-action="load">Load</button>
<button data-action="search">Search</button>
</div>
<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)。
行为:计数器
这里的特性 data-counter
给按钮添加了一个“点击增加”的行为。
Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>
<script>
document.addEventListener('click', function(event) {
if (event.target.dataset.counter != undefined) { // 如果这个特性存在...
event.target.value++;
}
});
</script>
如果我们点击按钮 —— 它的值就会增加。但不仅仅是按钮,一般的方法在这里也很重要。
我们可以根据需要使用 data-counter
特性,多少都可以。我们可以随时向 HTML 添加新的特性。使用事件委托,我们属于对 HTML 进行了“扩展”,添加了描述新行为的特性。
对于文档级的处理程序 —— 始终使用的是
addEventListener
当我们将事件处理程序分配给document
对象时,我们应该始终使用addEventListener
,而不是document.on<event>
,因为后者会引起冲突:新的处理程序会覆盖旧的处理程序。
对于实际项目来说。在document
上有许多由代码的不同部分设置的处理程序,这是很正常的。
行为:切换器
点击一个具有 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>
让我们再次注意我们做了什么。现在,要向元素添加切换功能 —— 无需了解 JavaScript,只需要使用特性 data-toggle-id
即可。
这可能变得非常方便 —— 无需为每个这样的元素编写 JavaScript。只需要使用行为。文档级处理程序使其适用于页面的任意元素。
我们也可以组合单个元素上的多个行为。
“行为”模式可以替代 JavaScript 的小片段。
浏览器默认行为
许多事件会自动触发浏览器执行某些行为。
例如:
- 点击一个链接 —— 触发导航(navigation)到该 URL。
- 点击表单的提交按钮 —— 触发提交到服务器的行为。
- 在文本上按下鼠标按钮并移动 —— 选中文本。
如果我们使用 JavaScript 处理一个事件,那么我们通常不希望发生相应的浏览器行为。而是想要实现其他行为进行替代。
阻止浏览器行为
有两种方式来告诉浏览器我们不希望它执行默认行为:
- 主流的方式是使用
event
对象。有一个event.preventDefault()
方法。 - 如果处理程序是使用
on<event>
(而不是addEventListener
)分配的,那返回false
也同样有效。
在下面这个示例中,点击链接不会触发导航(navigation),浏览器不会执行任何操作:
<a href="/" onclick="return false">Click here</a>
or
<a href="/" onclick="event.preventDefault()">here</a>
从处理程序返回
false
是一个例外
事件处理程序返回的值通常会被忽略。
唯一的例外是从使用on<event>
分配的处理程序中返回的return false
。
在所有其他情况下,return
值都会被忽略。并且,返回true
没有意义。
示例:菜单
菜单项是通过使用 HTML 链接 <a>
实现的,而不是使用按钮 <button>
。这样做有几个原因,例如:
- 许多人喜欢使用“右键单击” —— “在一个新窗口打开链接”。如果我们使用
<button>
或<span>
,这个效果就无法实现。 - 搜索引擎在建立索引时遵循
<a href="...">
链接。
所以我们在标记(markup)中使用了<a>
。但通常我们打算处理 JavaScript 中的点击。因此,我们应该阻止浏览器默认行为。
menu.onclick = function(event) {
if (event.target.nodeName != 'A') return;
let href = event.target.getAttribute('href');
alert( href ); // ...可以从服务器加载,UI 生成等
return false; // 阻止浏览器行为(不前往访问 URL)
};
如果我们省略 return false
,那么在我们的代码执行完毕后,浏览器将执行它的“默认行为” —— 导航至在 href
中的 URL。
顺便说一句,这里使用事件委托会使我们的菜单更灵活。我们可以添加嵌套列表并使用 CSS 对其进行样式设置来实现 “slide down” 的效果。
后续事件
某些事件会相互转化。如果我们阻止了第一个事件,那就没有第二个事件了。
例如,在<input>
字段上的mousedown
会导致在其中获得焦点,以及focus
事件。如果我们阻止mousedown
事件,在这就没有焦点了。
这是因为浏览器行为在mousedown
上被取消。如果我们用另一种方式进行输入,则仍然可以进行聚焦。例如,可以使用 Tab 键从第一个输入切换到第二个输入。但鼠标点击则不行。
处理程序选择“passive”
addEventListener
的可选项 passive: true
向浏览器发出信号,表明处理程序将不会调用 preventDefault()
。
为什么需要这样做?
移动设备上会发生一些事件,例如 touchmove
(当用户在屏幕上移动手指时),默认情况下会导致滚动,但是可以使用处理程序的 preventDefault()
来阻止滚动。
因此,当浏览器检测到此类事件时,它必须首先处理所有处理程序,然后如果没有任何地方调用 preventDefault
,则页面可以继续滚动。但这可能会导致 UI 中不必要的延迟和“抖动”。
passive: true
选项告诉浏览器,处理程序不会取消滚动。然后浏览器立即滚动页面以提供最大程度的流畅体验,并通过某种方式处理事件。
对于某些浏览器(Firefox,Chrome),默认情况下,touchstart
和 touchmove
事件的 passive
为 true
。
event.defaultPrevented
如果默认行为被阻止,那么 event.defaultPrevented
属性为 true
,否则为 false
。
这儿有一个有趣的用例。
你还记得我们在 冒泡和捕获 一章中讨论过的 event.stopPropagation()
,以及为什么停止冒泡是不好的吗?
有时我们可以使用 event.defaultPrevented
来代替,来通知其他事件处理程序,该事件已经被处理。
一个替代方案是,检查 document
处理程序是否阻止了浏览器的默认行为?如果阻止了,那么该事件已经得到了处理,我们无需再对此事件做出反应。
现在一切都可以正常工作了。如果我们有嵌套的元素,并且每个元素都有自己的上下文菜单,那么这也是可以运行的。只需确保检查每个 contextmenu
处理程序中的 event.defaultPrevented
。
event.stopPropagation() 和 event.preventDefault()
正如我们所看到的,event.stopPropagation()
和event.preventDefault()
(也被认为是return false
)是两个不同的东西。它们之间毫无关联。
嵌套的上下文菜单结构
还有其他实现嵌套上下文菜单的方式。其中之一是拥有一个具有document.oncontextmenu
处理程序的全局对象,以及使我们能够在其中存储其他处理程序的方法。
该对象将捕获任何右键单击,浏览存储的处理程序并运行适当的处理程序。
但是,每段需要上下文菜单的代码都应该了解该对象,并使用它的帮助,而不是使用自己的contextmenu
处理程序。
创建自定义事件
我们不仅可以分配事件处理程序,还可以从 JavaScript 生成事件。
自定义事件可用于创建“图形组件”。例如,我们自己的基于 JavaScript 的菜单的根元素可能会触发 open
(打开菜单),select
(有一项被选中)等事件来告诉菜单发生了什么。另一个代码可能会监听事件,并观察菜单发生了什么。
我们不仅可以生成出于自身目的而创建的全新事件,还可以生成例如 click
和 mousedown
等内建事件。这可能会有助于自动化测试。
事件构造器
内建事件类形成一个层次结构(hierarchy),类似于 DOM 元素类。根是内建的 Event 类。
我们可以像这样创建 Event
对象:
let event = new Event(type[, options]);
参数:
- type —— 事件类型,可以是像这样
"click"
的字符串,或者我们自己的像这样"my-event"
的参数。 - options —— 具有两个可选属性的对象:
bubbles: true/false
—— 如果为true
,那么事件会冒泡。cancelable: true/false
—— 如果为true
,那么“默认行为”就会被阻止。稍后我们会看到对于自定义事件,它意味着什么。
默认情况下,以上两者都为 false:{bubbles: false, cancelable: false}
。
dispatchEvent
事件对象被创建后,我们应该使用 elem.dispatchEvent(event)
调用在元素上“运行”它。
然后,处理程序会对它做出反应,就好像它是一个常规的浏览器事件一样。如果事件是用 bubbles
标志创建的,那么它会冒泡。
在下面这个示例中,click
事件是用 JavaScript 初始化创建的。处理程序工作方式和点击按钮的方式相同:
<button id="elem" onclick="alert('Click!');">Autoclick</button>
<script>
let event = new Event("click");
elem.dispatchEvent(event);
</script>
event.isTrusted
有一种方法可以区分“真实”用户事件和通过脚本生成的事件。
对于来自真实用户操作的事件,event.isTrusted
属性为true
,对于脚本生成的事件,event.isTrusted
属性为false
。
冒泡示例
我们可以创建一个名为 "hello"
的冒泡事件,并在 document
上捕获它。
我们需要做的就是将 bubbles
设置为 true
:
<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>
注意:
- 我们应该对我们的自定义事件使用
addEventListener
,因为on<event>
仅存在于内建事件中,document.onhello
则无法运行。 - 必须设置
bubbles:true
,否则事件不会向上冒泡。
内建事件(click
)和自定义事件(hello
)的冒泡机制相同。自定义事件也有捕获阶段和冒泡阶段。
MouseEvent KeyboardEvent 及其他
这是一个摘自于 UI 事件规范 的一个简短的 UI 事件类列表:
UIEvent
FocusEvent
MouseEvent
WheelEvent
KeyboardEvent
- …
如果我们想要创建这样的事件,我们应该使用它们而不是new Event
。例如,new MouseEvent("click")
。
正确的构造器允许为该类型的事件指定标准属性。
就像鼠标事件的clientX/clientY
一样:
let event = new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: 100,
clientY: 100
});
alert(event.clientX); // 100
请注意:通用的 Event
构造器不允许这样做。
让我们试试:
let event = new Event("click", {
bubbles: true, // 构造器 Event 中只有 bubbles 和 cancelable 可以工作
cancelable: true,
clientX: 100,
clientY: 100
});
alert(event.clientX); // undefined,未知的属性被忽略了!
从技术上讲,我们可以通过在创建后直接分配 event.clientX=100
来解决这个问题。所以,这是一个方便和遵守规则的问题。浏览器生成的事件始终具有正确的类型。
规范中提供了不同 UI 事件的属性的完整列表,例如 MouseEvent。
自定义事件
对于我们自己的全新事件类型,例如 "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
字段,以避免与其他事件属性的冲突。
此外,事件类描述了它是“什么类型的事件”,如果事件是自定义的,那么我们应该使用 CustomEvent
来明确它是什么。
event.preventDefault
许多浏览器事件都有“默认行为”,例如,导航到链接,开始一个选择,等。
对于新的,自定义的事件,绝对没有默认的浏览器行为,但是分派(dispatch)此类事件的代码可能有自己的计划,触发该事件之后应该做什么。
通过调用 event.preventDefault()
,事件处理程序可以发出一个信号,指出这些行为应该被取消。
在这种情况下,elem.dispatchEvent(event)
的调用会返回 false
。那么分派(dispatch)该事件的代码就会知道不应该再继续。
让我们看一个实际的例子 —— 一只隐藏的兔子(可以是关闭菜单或者其他)。
在下面,你可以看到一个在其上分派了 "hide"
事件的 #rabbit
和 hide()
函数,以使所有感兴趣的各方面都知道这只兔子要隐藏起来。
任何处理程序都可以使用 rabbit.addEventListener('hide',...)
来监听该事件,并在需要时使用 event.preventDefault()
来取消该行为。然后兔子就不会藏起来了:
<pre id="rabbit">
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
</pre>
<button onclick="hide()">Hide()</button>
<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>
请注意:该事件必须具有 cancelable: true
标志,否则 event.preventDefault()
调用将会被忽略。
事件中的事件是同步的
通常事件是在队列中处理的。也就是说:如果浏览器正在处理 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>
输出顺序为:1 → nested → 2。
请注意,嵌套事件 menu-open
会在 document
上被捕获。嵌套事件的传播(propagation)和处理先被完成,然后处理过程才会返回到外部代码(onclick
)。
这不只是与 dispatchEvent
有关,还有其他情况。如果一个事件处理程序调用了触发其他事件的方法 —— 它们同样也会被以嵌套的方式同步处理。
不过有时候,这并不是我们期望的结果。我们想让 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。