浏览器事件详解
浏览器事件模型
DOM事件
历史:
1997 年的 6 月和 10 月:Netscape Navigator 4 和 IE4 分别发布 DHTML
1998 年10 月:W3C 总结了 IE 和 Navigator4 的规范,制定了 DOMLevel 1即 DOM1,之前 IE 与 Netscape 的规范则被称为 DOMLevel 0 即 DOM0
W3C 后来将 DOM1 升级为 DOM2,DOM2级规范开始尝试以一种符合逻辑的方式来标准化 DOM事件
DOM0级事件
事件就是用户或浏览器自身执行的某种操作,如click、load、mouseover等,都是事件的名字,而响应某个事件的函数就被称为事件处理程序。
btn.onclick = function(){
console.log('this is a click event')
}
当浏览器检测到用户点击该按钮时,执行btn.onclick.call(btn,event),btn指代this(调用者),事件对象 event,该对象也可以通过 arguments[0] 来访问,它包含了事件相关的所有信息。
btn.onclick = function(e){
console.log('this is a click event');
console.log(e); // 事件对象
}
IE中DOM0级事件
但是在 IE 中,在使用 DOM0 级方法添加事件处理程序时,event 是作 window 对象的一个属性而存在的。此时访问事件对象需要通过 window.event
btn.onclick = function(){
console.log(window.event); // IE中事件对象
}
DOM1级事件
1级DOM标准中并没有定义事件相关的内容,所以没有所谓的1级DOM事件模型
DOM2级事件
- DOM0级 可以认为 onclick 是 btn 的一个属性,DOM2级 则将属性升级为队列。
- DOM2级事件定义了两个方法,用于处理指定和删除事件处理程序的操作,addEventListener()和removeEventListener(),所有的 DOM 节点中都包含这两个方法,它们都接收 3 个参数。
- 1、要处理的事件名
- 2、作为事件处理程序的函数;
- 3、布尔值,true 代表在捕获阶段调用事件处理程序,false 表示在冒泡阶段调用事件处理程序,默认为 false;
function fn2(){
// do something else
}
btn.addEventListener('click',function(){//匿名函数。
// do something
})
btn.addEventListener('click',fn2)//具名函数
addEventListener()将事件加入到监听队列中,当浏览器发现用户点击按钮时,click 队列中依次执行匿名函数1、具名函数2。
通过addEventListener()添加的事件只能由removeEventListener()来移除,并且removeEventListener()只能移除具名函数,不能移除匿名函数。
IE中DOM2级事件
IE8 及之前,实现类似addEventListener()和removeEventListener()的两个方法是attachEvent()和detachEvent(),这两个方法接受相同的两个参数。
- 要处理的事件名;
- 作为事件处理程序的函数;
IE8 之前的只支持事件冒泡,所以通过attachEvent()添加的事件处理程序只能添加到冒泡阶段。
btn.attachEvent('click',fn1)
btn.attachEvent('click',fn2)
当浏览器发现用户点击按钮时,click 队列中依次执行fn1,fn2
类似的detachEvent()也只能移除具名函数,不能移除匿名函数。
兼容处理
if(typeof btn.addEventListener === 'function'){
btn.addEventListener('click',fn);
}else if(typeof btn.attachEvent === 'function'){
btn.attachEvent('onclick',fn)
}else{
btn.onclick=function(){
// do something
}
}
事件捕获&事件冒泡
通常,一个事件会从父元素开始向目标元素传播,然后它将被传播回父元素。
DOM事件规定的事件流包括三个阶段:
- 事件捕获阶段;事件从父元素开始向目标元素传播,从 Window 对象开始传播。
- 处于目标阶段;该事件到达目标元素或开始该事件的元素。
- 事件冒泡阶段;这时与捕获阶段相反,事件向父元素传播,直到 Window 对象。
1. 事件捕获
.addEventListener("click", () => {
console.log('div');
},true); //第三个参数true,为事件捕获时触发
最不具体的节点最先收到事件,而最具体的节点最后收到事件。事件捕获实际上是为了在事件到达最终目标前拦截事件。
点击<div>
元素会以下列顺序触发 click 事件:
- document;
- <html>;
- <body>;
- <div>;
2. 事件冒泡
.addEventListener("click", () => {
console.log('div');
},true); //第三个参数false 。表示在冒泡阶段调用事件,默认为 false
在点击页面中的 <div>
元素后,click 事件会以如下顺序发生:
- <div>;
- <body>;
- <html>;
- document;
最先触发 click 事件。然后,click 事件沿 DOM 树一路向上,在经过的每个节点上依次触发,直至到达 document 对象。
事件捕获和事件冒泡属于两个相反的过程
实例:
<div id="testDiv">
testDiv
<ul id="testUl">
testUl
<li id="testIi">
testIi
</li>
</ul>
</div>
var testDiv=document.getElementById('testDiv');
var testUl=document.getElementById('testUl');
var testIi=document.getElementById('testIi');
window.addEventListener("click", () => {
console.log('Window click');
},true);//事件捕获时执行
document.addEventListener("click", () => {
console.log('Document click');
}); // 事件冒泡时执行
testDiv.addEventListener("click", () => {
console.log('DIV click');
}); // 事件冒泡时执行
testUl.addEventListener("click", () => {
console.log('UL click');
},true);//事件捕获时执行
testIi.addEventListener("click", () => {
console.log('LI click');
},true);//事件捕获时执行
点击testIi时,先进行事件捕获,从window开始到testIi节点本身,依次顺序是window、document、testDiv、testUl、testIi,其中window、testUl注册了事件捕获,所以打印“Window click”、“UL click”。然后执行目标阶段“LI click”。最后进行事件冒泡依次顺序是testIi、testUl、testDiv、document、window,其中testDiv、document、注册了事件冒泡,所以执行“DIV click”、”Document click“
可以同时注册事件捕获和事件冒泡吗?
答案是可以
var testDiv=document.getElementById('testDiv');
var testUl=document.getElementById('testUl');
var testIi=document.getElementById('testIi');
window.addEventListener("click", () => {
console.log('Window click');
},true);//事件捕获时执行
document.addEventListener("click", () => {
console.log('Document click');
}); // 事件冒泡时执行
testDiv.addEventListener("click", () => {
console.log('DIV click');
}); // 事件冒泡时执行
testUl.addEventListener("click", () => {
console.log('捕获UL click');
},true);//事件捕获时执行
testUl.addEventListener("click", () => {
console.log('冒泡UL click');
},false);//事件捕获时执行
testIi.addEventListener("click", () => {
console.log('LI click');
},true);//事件捕获时执行
点击testIi的执行结果
点击testUl执行结果
事件对象
event对象
DOM0和DOM2的事件处理程序都会自动传入event对象
IE中会有window.event、event两种情况,(只有在事件处理程序执行期间,event对象才会存在;一旦事件处理程序执行完成,event对象就会被销毁)
event对象里需要关心的属性:
- target:target永远是被触发事件的那个元素;
- currentTarget:被绑定事件的元素
- eventPhase:调用事件处理程序的阶段,有三个值
a. 捕获阶段;
b. 处于目标;
c. 冒泡阶段;
所以获取event对象兼容写法
// 获取event对象
getEvent: (event) => {
return event ? event : window.event
},
event.currentTarget
和event.target
通常情况下相同,不过如果当前事件是在冒泡或者捕获阶段被调用,则两者的值不同,target
的值为触发事件的DOM,currentTarget
的值为绑定事件的DOM,我们可以借助target属性实现事件委托,又称事件代理(下面介绍)。
父级套多个子级,父级上绑定事件,点击子级,通过冒泡触发父级的绑定事件,此时event.currentTarget就能拿到父级事件的节点,event.target拿到对应触发的子级
preventDefault和stopPropagation
stopPropagation
:阻止事件传播。但是默认事件任然会执行,当你调用这个方法的时候,如果点击一个连接,这个连接仍然会被打开。
preventDefault
:取消事件的默认动作,但是冒泡仍然会发生。
(JQ里 return false等效于同时调用e.preventDefault()和e.stopPropagation())
stopPropagation:遇到stopPropagation停止捕获/目标/冒泡
如果没有stopPropagation使用以上面例子来说执行顺序是:window捕获->document捕获->testDiv捕获->testUl捕获->testIi捕获->testIi冒泡->testUl冒泡->testDiv冒泡->document冒泡->window冒泡
var testDiv=document.getElementById('testDiv');
var testUl=document.getElementById('testUl');
var testIi=document.getElementById('testIi');
window.addEventListener("click", () => {
console.log('捕获Window click');
},true);//事件捕获时执行
document.addEventListener("click", () => {
console.log('冒泡Document click');
}); // 事件冒泡时执行
testDiv.addEventListener("click", () => {
console.log('冒泡DIV click');
}); // 事件冒泡时执行
testUl.addEventListener("click", () => {
console.log('冒泡UL click');
},false);//事件捕获时执行
testUl.addEventListener("click", (event) => {
console.log('捕获UL click');
event.stopPropagation() //依次打印 捕获Window click、捕获捕获UL click
},true);//事件捕获时执行
testIi.addEventListener("click", (event) => {
console.log('捕获LI click');
},true);//事件捕获时执行
testIi.addEventListener("click", (event) => {
console.log('冒泡LI click');
},false);//事件冒泡时执行
在 testUl捕获事件上使用 event.stopPropagation(),事件执行到testUl捕获,不会往下执行testIi捕获->testIi冒泡->testUl冒泡->testDiv冒泡->document冒泡->window冒泡,阻止事件向后传播
在 testIi冒泡事件上使用 event.stopPropagation(),事件执行到testIi冒泡,不会往下执行estUl冒泡->testDiv冒泡->document冒泡->window冒泡,阻止事件向后传播
testIi.addEventListener("click", (event) => {
console.log('冒泡LI click');
event.stopPropagation() //依次打印 捕获Window click、捕获捕获UL click 捕获LI click 冒泡LI click
},false);//事件冒泡时执行
preventDefault:取消事件的默认动作,但是冒泡仍然会发生。
点击Checkbox时,复选框会默认选中
加上event.preventDefault(),点击不会默认选中,但是冒泡仍然会发生(捕获也是会发生的)
<div id="testDiv">
testDiv
<ul id="testUl">
testUl
<li id="testIi">
testIi
</li>
<li id="testIi2">
<form>
<label for="id-checkbox">Checkbox:</label>
<input type="checkbox" id="id-checkbox" />
</form>
</li>
</ul>
</div>
var testDiv=document.getElementById('testDiv');
var testUl=document.getElementById('testUl');
var testIi=document.getElementById('testIi');
var testIi2=document.getElementById('testIi2');
window.addEventListener("click", () => {
console.log('捕获Window click');
},true);//事件捕获时执行
document.addEventListener("click", () => {
console.log('冒泡Document click');
}); // 事件冒泡时执行
testDiv.addEventListener("click", () => {
console.log('冒泡DIV click');
}); // 事件冒泡时执行
testUl.addEventListener("click", () => {
console.log('冒泡UL click');
},false);//事件捕获时执行
testUl.addEventListener("click", () => {
console.log('捕获UL click');
},true);//事件捕获时执行
testIi.addEventListener("click", (event) => {
console.log('捕获LI click');
},true);//事件捕获时执行
testIi.addEventListener("click", (event) => {
console.log('冒泡LI click');
},false);//事件冒泡时执行
testIi2.addEventListener("click", (event) => {
console.log('Checkbox click');
// event.stopPropagation()
event.preventDefault()
},true);//事件捕获时执行
点击checkbox后
IE8及以下不支持preventDefault和stopPropagation()
IE中对应的属性:
- srcElement => target
- returnValue => preventDefaukt()
- cancelBubble => stopPropagation()
所以兼容写法
// 获取当前目标
getTarget: (event) => {
return event.target ? event.target : event.srcElement
},
// 阻止默认行为
preventDefault: (event) => {
if (event.preventDefault) {
event.preventDefault()
} else {
event.returnValue = false
}
},
// 停止传播事件
stopPropagation: (event) => {
if (event,stopPropagation) {
event.stopPropagation()
} else {
event.cancelBubble = true
}
}
事件委托
事件委托:用来解决事件处理程序过多的问题,找到共同父级,在父级绑定事件,子级触发事件,父级事件里进行判断,走不同的分支
页面结构如下
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
按照传统的做法,需要像下面这样为它们添加 3 个事 件处理程序。
var item1 = document.getElementById("goSomewhere");
var item2 = document.getElementById("doSomething");
var item3 = document.getElementById("sayHi");
EventUtil.addHandler(item1, "click", function(event){
location.href = "http://www.xianzao.com";
});
EventUtil.addHandler(item2, "click", function(event){
document.title = "I changed the document's title";
});
EventUtil.addHandler(item3, "click", function(event){
alert("hi");
});
如果在一个复杂的 Web 应用程序中,对所有可单击的元素都采用这种方式,那么结果就会有数不 清的代码用于添加事件处理程序。此时,可以利用事件委托技术解决这个问题。使用事件委托,只需在 DOM 树中尽量最高的层次上添加一个事件处理程序,如下面的例子所示
var list = document.getElementById("myLinks");
EventUtil.addHandler(list, "click", function(event) {
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
switch(target.id) {
case "doSomething":
document.title = "I changed the document's title";
break;
case "goSomewhere":
location.href = "http://www.wrox.com";
break;
case "sayHi": 9 alert("hi");
break;
}
}
子节点的点击事件会冒泡到父节点,并被这个注册事件处理
最适合采用事件委托技术的事件包括 click、mousedown、mouseup、keydown、keyup 和 keypress。 虽然
mouseover 和 mouseout 事件也冒泡,但要适当处理它们并不容易,而且经常需要计算元素的位置。