JavaScript 与 HTML 之间的交互是经过事件来实现的, 因此了解事件的基本内容是必要的。文章将讨论 JavaScript 事件的部分相关内容,包括事件流、事件处理程序、事件对象、事件委托,以上内容分别构成了文章的 1~4 小节。html
已知网页的 DOM 节点构成的是树状结构,称为 DOM 树。浏览器
并且又知当页面上的一个节点触发了事件时,接收到事件通知的不只仅只有这个节点,而是沿着它所在的DOM树的一支从 Document 到这个节点自己的全部元素均可以接收到事件的通知,可是接收到通知也是有前后顺序的,事件流就描述了这一支DOM树中上的元素接收到事件通知的顺序。函数
不一样浏览器厂商对于事件流的方向的实现不一样甚至相反,能够分为两种:冒泡流和捕获流。有趣的是,这两种事件流几乎是彻底相反的,下面分别讨论这两种事件流。同时讨论DOM规定的事件流的三个阶段。性能
IE的事件流叫作事件冒泡(event bubbling),顾名思义,这是从下到上来的。即从DOM树的下层向上层流动,也就是从具体的元素向较为不具体的元素传播,也能够说是子元素向父元素的方向传播。优化
例如在下面的DOM结构(缩进表示节点的层级)中:ui
html
head
body
div
复制代码
DOM树结构和冒泡事件流方向是下面这样的:this
因此可知若是触发了上面div
元素的事件,则接收到事件通知的元素和顺序为:spa
div -> body -> html -> document
复制代码
与冒泡流相反,事件捕获的思想是从DOM树的最上方传到触发事件的节点自己,也就是从较为不具体的元素传向具体的元素传播,也能够说从父元素向子元素传播。设计
以上一节的DOM结构为例,可知事件捕获流的示意图为:code
即若是在div
元素上触发了事件,则受到事件通知的元素的顺序是:
document -> html -> body -> div
复制代码
虽然事件是在div
身上触发的,但它倒是最后才收到事件通知的。
DOM2级事件规定的事件流是分阶段的,共分红三个阶段,即:
也就是说DOM2级规定的事件流包括了冒泡和捕获这两个不一样方向的事件流,同时加上了一个处于目标的阶段。
须要注意的是DOM2级定义的捕获阶段并不会发生到触发事件的元素自己,而是只到它的上层节点;而处于目标阶段则发生且只发生在触发事件的元素上;冒泡阶段管的是最宽的,它从触发事件的元素自己到DOM树的顶部都会涉及到。
仍然以1.1节中的DOM结构为例,从下面的示意图看这三个阶段会比较清晰(为了方便,略去了无关的head
节点):
从上面能够看到,捕获阶段从根节点到body
节点就中止了;处于目标阶段只发生在了div
身上;冒泡阶段则是从div
一直通知到了根节点。
虽然DOM规定了在捕获阶段不要涉及到目标元素,可是大多数浏览器都没有遵循这个规定,都涉及到了目标元素,即以下图所示:
添加事件处理程序的方式能够分红如下几种:
能够在HTML标签中直接用onXXX
来指定事件处理程序,例如指定元素被点击时输出一个字符串能够这样作:
<div onclick="console.log('div is clicked'); alert('clicked')"></div>
复制代码
则控制台里会输出div is clicked
,窗口会弹框输出clicked
。
也能够调用页面中其余部分的代码,例如调用一个在script
标签中定义的函数:
<script>
// 定义了一个函数 fn
function fn(){
console.log(this + '调用了函数 fn!');
}
</script>
<!-- 调用页面中的 fn 函数 -->
<div onclick="fn()"></div>
复制代码
注意:通常不会用到这种方式直接处理事件,由于有以下几个缺点:
- 若是函数在被解析以前就调用了,则会报错
- 这样会致使 HTML 和 JavaScript 的强耦合,改动比较麻烦
这种方式为用 on
加上事件名称来指定事件处理程序,例如
element.onClick = handler
复制代码
下面举一个更具体的例子,获取页面上的一个id
为container
的元素,并指定点击事件的处理函数:
<!-- id是target的元素 -->
<div id="target"></div>
<script>
// 获取id是target的元素
let target = document.getElementById('target');
// 给 target 指定点击事件的处理程序
target.onclick = function(){
console.log('div#target is clicked');
}
</script>
复制代码
当点击 div#target
时,控制台会输出div#target is clicked
。
能够将事件处理程序指定为 null
来解除原来为元素指定的事件处理程序,例如将上面target
的点击事件的处理程序给解绑:
// 解绑点击事件的处理程序
target.onclick = null;
复制代码
这样再点击这个元素就不会有任何反应了
这种事件处理程序的指定方式有以下特色和须要注意的点:
事件处理程序中的this
指向的是这个元素自己,因此能够直接引用它身上的属性,例如给上个例子里的 target
元素加上三个类名,并在事件处理程序中经过 this
属性来获得它的类名这个属性:
<div id="target" class="cname1 cname2 cname3"></div>
target.onclick = function(){
console.log(this.className);
}
复制代码
当点击div#target
时控制台会输出:cname1 cname2 cname3
这种方法只能够指定一个事件处理程序,后面指定的会覆盖前面的,例如:
// 给 target 指定 第一个 点击事件的处理程序
target.onclick = function(){
console.log('我是第一个');
}
// 给 target 指定 第二个 点击事件的处理程序
target.onclick = function(){
console.log('我是第二个');
}
// 点击 target 时会输出:我是第二个
复制代码
经过上面的实验能够看出后面定义的处理程序覆盖了前面的。
DOM 2 级定义了两个函数来给一个元素绑定和解绑事件处理程序,分别是
addEventListener(eventName, handler, flag)
来给调用的元素指定事件处理程序removeEventListener(eventName, handler, flag)
来给调用的元素指定事件处理程序这两个函数都接收三个参数,分别是:
eventName
: 事件的名称,注意事件的名称是不以on
开头的,例如点击事件名为click
handler
:事件处理程序,能够是个匿名函数,也能够是实现定义好的具名函数flag
:布尔值,可取true || false
;当取true
时,会在捕获阶段调用事件处理程序,不然会在冒泡阶段调用例如,为元素div#target
使用这个方法添加事件处理程序:
target.addEventListener('click', function(){
console.log('我是 addEventListener 添加的事件处理程序');
}, false);
// 点击 target 时会输出: 我是 addEventListener 添加的事件处理程序
复制代码
上面的事件处理程序会在冒泡阶段调用, 由于第三个参数 flag
为 false
再例,用 removeEventListener
给元素取消某个事件处理程序:
// 添加事件处理程序
target.addEventListener('click', handler, false);
// 在没写下面的代码以前点击 target 会输出 '我是事件处理程序'
// 取消事件处理程序
target.removeEventListener('click', handler, false);
// 如今点击 target 元素就没有什么反应了
// 这个函数会被做为事件处理程序来调用
function handler(){
console.log('我是事件处理程序');
}
复制代码
这种添加和取消事件处理程序的方法有以下特色:
能够添加多个事件处理程序,会按添加的顺序触发
// 添加两个事件处理程序
target.addEventListener('click', function(){
console.log('我是第一个事件处理程序');
}, false);
target.addEventListener('click', function(){
console.log('我是第二个事件处理程序');
}, false);
/* 点击 target 元素会输出 我是第一个事件处理程序 我是第二个事件处理程序 */
复制代码
使用 removeEventListener
给元素取消事件处理程序时,参数要和对应的addEventListener
的参数彻底对应才能够,这意味着使用匿名函数做为事件处理函数时没法被取消,由于每一个匿名函数都不是同一个:
target.addEventListener('click', function(){
console.log(1);
}, false);
// 试图取消上面的事件处理函数
target.removeEventListener('click', function(){
console.log(1);
}, false);
// 点击 target 仍会输出 1, 由于 removeEventListener 中做为的事件处理程序的匿名函数
// 并非 addEventListener 中的事件处理函数同样,虽然他们内部定义的行为同样
复制代码
因此,若是但愿随时取消一个事件处理程序,那么不要使用匿名函数。
IE 实现了两个与DOM2级类似的方法:
attachEvent(eventName, handler)
:添加事件处理程序detachEvent(eventName, handler)
:解绑事件处理程序这两个函数都接收两个参数,分别是:
eventName
: 事件的名称,注意事件的名称与DOM 2级的两个方法addEventListener 和 removeEventListener
不一样,此处是以on
开头的,例如点击事件名为onclick
handler
:事件处理程序,能够是个匿名函数,也能够是实现定义好的具名函数IE 的这两个事件处理程序有几个要注意的点:
eventName
是以 on
开头的,和DOM 2级的两个方法的 eventName
的不以 on
开头不一样this
属性指向的是全局对象,不是当前的元素在编写跨浏览器的事件处理程序时,为了保持和IE 浏览器的一致性,应将DOM 2级的两个方法的第三个参数设置为
false
,以在事件冒泡阶段被调用。
在一个事件被触发时,会产生一个事件对象,这个事件对象包含着与当前事件有关的全部信息,并且会被传入事件处理程序中以便于获得关于事件的全部信息。
全部浏览器都支持事件对象,可是具体实现不一样,能够分红DOM中的事件对象和IE中的事件对象两种。
不管是DOM0级仍是DOM2级都支持事件对象,并且行为相同。
例如,在将事件对象以 event 为名并传入事件处理程序中,并且在其中访问它的一个属性:
target.addEventListener('click', function(event){
console.log('DOM 2 级中的event.type是: ' + event.type);
}, false);
target.onclick = function(event){
console.log('DOM 0 级中的event.type是: ' + event.type);
}
/* 点击时会输出 DOM 2 级中的event.type是 click DOM 0 级中的event.type是 click */
复制代码
上面的结果能够看出两种事件的指定方法中的事件对象的type
属性值没有区别。
DOM的事件对象中有不少属性和方法可供访问和使用,下面举几个经常使用的例子,更详细的信息能够去红宝书上查阅。
currentTarget
: 当前事件处理程序在哪一个元素上,因为有事件流,因此事件会在多个元素上被处理,如前文第1节所述所述target
:触发事件的元素preventDefault()
函数:取消事件的默认行为,这个函数最为熟悉的用法应该是用来取消连接元素<a>
的默认跳转行为,若是使用了这个函数,则点击<a>
连接时就不会自动跳转了。注意:只对cancelable
属性值为true
的事件对象才有效stopPropagation()
函数:取消事件继续冒泡或者捕获其中currentTarget
和target
属性值得区分一下,简单的说前者能够用来判断一个事件已经流到了哪里,也就是已经捕获或者冒泡到了哪里;后者能够用来判断一个事件的触发者是什么元素。例以下面的例子:
定义两层嵌套的div
元素,都在事件处理函数中输出currentTarget
和target
,观察两个元素中的属性差异:
<div id="outer" style="width: 300px; height: 300px; background-color: brown;">
<div id="inner" style="width: 100px; height: 100px; background-color: cadetblue;"></div>
</div>
outer.addEventListener('click', function(event){
console.log('>> 这是 outer 的事件处理函数');
console.log('如今事件冒泡到了 ' + event.currentTarget.id);
console.log('这个事件的发起元素是 ' + event.target.id);
}, false);
inner.addEventListener('click', function(event){
console.log('>> 这是 inner 的事件处理函数');
console.log('如今事件冒泡到了 ' + event.currentTarget.id);
console.log('这个事件的发起元素是 ' + event.target.id);
}, false);
/*
当点击内部元素时,控制台输出:
>> 这是 inner 的事件处理函数
如今事件冒泡到了 inner
这个事件的发起元素是 inner
>> 这是 outer 的事件处理函数
如今事件冒泡到了 outer
这个事件的发起元素是 inner
*/
复制代码
能够看到 currentTarget
属性随着事件流的流向而变化,当事件冒泡到 inner 函数时, currentTarget
指向的是 inner
,当冒泡到 outer
时,currentTarget
指向的是 outer
。
同时能够看出 current
属性始终是指向inner
的,由于inner
是click
事件的触发元素,因此不管事件冒泡到了哪一个元素,事件对象的target
属性始终指向的是inner
。
IE的事件对象也有本身特有的属性,在IE8以前的事件对象的属性虽然和DOM中的事件对象的属性名称不一样,可是具备对应关系,具体以下:
canceBubble
属性:布尔类型,当设置为true时能够取消事件冒泡,默认为falsereturnValue
属性:布尔类型,当设置为false时能够取消事件的默认行为,默认值是truesrcElement
属性:指向事件的触发元素,与DOM中的target
属性对应type
属性:值为数据类型IE8及以前中的事件对象的获取方式根据指定方式的不一样而不一样:当经过DOM 0级指定事件处理程序时,事件对象是window
的event
属性,这并非默认传入事件处理函数中的:
outer.onclick = function(e){
var event = window.event;
console.log(event.src);
}
复制代码
在IE9及以后的版本中,实现了兼容DOM事件对象。因此在IE9及以后,既可使用DOM事件对象,也可使用IE自己特有的事件对象(MSEventObj)。并且在IE11以后,在DOM0级的方式中,经过传递的event对象和从window中取出的event对象均指向同一个对象:
outer.onclick = function(e){
console.log(e === window.event); // IE11:true; IE10-IE8: false
}
复制代码
页面中事件处理程序的数量会影响到应用的性能,这是由于每一个事件处理函数都是对象,内存中对象越多,则占用内存越多;其次,事件处理程序越多,每每意味着其中的DOM操做越多,而DOM操做的频率是影响页面性能的重要因素。
因为1. 事件流 和 2. 事件对象中target
属性始终会指向当前事件的触发元素自己这两个性质的存在,便产生了事件委托策略来减小页面中事件处理程序的绑定数量。
如有下面的DOM结构:
html
head
body
div#div1
div#div2
复制代码
对应的DOM树为
能够看到div#container
和button#btn
两个元素的事件流都会通过document
元素。
若是想给div#container
和button#btn
两个元素分别加上一个事件处理程序,例如:
div1.addEventListener('click', function(event){
console.log('>> 这是 div1 的事件处理函数');
}, false);
/* 点击 div1, 会输出: >> 这是 div1 的事件处理函数 */
div2.addEventListener('click', function(event){
console.log('>> 这是 div2 的事件处理函数');
}, false);
/* 按下 div2, 会输出: >> 这是 div2 的事件处理函数 */
复制代码
上面的例子给两个div
元素都添加了一个事件处理函数,可是咱们知道这两个事件都会流到document
上,所以能够在document的事件处理程序中使用事件对象的target
属性来判断事件的触发元素是谁,并做出相应的处理:
document.addEventListener('click', function(event){
switch (event.target.id) { // 取出事件触发者的 id
case 'div1': // 若事件的触发者是 div1
console.log('>> 这是 div1 的事件处理函数');
break;
case 'div2': // 若事件的触发者是 div2
console.log('>> 这是 div2 的事件处理函数');
break;
default:
break;
}
}, false);
/* 点击 div1, 会输出: >> 这是 div1 的事件处理函数 按下 div2, 会输出: >> 这是 div2 的事件处理函数 */
复制代码
从上面能够看出,灵活的利用事件流和事件对象的属性能够完成事件委托策略,将具体元素的事件委托到顶部元素上处理,这样虽然增长了些许判断的操做,却能显著减小页面中事件处理程序的数量,达到优化性能的目的。
参考文献:《JavaScript高级程序设计》
若有错误,感谢指正~
#完