Javascript和HTML的交互都是通过事件来实现的,而事件的产生与执行则是遵循着传统软件工程领域中的观察者模式,其能够做到页面行为和页面展示的解耦合。
本节内容会从事件流谈起,然后逐一介绍几种注册事件的方式以及他们其中的一些细节,最后还会提到关于**事件委托(代理)**的概念。
事件流
- 事件流描述了页面接受事件的顺序。因为一个事件的触发可能会影响好几处地方,这很容易理解,比如在页面上嵌套着写了几个div元素,同时在最里层的div元素上进行事件触发,那这不仅仅是最内层的div对事件进行响应,任意一层的嵌套的div都会对事件进行相应处理。(其实通过上述的观察者模式也可以推断出其合理性,因为一个对象可以由多个观察者进行观察)
- 所以多个监听事件的元素响应顺序需要进行统一,因为一些历史原因,事件的响应顺序有两种模式:冒泡和捕获。
事件冒泡
- 顾名思义,事件的冒泡就如水底下的气泡一下,从内到外,同理,事件冒泡规定的事件流顺序也是从内而外,事件会从最深层的节点开始触发,然后向外传播到document(文档)。
- 代码如下所示。
<html>
<body>
<div id="ddd">
click me
</div>
</body>
</html>
- 此时我如果对id=ddd的div元素进行click事件触发,那么该事件会以如下顺序发生:
- div
- body
- html
- document
- 现代的浏览器的事件会一直冒泡到
window
对象。
事件捕获
- 事件捕获则和事件冒泡相反,事件的响应顺序是从外到内的,还是以上一个例子为例,那么对应click事件讲会以如下顺序发生:
- document
- html
- body
- div
DOM事件流
- DOM2 Events规范里规定里事件流为分三个部分:事件捕获,到达目标和事件冒泡。
- 需要注意的是,div元素(即直接触发元素)是不会响应捕获事件的,因为通常认为直接触发事件是冒泡阶段发生的,所以它也是冒泡阶段第一个发生的事件。
- 但现在大多数支持DOM事件流的浏览器都实现了一个小小的拓展,即在捕获阶段在事件目标上触发事件。最终结果表现为有两个机会来处理事件。
事件处理程序
- 事件意味着用户或浏览器执行的某种动作,而为响应事件而调用的函数被称为事件处理程序(事件监听器)。
HTML事件处理程序
- HTML事件处理程序是以HTML属性的形式来进行指定的。该属性的值必须是能够执行的javascript代码。
- 比如下面这个例子,就是按钮在被点击时执行一段代码。
<button onclick="console.log('click')">
click me
</button>
- 当然也可以以函数的形式来进行响应事件定义。
<script>
function click(event){
console.log('click')
}
</script>
<button onclick="click(event)">
click me
</button>
- 可以看到,除了把函数单独拎出来以外,还多了一个event对象,这个是一个特殊的局部变量,它定义了事件触发的一些属性以及被触发元素的一些属性。
- 除此之外,HTML事件处理程序中的this就是DOM元素本身,所以可以直接使用this对象去获取对应元素上的属性。
<input type="button" value="Click Me" onclick="console.log(this.value)">
- 这里还有个比较有趣的地方,获取元素属性时可以直接省略掉this,直接使用value,即下面写法也能达到同样的效果。
<input type="button" value="Click Me" onclick="console.log(value)">
- 因为这个包装函数在创建时其作用域链被
with
操作符给延长了,所以document和元素自身的成员都可以被当成局部变量来使用。
function() {
with(document) {
with(this) {
// 属性值
}
}
}
- 但最好不要这样做,因为不仅仅会显得很诡异,而且在后期调试时也会造成误解。
HTML事件处理程序一个比较大的问题是:它把HTML和Javascript在代码上进行了强耦合(在逻辑上依然是分开的),如果我们需要更改响应程序,那么两处都需要进行修改。
所以更多的时候,我们使用的是Javascript去指定事件处理程序而不是HTML。
DOM0 事件处理程序
- 这是Javascript指定事件处理程序的传统方式,在Javascript中对DOM元素的属性进行赋值从而达到事件监听的效果。
let btn = document.getElementById("myBtn");
// 如果在下面的事件赋值之前进行事件点击,是没有任何响应的。
btn.onclick = function() {
console.log("Clicked");
};
- 如果需要移除事件处理程序,只需要对onclick属性赋值null即可。
btn.onclick = null;
DOM2 事件处理程序
- DOM2 Events为事件处理程序的赋值和移除定义了两个方法: addEventListener 和 removeEventListener。其目的在于统一事件注册接口,因为不只有click这么一个事件,还有诸多的类似于mouseover,scroll等的事件,所以把接口统一是很有必要的。
- 如果要给按钮添加click事件,可以这样写:
let btn = document.getElementById("myBtn");
btn.addEventListener("click", (e) => {
console.log(this.id);
}, false);
// 第三个参数表示是冒泡事件还是捕获事件
// false -> 冒泡(缺省状态)
// true -> 捕获
- 需要注意的是,通过addEventListener添加的匿名函数无法被移除,如上述例子添加的click事件就无法移除,所以事件的处理函数最好单独用一个变量进行存储,方便以后删除。
let btn = document.getElementById("myBtn");
let handle = () => {
console.log(this.id);
}
btn.addEventListener("click", handle ,false);
// 进行一番处理后移除监听函数
btn.removeEventListener('click', handle)
事件对象 event
- 事件对象就是之前在HTML事件处理程序中提到的Event对象,只不过在Javascript事件处理程序中不需要显示传入Event对象,因为DOM的所有事件函数都已经装载了event对象,直接使用第一个变量即可。
let btn = document.getElementById("myBtn");
let handle = (e) => {
console.log(e.target);
}
btn.addEventListener("click", handle ,false);
- 这里需要提到event对象的两个比较混淆的属性:
target
和currentTarget
。 - target指的是事件目标,即直接触发了事件的那个DOM元素本身。
- 而currentTarget则是当前事件的处理程序所绑定的元素。这可能不怎么好理解,下面有个实例。
<body>
<div id="btn">
</div>
</body>
<script>
document.body.addEventListener('click', function(e) {
console.log('事件冒泡获得')
console.log(this === document.body) // true
console.log(this === e.currentTarget) // true
console.log(this === e.target) // false
})
</script>
- 当我点击id=btn的div元素时,直接触发事件的元素是div,但由于事件冒泡所以body上也会响应该事件,且执行对应的事件函数。
- 而这里的target指的是id=btn的div元素,currentTarget则指的是body,即绑定事件的DOM元素。
事件委托(代理)
- 绑定事件监听函数是会占据内存空间的,虽然每个事件本身不会占据太多空间,但一旦数量多起来,还是会对性能造成一定的影响,所以在优化内存占用时,会尽可能地去减少事件的注册数量,尽量用一个代理函数去处理多个事件。
- 从代码实现的层面来考虑,通过事件冒泡和DOM树机制可以联想到,多个子节点的事件进行冒泡时最终会汇聚到某个父节点上,利用这一点,我们就不必在每个子节点上都设置事件监听函数,而是在对应的父节点上设置一个事件冒泡拦截函数,用这一个函数去处理每个子节点的事件触发,这就完成了一次事件委托,极大减少了事件监听的数量。
- 最常见的应用便是列表节点的事件监听,如下所示:
<!-- 这里直接使用红宝书上的例子 -->
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
<script>
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;
}
});
</script>