背景
JavaScript是一套使用事件机制较多的语言,特别是与DOM交互的时候。所以了解并理解事件机制就变得很必要了。
事件绑定的方式
// 第一种:HTML内联属性
<div onclick="fun();">click</div>
// 第二种,DOM属性绑定
document.getElementById("xxx").onclick = function(){
};
// 第三种:事件监听函数
element.addEventListener(<event-name>, <callback>, <use-capture>);
当你同时使用三个的时候,第二个会把第一个给覆盖掉,也就是说第一种和第二种是属于同一个方式,只是写法不同。原因是属性只能有一个,重复赋值会覆盖上一次的。但是addEventListener
不会,这个方法会绑定多个事件程序,依次执行。
事件触发三个阶段
JavaScript 事件触发有三个阶段。
- CAPTURING_PHASE,即捕获阶段
- AT_TARGET,即目标阶段
- BUBBLING_PHASE,即冒泡阶段
事件对象中有一个参数叫eventPhase
,是一个数字,表示这个事件在哪个阶段触发。
eventPhase的定义可以在 DOM specification 里面找到:
// PhaseType
const unsigned short CAPTURING_PHASE = 1;
const unsigned short AT_TARGET = 2;
const unsigned short BUBBLING_PHASE = 3;
事件传递图示

先捕获再冒泡
接着,先试试看帮每一个元素的每一个阶段都添加事件,看一看结果跟想象中的是否一样:
<!DOCTYPE html>
<html>
<body>
<ul id="list">
<li id="list_item">
<span id="list_item_link" target="_blank" href="http://google.com">
click me
</span>
</li>
</ul>
</body>
<script>
const get = (id) => document.getElementById(id);
const $list = get('list');
const $list_item = get('list_item');
const $list_item_link = get('list_item_link');
// list 的捕獲
$list.addEventListener('click', (e) => {
console.log('list capturing', e.eventPhase);
}, true)
// list 的冒泡
$list.addEventListener('click', (e) => {
console.log('list bubbling', e.eventPhase);
}, false)
// list_item 的捕獲
$list_item.addEventListener('click', (e) => {
console.log('list_item capturing', e.eventPhase);
}, true)
// list_item 的冒泡
$list_item.addEventListener('click', (e) => {
console.log('list_item bubbling', e.eventPhase);
}, false)
// list_item_link 的捕獲
$list_item_link.addEventListener('click', (e) => {
console.log('list_item_link capturing', e.eventPhase);
}, true)
// list_item_link 的冒泡
$list_item_link.addEventListener('click', (e) => {
console.log('list_item_link bubbling', e.eventPhase);
}, false)
</script>
</html>
输出结果:
list capturing 1
list_item capturing 1
list_item_link capturing 2
list_item_link bubbling 2
list_item bubbling 3
list bubbling 3
1 是CAPTURING_PHASE,2 是AT_TARGET,3 是BUBBLING_PHASE。
在 target 注册的监听器,不分捕获和冒泡
当事件传递到真正的点击对象,也就是 e.target 的時候,无论你使用addEventListener的第三参数是true还是false,这边的e.eventPhase都会变成AT_TARGET。
const get = (id) => document.getElementById(id);
const $list = get('list');
const $list_item = get('list_item');
const $list_item_link = get('list_item_link');
// list 的冒泡
$list.addEventListener('click', (e) => {
console.log('list bubbling', e.eventPhase);
}, false)
// list 的捕獲
$list.addEventListener('click', (e) => {
console.log('list capturing', e.eventPhase);
}, true)
// list_item 的冒泡
$list_item.addEventListener('click', (e) => {
console.log('list_item bubbling', e.eventPhase);
}, false)
// list_item 的捕獲
$list_item.addEventListener('click', (e) => {
console.log('list_item capturing', e.eventPhase);
}, true)
// list_item_link 的冒泡
$list_item_link.addEventListener('click', (e) => {
console.log('list_item_link bubbling', e.eventPhase);
}, false)
// list_item_link 的捕獲
$list_item_link.addEventListener('click', (e) => {
console.log('list_item_link capturing', e.eventPhase);
}, true)
list capturing 1
list_item capturing 1
list_item_link bubbling 2
list_item_link capturing 2
list_item bubbling 3
list bubbling 3
关于這些事件的传递顺序,只要记住两个原則就好:
- 先捕获,再冒泡
- 当事件传到 target 本身,沒有分捕获跟冒泡,先添加的先执行,后添加的后执行
target 和 currentTarget
在了解上述的事件传递的三个阶段后,我们来梳理事件对象中容易混淆的两个属性:target 和 currentTarget 。
- target 是触发事件的某个具体的对象,只会出现在事件机制的目标阶段,即“谁触发了事件,谁就是 target ”。
- currentTarget 是绑定事件的对象。
取消事件传递
我们可以通过 e.stopPropagation 中断事件的向下或向上传递。
// list 的捕获
$list.addEventListener('click', (e) => {
console.log('list capturing');
e.stopPropagation();
}, true);
list capturing
可见,事件传播被中断了,剩下的 listener 不能接收到事件。 不过,需要注意:stopPropagation 不能阻止同一节点的其他 listener 的执行 。 比如说:
// list 的捕获
$list.addEventListener('click', (e) => {
console.log('list capturing');
e.stopPropagation();
}, true);
// list 的捕获2
$list.addEventListener('click', (e) => {
console.log('list capturing 2');
}, true);
则输出结果为:
list capturing
list capturing 2
复制代码若想让同一节点的其他 listener 不被执行,我们可以使用 e.stopImmediatePropagation
方法。
取消预设行为
我们可以使用 e.preventDefault 取消默认行为。
<a id="list_item_link" href="#">click me</a>
// list_item_link 的冒泡
$list_item_link.addEventListener('click', (e) => {
e.preventDefault();
}, false);
这样,当我们点击超链接时,就不会执行原本的默认行为(新开分页或跳转)。很多人会将 e.stioPropagation 和 e.preventDefault 混淆,事实上,e.preventDefault 与事件传递没有任何关系,并不会影响事件的向下或向上传播。
这里有个特别值得注意的地方,来自 W3C 。
Once preventDefault has been called it will remain in effect throughout the remainder of the event’s propagation.
上面这句话的意思是,只要调用了 preventDefault 方法,在之后传递新的事件里面也会有效果。
// list 的冒泡
$list.addEventListener('click', (e) => {
console.log('list bubbling', e.eventPhase);
e.preventDefault();
}, true);
结果是超链接的默认行为没有被执行,注意到:不管是在捕获阶段还是在冒泡阶段,只要使用了 preventDefault 方法,即可取消默认行为的执行 。
事件代理
当我们想在 ul 节点添加 1000 个 li ,若在每个 li 添加 eventListener ,则新建了 1000 个 function 。但通过事件传播机制,我们可以在 ul 注册 eventListener 。 这样的好处有亮点:
- 节省内存
- 不需要给子节点注销事件
事件的执行顺序
- a标签的href中的代码总是最后执行,最低的优先级。
- 无论是 onclick 还是 addEventListener 的执行顺序是按照绑定的顺序在执行,就是先绑定的就先执行。
- 如果 onclick 事件被重复绑定,则以最后一次的绑定所在的顺序为准。
- 如果在DOM中直接使用onclick ,并且没有覆盖,则onclick的绑定是早于 addEventListener 的。
- 如果绑定多个 addEventListener 事件,在任意一个事件中 stopPropagation(); 都会阻止事件的冒泡,但不会阻止后续事件的执行。
事件列表
可以通过MDN查询,也可以在浏览器中输入:
for (i in window) {
if ( /^on/.test(i)) { console.log(i); }
}
查看,你会发现提供的事件超过你想象的多!
常用事件
- load:资源加载完成时触发。这个资源可以是图片、CSS 文件、JS 文件、视频、document 和 window 等等。
- DOMContentLoaded DOM构建完毕的时候触发, jQuery的ready方法包裹的就是这个事件。
- beforeunload:当浏览者在页面上的输入框输入一些内容时,未保存、误操作关掉网页可能会导致输入信息丢失。当浏览者输入信息但未保存时关掉网页,我们就可以开始监听这个事件,这时候试图关闭网页的时候,会弹窗阻止操作,点击确认之后才会关闭。
- resize:当节点尺寸发生变化时,触发这个事件。通常用在 window 上,这样可以监听浏览器窗口的变化。
- error:当我们加载资源失败或者加载成功但是只加载一部分而无法使用时,就会触发 error 事件,我们可以通过监听该事件来提示一个友好的报错或者进行其他处理。比如 JS 资源加载失败,则提示尝试刷新;图片资源加载失败,在图片下面提示图片加载失败等。该事件不会冒泡。因为子节点加载失败,并不意味着父节点加载失败,所以你的处理函数必须精确绑定到目标节点。
详解一个事件对象
一个 MouseEvent 对象的部分字段:
- "screenX"、"screenY"、"clientX"、"clientY",设置鼠标事件时的坐标位置
- "ctrlKey",Boolean 型可选,默认为false,标明是否同时按下 ctrl 键。
- "shiftKey",Boolean 型可选,默认为false,标明是否同时按下 shift 键。
- "altKey",Boolean 型可选,默认为 false,标明是否同时按下 alt 键。
- "metaKey",Boolean 型可选,默认为false,标明是否同时按下 meta 键。
有个问题,MouseEvent.prototype 没有stopPropagation和preventDefault方法。
其实,topPropagation 和 preventDefault 是Event的方法
使用MouseEvent.__proto__
可以看到MouseEvent派生自UIEvent。
同样的方法可以看到。UIEvent派生自Event。一些具体的事件都派生自 MouseEvent:WheelEvent 和 DragEvent
Vue中的事件处理
监听事件
vue可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。
<div id="example-1">
<button v-on:click="doSomething">do something</button>
</div>
事件修饰符
在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。
- .stop
- .prevent
- .capture
- .self
- .once
- .passive
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即元素自身触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self
会阻止所有的点击,而 v-on:click.self.prevent
只会阻止对元素自身的点击。
延伸
addEventListener 和 removeEventListener
target.addEventListener(type, listener[, useCapture]);
- true - 事件句柄在捕获阶段执行
- false- 默认。事件句柄在冒泡阶段执行
target.removeEventListener(event, function[, useCapture])
- function 必须。指定要移除的函数。
Vue中的 $on
和 $off
-
vm.$emit(eventName,callback)
和$on(eventName,callback)
一般结合使用。使用on 监听该事件并调用回调函数。这两个事件方法可以结合props 属性实现父子组件双向传参。
-
vm.$once(eventName,callback)
监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。 -
vm.$off([eventName,callback])
用来移除自定义事件监听器。如果没有提供参数,则移除所有的事件监听器;如果只提供了事件,则移除该事件所有的监听器;如果同时提供了事件与回调,则只移除这个回调的监听器。