Javascript和HTML的交互都是通过事件来实现的,而事件的产生与执行则是遵循着传统软件工程领域中的观察者模式,其能够做到页面行为页面展示的解耦合。

本节内容会从事件流谈起,然后逐一介绍几种注册事件的方式以及他们其中的一些细节,最后还会提到关于**事件委托(代理)**的概念。

事件流

  • 事件流描述了页面接受事件的顺序。因为一个事件的触发可能会影响好几处地方,这很容易理解,比如在页面上嵌套着写了几个div元素,同时在最里层的div元素上进行事件触发,那这不仅仅是最内层的div对事件进行响应,任意一层的嵌套的div都会对事件进行相应处理。(其实通过上述的观察者模式也可以推断出其合理性,因为一个对象可以由多个观察者进行观察)
  • 所以多个监听事件的元素响应顺序需要进行统一,因为一些历史原因,事件的响应顺序有两种模式:冒泡和捕获

事件冒泡

  • 顾名思义,事件的冒泡就如水底下的气泡一下,从内到外,同理,事件冒泡规定的事件流顺序也是从内而外,事件会从最深层的节点开始触发,然后向外传播到document(文档)。
  • 代码如下所示。
<html>
  <body>
    <div id="ddd">
      	click me
    </div>
  </body>
</html>
  • 此时我如果对id=ddd的div元素进行click事件触发,那么该事件会以如下顺序发生:
  1. div
  2. body
  3. html
  4. document
  • 现代的浏览器的事件会一直冒泡到window对象。

事件捕获

  • 事件捕获则和事件冒泡相反,事件的响应顺序是从外到内的,还是以上一个例子为例,那么对应click事件讲会以如下顺序发生:
  1. document
  2. html
  3. body
  4. 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对象的两个比较混淆的属性:targetcurrentTarget
  • 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>