1.事件流
在浏览器中,JavaScript和HTML之间的交互是通过事件去实现的,常用的事件有代表鼠标单击的click事件、代表加载的load事件、代表鼠标指针悬浮的mouseover事件。在事件发生时,会相对应地触发绑定在元素上的事件处理程序,以处理对应的操作。
通常一个页面会绑定很多的事件,那么具体的事件触发顺序是什么样的呢?
这就会涉及事件流的概念,事件流描述的是从页面中接收事件的顺序。事件发生后会在目标节点和根节点之间按照特定的顺序传播,路径经过的节点都会接收到事件。我们通过下面的场景来直观地想象一下事件的流转顺序。
页面上有一个div,分别在body、div、p、span上绑定了click事件。假如我在span上执行了单击的操作,那么将会产生什么样的事件流呢?
<body>
<div>
<p>
<span>
文本一
</span>
</p>
</div>
</body>
第一种事件传递顺序是先触发最外层的body元素,然后向内传播,依次触发div、p与span元素。
第二种事件传递顺序先触发由最内层的span元素,然后向外传播,依次触发p、div与body元素。
第一种事件传递顺序对应的是捕获型事件流,第二种事件传递顺序对应的是冒泡型事件流。
一个完整的事件流实际包含了3个阶段:事件捕获阶段>事件目标阶段>事件冒泡阶段。上述两种类型的事件流实际对应其中的事件捕获阶段与事件冒泡阶段。

- 事件捕获阶段
事件捕获阶段的主要表现是不具体的节点先接收事件,然后逐级向下传播,最具体的节点最后接收到事件。根据图中的指示就是Window > Document > html > body > div > p > span。 - 事件目标阶段
事件目标阶段表示事件刚好传播到用户产生行为的元素上,可能是事件捕获的最后一个阶段,也可能是事件冒泡的第一个阶段。 - 事件冒泡阶段
事件冒泡阶段的主要表现是最具体的元素先接收事件,然后逐级向上传播,不具体的节点最后接收事件,根据图中的指示就是span > p > div > body > html > Document >Window。
let spanDom = document.querySelector('span');
spanDom .addEventListener("click", function (e) {
console.info(`span trigger target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
}, false)
let p1Dom = document.querySelector('p');
p1Dom.addEventListener('click', function (e) {
console.info(`p trigger target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
})
let divDom = document.querySelector('div');
divDom.addEventListener('click', function (e) {
console.info(`div trigger target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
})
let bodyDom = document.querySelector('body');
bodyDom.addEventListener('click', function (e) {
console.info(`body trigger target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
})
当点击span标签时候,将输出

使用addEventListener()函数绑定的事件在默认情况下,即第三个参数默认为false时,按照冒泡型事件流处理。第三个参数为true时,按捕获型事件流处理,再次点击span时候会输出如下:

如果有元素绑定了捕获类型事件,则会优先于冒泡类型事件而先执行。
2.事件处理程序
简单理解事件处理程序,就是响应某个事件的函数,例如onclick()函数、onload()函数就是响应单击、加载事件的函数,对应的是一段JavaScript的函数代码。
根据W3C DOM标准,事件处理程序分为DOM0、DOM2、DOM3这3种级别的事件处理程序。由于在DOM1中并没有定义事件的相关内容,因此没有所谓的DOM1级事件处理程序。
- DOM0级事件处理程序
DOM0级事件处理程序是将一个函数赋值给一个事件处理属性,有两种表现形式。第一种是先通过JavaScript代码获取DOM元素,再将函数赋值给对应的事件属性。
var btn = document.getElementById("btn");
btn.onclick = function(){}
第二种是直接在html中设置对应事件属性的值,值有两种表现形式,一种是执行的函数体,另一种是函数名,然后在script标签中定义该函数。
<button onclick="alert('hi');">单击</button>
<button onclick="clickFn()">单击</button>
<script>
function clickFn() {
alert('hi');
}
</script>
以上两种DOM0级事件处理程序同时存在时,第一种在JavaScript中定义的事件处理程序会覆盖掉后面在html标签中定义的事件处理程序。
DOM0级事件处理程序只支持事件冒泡阶段,一个事件处理程序只能绑定一个函数。
- DOM2级事件处理程序
在DOM2级事件处理程序中,当事件发生在节点时,目标元素的事件处理函数就会被触发,而且目标元素的每个祖先节点也会按照事件流顺序触发对应的事件处理程序。DOM2级事件处理方式规定了添加事件处理程序和删除事件处理程序的方法。
在IE10及以下版本中,只支持事件冒泡阶段
element.attachEvent("on"+ eventName, handler); //添加事件处理程序
element.detachEvent("on"+ eventName, handler); //删除事件处理程序
在IE11及其他非IE浏览器中,同时支持事件捕获和事件冒泡两个阶段,可以通过addEventListener()函数添加事件处理程序,通过removeEventListener()函数删除事件处理程序。
addEventListener(eventName, handler, useCapture); //添加事件处理程序
removeEventListener(eventName, handler, useCapture); //删除事件处理程序
//其中的useCapture参数表示是否支持事件捕获,true表示支持事件捕获,false表示支持事件冒泡,默认状态为false。
- DOM3级事件处理程序
DOM3级事件处理程序是在DOM2级事件的基础上重新定义了事件,也添加了一些新的事件。最重要的区别在于DOM3级事件处理程序允许自定义事件,自定义事件由createEvent("CustomEvent")函数创建,返回的对象有一个initCustomEvent()函数,通过传递对应的参数可以自定义事件。
函数可以接收以下4个参数。
· type:字符串、触发的事件类型、自定义,例如“keyDown”“selectedChange”。
· bubble(布尔值):表示事件是否可以冒泡。
· cancelable(布尔值):表示事件是否可以取消。
· detail(对象):任意值,保存在event对象的detail属性中。
创建完成的自定义事件,可以通过dispatchEvent()函数去手动触发,触发自定义事件的元素需要和绑定自定义事件的元素为同一个元素。
<body>
<div>
<p>
<span>
自定义事件
</span>
</p>
</div>
</body>
<script>
var customEvent;
(function () {
if (document.implementation.hasFeature('CusomEvents', '3.0')) {
let detailData = {name: 'climber.lee'}
customEvent = document.createEvent('CustomEvent');
customEvent.initCustomEvent('myEvent', true, false, detailData)
let p = document.querySelector('p');
p.addEventListener('myEvent', function (e) {
console.info(`p监听到自定义事件执行,参数为:`,e.detail)
});
let span = document.querySelector('span');
span.addEventListener('click', function () {
p.dispatchEvent(customEvent);
})
}
})();
</script>
点击span后输出

3.事件(Event)对象
事件在浏览器中是以Event对象的形式存在的,每触发一个事件,就会产生一个Event对象。该对象包含所有与事件相关的信息,包括事件的元素、事件的类型及其他与特定事件相关的信息。
在Event对象中有两个属性总是会引起大家的困扰,那就是target属性和currentTarget属性。两者都可以表示事件的目标元素,但是在事件流中两者却有不同的意义。
· target属性在事件目标阶段,理解为真实操作的目标元素。
· currentTarget属性在事件捕获、事件目标、事件冒泡这3个阶段,理解为当前事件流所处的某个阶段对应的目标元素。
可以参考第一部分事件流的实例来理解target和currentTarget的区别
有时我们并不想要事件进行冒泡,可通过stopPropagation和stopImmediatePropagation两个方法阻止冒泡
· stopPropagation()函数仅会阻止事件冒泡,其他事件处理程序仍然可以调用。
· stopImmediatePropagation()函数不仅会阻止冒泡,也会阻止其他事件处理程序的调用。
在众多的HTML标签中,有一些标签是具有默认行为的
· a标签,在单击后默认行为会跳转至href属性指定的链接中。
那么该如何编写代码来阻止元素的默认行为呢?
很简单,就是通过event.preventDefault()函数去实现。
4.事件委托
过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。例如,click事件冒泡到document。这意味着可以为整个页面指定一个onclick事件处理程序,而不用为每个可点击元素分别指定事件处理程序。举个栗子
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
这里的HTML包含3个列表项,在被点击时应该执行某个操作。对此,通常的做法是像这样指定3个事件处理程序
let item1 = document.getElementById("goSomewhere");
let item2 = document.getElementById("doSomething");
let item3 = document.getElementById("sayHi");
item1.addEventListener("click", (event) => {
location.href = "http:// www.wrox.com";
});
item2.addEventListener("click", (event) => {
document.title = "I changed the document's title";
});
item3.addEventListener("click", (event) => {
console.log("hi");
});
如果对页面中所有需要使用onclick事件处理程序的元素都如法炮制,结果就会出现大片雷同的只为指定事件处理程序的代码。使用事件委托,只要给所有元素共同的祖先节点添加一个事件处理程序,就可以解决问题。
let list = document.getElementById("myLinks");
list.addEventListener("click", (event) => {
let target = event.target;
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":
console.log("hi");
break;
}
});
事件委托具有如下优点
- document对象随时可用,任何时候都可以给它添加事件处理程序(不用等待DOMContentLoaded或load事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。
- 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省DOM引用,也可以节省时间。
- 减少整个页面所需的内存,提升整体性能。
网友评论