浏览器专题之事件机制
Opened this issue · 0 comments
事件流
在早期 IE
和 Netscape
团队在开发第四代浏览器的时候,遇到一个问题:当点击一个按钮的时候,是应该先处理父级的事件呢?还是应该先处理按钮的事件呢?IE
和 Netscape
给出了 2 种完全相反的答案,IE
提出事件冒泡的概念,而 Netscape
则支持事件捕获。
事件冒泡
事件冒泡认为事件应该由最具体的元素开始触发,然后层层往父级传播:
事件捕获
而事件捕获则相反,认为最外层的元素应该最先收到事件,然后层层往下级传递:
DOM
事件流
为了在浏览器中兼容这 2 种事件流,在 DOM2 Events
规范中将事件流分为 3 个阶段:事件捕获阶段、到底目标阶段、事件冒泡阶段。
可以通过指定 addEventListener
的第三个参数为 true
来设置事件是在捕获阶段调用事件处理程序,默认是 false
指在冒泡阶段调用事件处理程序。
所有现代浏览器都支持
DOM
事件流,只有 IE8 及更早版本不支持。
事件处理程序
HTML
事件处理程序
就是将事件处理程序直接绑定到 HTML
的属性中:
// 方式一
<div onclick="console.log('hello world')"></div>
方式二
<div onclick="print(event)"></div>
<script>
function print(e) { }
</script>
HTML
事件处理程序修改事件相对麻烦,可能需要同时修改 HTML
和 JS
,所以大家都不爱使用这种方式绑定事件。
DOM0
事件处理程序
将一个函数赋值给 DOM
元素的一个事件处理程序属性,比如 onclick
:
let btn = document.getElementById('div')
// 添加事件
btn.onclick = function() { }
// 移除事件
btn.onclick = null
DOM2
事件处理程序
通过 addEventListener
可以添加 DOM2
级别的事件处理程序,它接收 3 个参数:事件名、事件处理程序和 useCapture
(它是一个可选参数,是个布尔值,默认为 false
表示在冒泡阶段调用事件处理程序)
let btn = document.getElementById('div')
btn.addEventListener('click', () => {
}, false)
和 DOM0
事件处理程序的区别:
addEventListener
可以改变事件流,即可以在捕获阶段触发事件,而DOM0
是不行的;addEventListener
可以为同一个元素多次添加同一类型的事件处理程序,先添加的事件处理程序会先触发,而DOM0
如果给同一个元素绑定多个相同类型的事件处理程序的话,则后面添加的会覆盖前面定义的;
它有几个注意事项:
- 如果不需要在捕获阶段进行拦截操作,则
useCapture
即第三个参可以不传; - 通过
addEventListener
添加的事件处理程序只能通过removeEventListener
移除,而且绑定的事件处理程序必须是同一个。
let btn = document.getElementById('div')
let handler = function() { }
btn.addEventListener("click", handler)
btn.removeEventListener("click", handler)
IE 事件处理函数
由于 addEventListener
无法兼容 IE8 及更早版本,所以此时就可以使用 attachEvent
添加事件处理程序和用 detachEvent
移除事件处理程序。
let btn = document.getElementById('div')
btn.attachEvent("onclick", function() { })
它有这么几个注意事项:
- 注册的事件名和
DOM0
一样,需要带上on
,比如onclick
; - 在通过
attachEvent
添加的事件处理程序内部this
会指向window
,而DOM0
和DOM2
的this
会指向元素本身; - 和
addEventListener
一样,attachEvent
也可以针对同一元素多次添加同一个事件类型的处理程序,但是触发顺序是后定义的先触发; - 通过
detachEvent
移除事件处理程序的时候,处理函数必须是和注册的同一个,这点和addEventListener
保持一致;
attachEvent
和 detachEvent
是 IE
专属的 API
,所以如果有兼容性要求,我们可以写出跨浏览器的事件处理程序:
var EventUtil = {
addHandler: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false)
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler)
} else {
element["on" + type] = handler;
}
},
removeHandler: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false)
} else if (element.detachEvent) {
element.detachEvent("on" + type, handler)
} else {
element["on" + type] = null
}
}
}
事件对象
通过不同的事件处理程序添加的事件,event
对象的属性略有不同,我们不需要记住他们的差异,只需要在平时写代码的时候养成一个写兼容代码的习惯即可,如下是一个兼容各种 event
对象的事件处理程序:
let handler = function(event) {
// 事件对象
let event = event || window.event
// 目标元素
let target = event.target || event.srcElement
// 阻止默认事件触发
if (event.preventDefault) {
event.preventDefault()
} else {
event.returnValue = false
}
// 阻止事件冒泡
if (event.stopPropagation) {
event.stopPropagation()
} else {
event.cancelBubble = true
}
}
事件类型
DOM3 Events
定义了如下事件类型:
- 用户界面事件(
UIEvent
):涉及与BOM
交互的通用浏览器事件,比如onload
、resize
、scroll
、input
、select
等; - 焦点事件(
FocusEvent
):在元素获得和失去焦点时触发,比如focus
、blur
; - 鼠标事件(
MouseEvent
):使用鼠标在页面上执行某些操作时触发,比如click
、mousedown
、mouseover
等; - 滚轮事件(
WheelEvent
):使用鼠标滚轮(或类似设备)时触发,比如mousewheel
; - 输入事件(
InputEvent
):向文档中输入文本时触发,比如textInput
; - 键盘事件(
KeyboardEvent
):使用键盘在页面上执行某些操作时触发,比如keydown
、keypress
; - 合成事件(
CompositionEvent
):在使用某种IME
(Input Method Editor
,输入法编辑器)输入字符时触发,比如compositionstart
。
事件委托
事件委托是指将多个元素上绑定的事件通过利用事件冒泡的原理从而转移到他们共同的父级上去绑定,从而在一定程度上起到优化的作用,有的人也喜欢叫它事件代理。比如在 Vue
中经常会将事件绑定到每个列表项中:
<ul>
<li v-for="item in list" :key="item" @click="handleClick(item)">{{item}}</li>
</ul>
其实更好的做法是利用事件委托,将事件绑定到 ul
上:
<ul @click="handleClick">
<li v-for="item in list" :key="item" :data-item="item">{{item}}</li>
</ul>
handleClick(event) {
let target = event.target
if (target === 'li') {
let data = target.dataset.item
}
}