关于这一篇章有太多对于我来说杂且乱的知识点,单单是分别DOM层级划分我看过的文章就有(0,2,3)的,(0,2)的,由于自己知识掌握还很薄弱所以只能参考别人文章结合自己理解来写,这其中也涉及到一点W3C标准制定史的发展,不了解的像我这样的小白肯定会一头雾水啦。
主要的有,DOM层级事件处理、事件流、熟练一些事件处理方法等
简介
JavaScript中,最重要的就是对事件进行处理。Web应用也是通过事件驱动程序设计其功能的。在事件驱动程序设计中,需要注册不同事件的处理方式。
在注册了事件的处理方式后,浏览器就会在该事件发生时执行所注册的处理方式。所注册的处理方式被称作事件处理程序、事件句柄或事件监听器。
JavaScript程序设计的基本内容之一就是获取需要对事件进行捕捉的元素,并针对该元素注入相应的事件处理程序。
事件处理程序/事件侦听器的设定(DOM 0级/DOM 2级
)
首先讲DOM 0级事件处理程序
对事件的方式被称为事件处理程序或事件侦听器,但这两者之间是有区别的。
设定方法不同
-
支持处理元素数量不同
详细点,说一些对事件处理进行设定的方式
指定为HTML元素的属性(DOM 0级)
指定为DOM 元素的属性(DOM 0级)
通过
EventTarget.addEventListener
进行设定(DOM 2级)
1. 首先是第一个,指定为HTML元素的属性
<input type="button" id="input" value="Click Me!" onclick="alert('bar');alert('baz')"/>
这个例子中,通过字符串对onclick事件处理程序将要执行的JavaScript代码进行了设定。如果包含代码,可以分号分隔,当然,事件另外定义一个函数之后再执行该函数的方式也不会有问题。
优点:
设定步骤简单,确保事件处理程序会在载入时被设定。如果使用第二种方式(DOM元素属性),元素被载入时,其事件处理程序可能还没有注册,这时用户执行任何本应触发事件的操作,也没有效果。
注意:这里的onclick
全都是以小写字母书写。HTML不会区分大小写字母,所以改写为onClick
也没有问题。但是XHTML会区分大小写字母,所以最好还是使用全部小写的onclick
,提高兼容性。
小应用:如果事件处理程序返回一个false
值,则会取消该事件的默认行为,(啥是默认行为,比如点击a标签会跳转链接,表单的提交等这些就属于默认行为),例如:当onsubmit
事件处理程序返回一个false
,表单内容不会被发送,,这可以发现内容有误时返回false
取消表单数据发送。或者像下面代码,取消页面跳转。
<a href="http://www.baidu.com" onclick="return stop();">BAIDU.com</a>
<script type="text/javascript">
function stop(e) {
alert("Stop page transfer");
return false;
};
这里放一个W3C链接,有关HTML的事件属性:HTML 事件属性
2. 第二种,指定为DOM元素的属性
如果一个页面分别使用了HTML文件和JavaScript文件(常说的关注点分离),则应该尽可能少地在HTML文件中使用JavaScript代码,以提高可维护性。因此,最好将事件处理程序的设定全都写在JavaScript内。
<input type="button" id="btn" value="Click Me!" />
<script type="text/javascript">
var btn = document.getElementById("btn");
function btnFn() {
alert("btnFn");
}
btn.onclick = btnFn;
需要注意的是,这里被指定为事件处理程序的正是一个函数。
btn.onclick = btnFn(); // 这种方式指定的是函数的返回值,错误
btn.onclick = "btnFn()"; // 以字符串形式指定该函数也是无效的
btn.onclick = btnFn; // 正常执行
第一种指定返回值,相当于是在HTML节点属性上设置alert("btnFn")
,所以run就会直接弹出窗口。
注意:与通过HTML属性设定不同,这里必须全部使用小写字母(毕竟是JavaScript代码),设定属性之后,HTML标签属性中的内容将被覆写。因此,如果希望通过JavaScript代码在HTML标签属性所指定的内容之后追加新的处理操作,仅采用这种指定DOM元素的方法时很难实现的。但是在DOM2级中可以解决这一问题,后续再说。
第一,第二种方式能够对事件注册各种各样的处理,但有一个共同缺点,对于某一元素的某一事件,只能够执行一种处理操作。
<input type="button" id="btn" value="Click Me!" />
<script type="text/javascript">
document.getElementById("btn").onclick = function () {
alert("0");
};
document.getElementById("btn").onclick = function () {
alert("1");
}
// "1"
总结缺点:
标记数量大、可读性、维护性差、不符合关注点分离思想,不能处理复杂的行为、事件等
3. 第三种,通过EventTarget.addEventListener()
进行指定(DOM2级)
打个脸:此方法不能在IE8
之前的浏览器版本中使用,为此可以使用attachEvent()
方法。
<input type="button" id="btn" value="Click Me!" />
<script type="text/javascript">
var btn = document.getElementById("btn");
btn.addEventListener("click",function(){
alert("click me");
},false);
语法:element.addEventListener(event, function, useCapture)
event
:必须。字符串,指定事件名function
:必须。指定要事件触发时执行的函数。useCapture
:可选。布尔值,指定事件是否在捕获或冒泡阶段执行
在DOM2中,useCapture
这一参数是必须的。而在DOM3中,如果省略了该参数,则会默认从事件冒泡阶段开始执行。而在DOM3中省略该参数,则会默认从事件冒泡阶段开始执行。之前说的HTML属性、DOM元素属性设定事件的方式都会在冒泡阶段开始执行处理程序。如果希望在捕获阶段执行事件处理程序的话,只能使用EventTarget.addEventListener()
方法了。在IE使用的attachEvent()
方法中,是没有与之对应的参数,所以IE中时间侦听器总是在冒泡阶段被执行。
var btn = document.getElementById("btn");
function handler() {
alert("btnFn");
};
btn.addEventListener("click",handler,false);
btn.addEventListener("click",handler,false); // 对相同的事件侦听器进行注册
btn.addEventListener("click",handler,true); // 由于执行阶段不同,则将会被作为另一个事件侦听器注册
一般来说执行顺序与注册顺序相同,绝大部分浏览器也都是以注册的顺序对事件侦听器执行的。但是对于和执行顺序有关的处理,还是应该放在同一个事件侦听器中执行,而不应该将它们置于多个不同的事件侦听器之中。
此外,不能对事件目标、事件类型、执行阶段都相同的对象注册多个相同的事件侦听器。之后的注册将被忽略掉,这种情况下,事件侦听器注册顺序不会变化,执行顺序也不会改变。
var btn = document.getElementById("btn");
function btnfn1() {
alert("btnFn1");
};
function btnfn2() {
alert("btnFn2");
};
function btnfn3() {
alert("btnFn3");
};
btn.addEventListener("click",btnfn1,false);
btn.addEventListener("click",btnfn2,false);
btn.addEventListener("click",btnfn3,false);
btn.addEventListener("click",btnfn1,false); // 这次注册将被忽略
// btnfn1——btnfn2——btnfn3
发现:如果使用匿名函数形式,则可以对事件目标、事件类型、执行阶段都相同的目标注册相同的事件侦听器
将对象注册为时间侦听器
var btn = document.getElementById("btn");
var eventListener = {
message:"This is an event listener object",
handleEvent:function (e) {
alert(this.message);
}
};
btn.addEventListener("click",eventListener,false);
// "This is an event listener object"
事件处理程序/事件侦听器内的this引用
在事件处理程序内部的this引用的对象是设定了该事件处理程序的元素
document.getElementById("btn").onclick = function () {console.log(this);}; // "[object HTMLInputElement]"===this
但是下面情况不同了
var Listener = function () {};
var lib = new Object;
lib.handleClick = function (event) {console.log(this)};
document.getElementById("btn").onclick = lib.handleClick;
这里 this引用的是#btn,即设定了事件处理程序的元素。
如果希望在lib.handlerClick内通过this引用lib,可以加一个匿名函数。
document.getElementById("btn").onclick = function(event) {
lib.handleClick(event);
// lib.handlerClick中的this引用将会引用lib
};
事件流
PS:事件的发生是由用户操作引起的,在用户浏览网页的过程中,发生最为频繁的事件时mousemove事件。在鼠标指针移动的过程中,这一事件持续发生,如果设定了mousemove事件的处理,则有可能导致鼠标移动速度变慢,要注意这一点。
一个DOM元素被触发的时候,不只是在它的起源对象上触发,而是经历三个不同的阶段,事件一开始从文档根节点流向目标对象(捕获阶段),然后到目标对象上被触发(目标阶段),最后又流出到文档根节点(冒泡)。
捕获阶段:在捕获阶段中,事件将会从window对象开始向下遍历DOM树来传播。如果在流向目标对象过程中遇到设置为捕获阶段的事件侦听器,则会直接执行
目标阶段:在这一阶段中,被事件目标注册的事件侦听器将会被执行。如果一个事件处理程序被指定为了HTML的标签属性,或者指定为对象属性,则会在这一阶段执行
冒泡阶段:这一阶段中,事件的传播方式为从事件目标开始向上遍历DOM树,直至Window对象结束。在冒泡阶段遇到设置的事件侦听器将会被执行。
注意:有些事件不会经过冒泡阶段,比如,click事件为了确定需要涉及那些元素而有必要在传播事件过程中遍历DOM树,focus事件则是一种只需处理当前元素的事件。这种情况下对focus事件进行传播是没有意义的,所以focus事件不会经过冒泡阶段。
例:
<div id="div">
<button id="btn">click me!</button>
</div>
<script type="text/javascript">
var div = document.getElementById("div");
var btn = document.getElementById("btn");
// 由内之外
btn.addEventListener("click",function(e){
alert("Now to the element BUTTON");
},false);
div.addEventListener("click",function(e){
alert("Now to the element DIV");
},false);
document.body.addEventListener("click",function(e){
alert("Now to the element BODY");
},false);
document.documentElement.addEventListener("click",function(e){
alert("Now to the element DOCUMENT");
},false);
// click me! BUTTON——DIV——BODY——DOCUMENT
由于第三个参数全都设置为冒泡阶段执行,所以执行起来,事件先从window流入,但是没有发现捕获阶段的时间侦听器,继续流入直到遇到目标节点。我们操作的元素是button,它就是目标节点,所以此时是执行处于目标阶段的目标节点,弹出BUTTON,之后便是事件流流出过程,也就是冒泡过程,所有设置了冒泡阶段的时间侦听器将被执行,最后流出window对象。
那如果改变事件目标节点呢?
我们在DIV上设置CSS样式方便我们查看,此时目标元素就是DIV。随着事件流入.....到目标元素,发现我们的button虽然设置了事件侦听器,却没有执行。因为此时button已经不是目标元素,事件流经过时,它是处于流入阶段也就是捕获阶段的,但button是冒泡阶段调用,所以不会执行button,“忽略了它”。之后便是老套路:DIV-BODY-DOCUMENT.
接下来我们看看捕获阶段:
var div = document.getElementById("div");
var btn = document.getElementById("btn");
// 由内之外
btn.addEventListener("click",function(e){
alert("Now to the element BUTTON");
},true);
div.addEventListener("click",function(e){
alert("Now to the element DIV");
},true);
document.body.addEventListener("click",function(e){
alert("Now to the element BODY");
},true);
document.documentElement.addEventListener("click",function(e){
alert("Now to the element DOCUMENT");
},true);
// click me! DOCUMENT-BODY-DIV-BUTTON
点击click,事件流流入,事件目标是button,则到button之前是捕获阶段,恰好这些元素刚好是在捕获阶段设定事件侦听器,则按照DOM数规则,由外至内一层一层执行。但是注意:事件目标button是在目标阶段执行的,不是捕获也不是冒泡,不信?我们用eventPhase
来测试:
btn.addEventListener("click",function(e){
alert("Now to the element BUTTON");
alert(e.eventPhase);
},false);
// 不论是true还是false,只要点击的是button元素,eventPhase都是返回2
找到规律了吗?也没有什么规律,脑子里能模拟出DOM树就都清楚了。
btn.addEventListener("click",function(e){
alert("Now to the element BUTTON");
},false);
div.addEventListener("click",function(e){
alert("Now to the element DIV");
alert(e.eventPhase);
},true);
document.body.addEventListener("click",function(e){
alert("Now to the element BODY");
},false);
document.documentElement.addEventListener("click",function(e){
alert("Now to the element DOCUMENT");
},true);
// click me! DOCUMENT-DIV(2)-BODY
目标节点是DIV,那事件流流入最深处就是DIV,DIV就是事件目标,则button根本不会执行,不管它是捕获还是冒泡,因为它处于DOM树最深处。
如何取消事件流传播?
1.Event.stopPropagation()
终止事件在传播过程的捕获、目标处理或起泡阶段进一步传播。调用该方法后,该节点上处理该事件的处理程序将被调用,事件不再被分派到其他节点。
-
该方法将停止事件的传播,阻止它被分派到
其他
Document 节点。在事件传播的任何阶段都可以调用它。注意,虽然该方法不能阻止同一个 Document 节点上的其他事件句柄被调用,但是它可以阻止把事件分派到其他节点。btn.addEventListener("click",function(e){ alert("Now to the element BUTTON"); e.stopPropagation(); },false); div.addEventListener("click",function(e){ alert("Now to the element DIV"); },false); document.body.addEventListener("click",function(e){ alert("Now to the element BODY"); },false); document.documentElement.addEventListener("click",function(e){ alert("Now to the element DOCUMENT"); },false); // click me! BUTTON
此时只会在button执行一次事件侦听器,之后的传播被阻止了。
但是如果你点击的是DIV,那依然会往上一层一层执行事件侦听器,设置stopPropagation()
的是button
。
以此类推...
2. event.stopImmediatePropagation()
如果你希望阻止当前节点上的其他回调函数被调用的话,你可以使用更激进的event.stopImmediatePropagation()
方法。
MDN:如果某个元素有多个相同类型事件的事件监听函数,则当该类型的事件触发时,多个事件监听函数将按照顺序依次执行.如果某个监听函数执行了 event.stopImmediatePropagation()
方法,则除了该事件的冒泡行为被阻止之外(event.stopPropagation
方法的作用),该元素绑定的后序相同类型事件的监听函数的执行也将被阻止。
看MDN这段话我再试着理解理解,一直再说这事件传播,传播,到底是只有跨节点的事件处理程序被触发才能叫传播,同节点上,多个时间侦听器这个不能算是传播,这样理解就好了。
btn.addEventListener("click",function(e){
alert("1");
},false);
btn.addEventListener("click",function(e){
alert("2");
e.stopPropagation();
// e.stopImmediatePropagation()
alert("2-1");
},false);
btn.addEventListener("click",function(e){
alert("3");
},false);
div.addEventListener("click",function(e){
alert("div");
},false)
对于e.stopPropagation()而言,它能阻止的是所有阶段的事件侦听器传播,结果就是调用该方法的节点上所有的时间侦听器可以触发,但是传播被终止,也就是DIV没有了。弹出1-2-(2-3)-3,这些都是btn节点上的。
换到e.stopImmediatePropagation() ,那它就是精确到节点内的事件侦听器了
document.documentElement.addEventListener("click",function(e){
alert("document")
},true)
btn.addEventListener("click",function(e){
alert("1");
},false);
btn.addEventListener("click",function(e){
alert("2");
e.stopImmediatePropagation(); // 这里往后的节点内事件侦听器不能执行
// e.stopPropagation();
alert("2-1");
},false);
btn.addEventListener("click",function(e){
alert("3");
},false);
div.addEventListener("click",function(e){
alert("div");
},false)
// document/1/2/2-1
// 阻止事件流中当前节点的和所有后续节点的事件监听器的执行。即影响当前结点的事件监听器
这就是MDN:该元素绑定的后序相同类型事件的监听函数的执行也将被阻止,这句话的意思了。
默认操作Event.preventDefault()
在默认情况下,点击a描点元素后,将会跳转至链接页面。而如果在这时执行了Event.preventDefault()方法,则不会发生这一行为。这个方法作用等同于让一个指定为了HTML标签属性或DOM属性的事件处理程序返回一个false
值。
<a href="https://segmentfault.com" id="a">segmentfault</a>
<script type="text/javascript">
var link = document.getElementById("a");
function fn(e) {
alert("不会发生页面跳转");
event.preventDefault();
}
link.addEventListener("click",fn,false);
</script>
不过也有一些事件无法通过使用preventDefault()方法来中止。blur事件就是其中之一,它是一个焦点移动至其他元素时被触发的事件。stopPropagation()
和preventDefault()
方法不仅能够用于事件冒泡阶段,在其他阶段中也能够使用这些方法。
IE
IE浏览器下,绑定/删除(attachEvent
/detachEvent
)、阻止事件冒泡(cancelBubble
)、阻止事件的默认行为(returnValue
)、用于获取事件的目标(srcElement
)等等需要另外探讨,大体相同。到时候我另开一篇来继续学习。
Event接口对象
关于target和currentTarget
这两个属性在不同情况下指向也不尽相同,先来看看概念
target
:事件属性可返回事件的目标节点(触发该事件的节点),如生成事件的元素、文档或窗口。currentTarget
事件属性返回其监听器触发事件的节点,即当前处理该事件的元素、文档或窗口。
在捕获和起泡阶段,该属性是非常有用的,因为在这两个节点,它不同于 target 属性。
event.currentTarget
指向事件所绑定的元素,而event.target
始终指向事件发生的元素。
<div id="div">
<p id="p">
<button id="btn">click me!</button>
</p>
</div>
<script type="text/javascript">
document.getElementById("btn").onclick = function (e) {
console.log(e.target+" | "+e.currentTarget+" | "+this);
};
document.getElementById("p").onclick = function (e) {
console.log(e.target+" | "+e.currentTarget+" | "+this);
};
document.getElementById("div").onclick = function (e) {
console.log(e.target+" | "+e.currentTarget+" | "+this);
};
document.body.onclick = function (e) {
console.log(e.target+" | "+e.currentTarget+" | "+this);
};
不管你在哪个节点注册事件侦听器,引发这一系列程序的"罪魁祸首"是button
,所以target
指向它。如果button
节点换成a
描点元素,你再点击a
,target
就指向a
(理解为事件目标也行)
而currentTarget
指向的是这个事件流三个阶段中,事件侦听器绑定的元素。
由于我只是初步了解了一下,重点在应用,没有几个实实在在的前端练习是不行的。