17.1 事件流

事件流描述了页面接收事件的顺序。

17.1.1 事件冒泡

IE事件流被称为事件冒泡。因为事件被定义为从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)

元素,即被点击的元素,最先触发click事件。然后,click事件沿DOM树一路向上,在经过的每个节点上依次触发,直至到达document对象
image.png
所有现代浏览器都支持事件冒泡,现代浏览器中的事件会一直冒泡到window对象。

17.1.2 事件捕获

事件捕获:最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。
事件捕获实际上,是为了在事件到达最终目标前拦截事件
在事件捕获中,click事件首先由document元素捕获,然后沿DOM树依次向下传播,直至到达实际的目标元素


image.png
事件捕获得到了所有现代浏览器的支持
由于旧版本浏览器不支持,因此实际当中几乎不会使用事件捕获。通常建议使用事件冒泡,特殊情况下可以使用事件捕获。

17.1.3 DOM事件流

DOM2 Events规范规定事件流分为3个阶段:事件捕获、到达目标和事件冒泡。
事件捕获最先发生,为提前拦截事件提供了可能。
然后,实际的目标元素接收到事件。
最后一个阶段是冒泡,最迟要在这个阶段响应事件
点击

元素会以如霞所示的顺序触发事件:
image.png
在DOM事件流中,实际的目标(
元素)在捕获阶段不会接收到事件。因为捕获阶段从document到再到就结束了。
下一阶段,即会在
元素上触发事件的“到达目标”阶段,通常在事件处理时被认为是冒泡阶段的一部分(稍后讨论)。
然后,冒泡阶段开始,事件反向传播至文档。
虽然DOM2 Events规范明确捕获阶段不命中事件目标,但现代浏览器都会在捕获阶段在事件目标上触发事件。最终结果是在事件目标上有两个机会来处理事件。
注:所有现代浏览器都支持DOM事件流,只有IE8及更早版本不支持。

17.2 事件处理程序

事件意味着用户或浏览器执行的某种动作。比如,单击(click)、加载(load)、鼠标悬停(mouseover)。
为响应事件而调用的函数被称为事件处理程序(或事件监听器)
事件处理程序的名字以”on”开头,因此click事件的处理程序叫作onclick,而load事件的处理程序叫作onload。

17.2.1 HTML事件处理程序

特定元素支持的每个事件都可以使用事件处理程序的名字以HTML属性的形式来指定。
此时属性的值必须是能够执行的JavaScript代码。

  1. <input type="button" value="click me" onclick="console.log('点击')">

注:因为属性的值是JavaScript代码,所以不能在未经转义的情况下使用HTML语法字符,比如和号(&)、双引号(”)、小于号(<)和大于号(>)。此时,为了避免使用HTML实体,可以使用单引号代替双引号。如果确实需要使用转义字符来代替(比如:&quot表示”)
在HTML中定义的事件处理程序可以包含精确的动作指令,也可以调用在页面其他地方定义的脚本

  1. <script>
  2. function showMessage() {
  3. console.log('Hello World!');
  4. }
  5. </script>
  6. <input type="button" value="click me" onclick="showMessage()">

以这种方式指定的事件处理程序有一些特殊的地方。首先,会创建一个函数来封装属性的值。这个函数有一个特殊的局部变量event,其中保存的就是event对象

  1. <!-- 输出“click -->
  2. <input type="button" value="click me" onclick="console.log(event.type)">

有了这个对象,就不用开发者另外定义其他变量,也不用从包装函数的参数列表中去取了。
在这个函数中,this值相当于事件的目标元素

  1. <!-- 输出“click me -->
  2. <input type="button" value="click me" onclick="console.log(this.value)">

有意思的地方,就是其作用域链被扩展了。在这个函数中,document和元素自身的成员都可以被当成局部变量来访问(是通过使用with实现的)
这意味着事件处理程序可以更方便地访问自己的属性(以下代码等价于前面的)

  1. <!-- 输出“click me -->
  2. <input type="button" value="click me" onclick="console.log(value)">

经过这样的扩展,事件处理程序的代码就可以不必引用表单元素,而直接访问同一表单中的其他成员

  1. <form method="post">
  2. <input type="text" name="username" value="">
  3. <input type="button" name="Echo Username" onclick="console.log(username.value)">
  4. </form>

点击按钮会显示出文本框中包含的文本。
注:事件处理程序中的代码直接引用了username。
在HTML中指定事件处理程序有一些问题:
第一个问题是:时机问题。
有可能HTML元素已经显示在页面上,用户都与其交互了,而事件处理程序的代码还无法执行。比如在前面的例子中,如果showMessage()函数是在页面后面,在按钮中代码的后面定义的,那么当用户在showMessage()函数被定义之前点击按钮时,就会发生错误。为此,大多数HTML事件处理程序会封装在try/catch块中,以便在这种情况下静默失败

  1. <input type="button" value="click me" onclick="try{showMessage();}catch(ex){}">

如果在showMessage()函数被定义之前点击了按钮,就不会发生JavaScript错误了。因为错误在浏览器收到之前已经被拦截了。
另一个问题是:对事件处理程序作用域链的扩展,在不同浏览器中可能导致不同的结果。
不同JavaScript引擎中标识符解析的规则存在差异,因此访问无限定的对象成员可能导致错误。
使用HTML指定事件处理程序的最后一个问题是HTML与JavaScript强耦合。如果需要修改事件处理程序,则必须在两个地方,即HTML和JavaScript中,修改代码。
这也是很多开发者不使用HTML事件处理程序,而使用JavaScript指定事件处理程序的主要原因。

17.2.2 DOM0事件处理程序

在JavaScript中,指定事件处理程序的传统方式是:把一个函数赋值给(DOM元素的)一个事件处理程序属性。
要使用JavaScript指定事件处理程序,必须先取得要操作对象的引用。
每个元素(包括window和document)都有通常小写的事件处理程序属性,比如onclick。只要把这个属性赋值为一个函数即可:

  1. let btn = document.getElementById('myBtn');
  2. btn.onclick = function() {
  3. console.log('点击');
  4. }

先从文档中取得按钮,然后给它的onclick事件处理程序赋值一个函数。
注:前面的代码在运行之后才会给事件处理程序赋值。因此如果在页面中上面的代码出现在按钮之后,则有可能出现用户点击按钮没有反应的情况。
像这样使用DOM0方式为事件处理程序赋值时,所赋函数被视为元素的方法。因此,事件处理程序会在元素的作用域中运行,即this等于元素。下面的例子演示了使用this引用元素本身:

  1. let btn = document.getElementById('myBtn');
  2. btn.onclick = function() {
  3. console.log(this.id); // myBtn
  4. };

点击按钮,这段代码会显示元素的ID。这个ID是通过this.id获取的。
不仅仅是id,在事件处理程序里通过this可以访问元素的任何属性和方法。
以这种方式添加事件处理程序是注册在事件流的冒泡阶段的。
通过将事件处理程序属性的值设置为null,可以移除通过DOM0方式添加的事件处理程序

  1. btn.onclick = null; // 移除事件处理程序

把事件处理程序设置为null,再点击按钮就不会执行任何操作了。

17.2.3 DOM2事件处理程序

DOM2 Events为事件处理程序的赋值和移除定义了两个方法:addEventListener()和removeEventListener()。
这两个方法暴露在所有DOM节点上
接收3个参数:事件名、事件处理函数和一个布尔值,
true表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序。
仍以给按钮添加click事件处理程序为例:

  1. let btn = document.getElementById('myBtn');
  2. btn.addEventListener('click', () => {
  3. console.log(this.id);
  4. }, false);

按钮添加了会在事件冒泡阶段触发的onclick事件处理程序(因为最后一个参数值为false)。
与DOM0方式类似,这个事件处理程序同样在被附加到的元素的作用域中运行。
使用DOM2方式的主要优势是:可以为同一个事件添加多个事件处理程序

  1. let btn = document.getElementById('myBtn');
  2. btn.addEventListener('click', () => {
  3. console.log(this.id);
  4. }, false);
  5. btn.addEventListener('click', () => {
  6. console.log('hello world!');
  7. }, false);

给按钮添加了两个事件处理程序。多个事件处理程序以添加顺序来触发,因此前面的代码会先打印元素ID,然后显示消息“Hello world! ”。
通过addEventListener()添加的事件处理程序,只能使用removeEventListener()并传入与添加时同样的参数来移除。
这意味着使用addEventListener()添加的匿名函数无法移除

  1. let btn = document.getElementById('myBtn');
  2. btn.addEventListener('click', () => {
  3. console.log(this.id);
  4. }, false);
  5. // 其他代码
  6. btn.removeEventListener('click', function() {
  7. // 没有效果!
  8. console.log(this.id);
  9. }, false);

通过addEventListener()添加了一个匿名函数作为事件处理程序。然后,又以看起来相同的参数调用了removeEventListener()。但实际上,第二个参数与传给addEventListener()的完全不是一回事。
传给removeEventListener()的事件处理函数,必须与传给addEventListener()的是同一个

  1. let btn = document.getElementById('myBtn');
  2. let handler = function() {
  3. console.log(this.id);
  4. }
  5. btn.addEventListener("click", handler, false);
  6. // 其他代码
  7. btn.removeEventListener('click', handler, false);

有效,因为调用addEventListener()和removeEventListener()时传入的是同一个函数。
大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,主要原因是跨浏览器兼容性好。
把事件处理程序注册到捕获阶段,通常用于在事件到达其指定目标之前拦截事件。如果不需要拦截,则不要使用事件捕获。

17.2.4 IE事件处理程序

IE实现了与DOM类似的方法,即attachEvent()和detachEvent()。
接收两个同样的参数:事件处理程序的名字和事件处理函数。
因为IE8及更早版本只支持事件冒泡,所以使用attachEvent()添加的事件处理程序会添加到冒泡阶段。
注:attachEvent()的第一个参数是”onclick”,而不是DOM的addEventListener()方法的”click”。
在IE中使用attachEvent()与使用DOM0方式的主要区别是:事件处理程序的作用域。
使用DOM0方式时,事件处理程序中的this值等于目标元素;
使用attachEvent()时,事件处理程序是在全局作用域中运行的,因此this等于window
理解这些差异对编写跨浏览器代码是非常重要的。
与使用addEventListener()一样,使用attachEvent()方法也可以给一个元素添加多个事件处理程序
使用attachEvent()添加的事件处理程序将使用detachEvent()来移除,只要提供相同的参数。
与使用DOM方法类似,作为事件处理程序添加的匿名函数也无法移除。但只要传给detachEvent()方法相同的函数引用,就可以移除。

17.2.5 跨浏览器事件处理程序

要确保事件处理代码具有最大兼容性,只需要让代码在冒泡阶段运行即可。
为此,要先创建一个addHandler()方法。
addHandler()方法的任务是:根据需要分别使用DOM0方式、DOM2方式或IE方式来添加事件处理程序。
会在EventUtil对象(本章示例使用的对象)上添加一个方法,以实现跨浏览器事件处理。
接收3个参数:目标元素、事件名和事件处理函数。
有了addHandler(),还要写一个也接收同样的3个参数的removeHandler()。任务是移除之前添加的事件处理程序,不管是通过何种方式添加的,默认为DOM0方式。

  1. var EventUtil = {
  2. addHandler: function(element, type, handler) {
  3. if (element.addEventListener) {
  4. element.addEventListener(type, handler, false);
  5. } else if (element.attachEvent) {
  6. element.attachEvent('on' + type, handler);
  7. } else {
  8. element['on' + type] = handler;
  9. }
  10. },
  11. removeHandler: function(element, type, handler) {
  12. if (element.removeEventListener) {
  13. element.removeEventListener(type, handler, false);
  14. } else if (element.detachEvent) {
  15. element.detachEvent('on' + type, handler);
  16. } else {
  17. element['on' + type] = null;
  18. }
  19. }
  20. };
  21. // 使用
  22. let btn = document.getElementById('myBtn');
  23. let handler = function() {
  24. console.log('点击');
  25. };
  26. EventUtil.addHandler(btn, 'click', handler);
  27. // 其他代码
  28. EventUtil.removeHandler(btn, 'click', handler);

两个方法都是首先检测传入元素上是否存在DOM2方式。如果有DOM2方式,就使用该方式,传入事件类型和事件处理函数,以及表示冒泡阶段的第三个参数false。
否则,如果存在IE方式,则使用该方式。注意这时候必须在事件类型前加上”on”,才能保证在IE8及更早版本中有效。
最后是使用DOM0方式(在现代浏览器中不会到这一步)。
注:使用DOM0方式时使用了中括号计算属性名,并将事件处理程序或null赋给了这个属性
注:DOM0只支持给一个事件添加一个处理程序。好在DOM0浏览器已经很少有人使用了,所以影响应该不大。