事件就是用户或浏览器自身执行的某种动作,而响应某个事件的函数就叫做事件处理程序(或事件侦听器),也就是我们常说的事件绑定。

1. HTML 事件处理程序

某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的 HTML 特性来指定。这个特性的值应该是能够执行的 JavaScript 代码

  1. <input type="button" value="click me" onclick="alert('clicked')">

特点是:
(1)可以调用页面其他地方定义的脚本,也就是有权访问全局作用域中的任何代码

(2)这样会创建一个封装这元素属性值的函数,有一个局部变量 event,也就是事件对象,可以直接访问,且其中的 this 指向目标元素

(3)它会拓展作用域,这个函数内部可以像访问局部变量一样访问 document 及该元素本身的成员,如果当前元素是一个表单元素,则作用域中还会包含访问表单元素的入口。实际上,是想让事件处理程序无需引用表单元素就能访问其他表单字段。

缺点:
(1)存在时差问题。用户可能在元素一出现在页面中就触发相应事件,但当时的事件处理程序有可能还不具备执行条件,就会报错。

(2)这样扩展事件处理程序的作用域链在不同的浏览器中会导致不同的结果,不同的引擎解析规则有差异,很可能会在访问非限定对象成员时出错

(3)HTML 与 JavaScript 代码紧密耦合。如果要更换事件处理程序,会更加麻烦,而且都写在结构中,显得累赘。这正是大多数开发人员摒弃 HTML 事件处理程序,而使用 JavaScript 指定时间处理程序的原因所在。

但是,现在很多框架又重新使用了这一形式,目的就是不需要通过获取去元素再绑定事件的繁琐流程,但是其底层的实现仍然进行了处理,使用的是 JS 事件处理程序,而不是使用原生的 HTMl 事件处理程序。

2. DOM 0 级事件处理程序

DOM 0 级事件处理程序

  • 上面带有 on 的事件都是 0 级事件

  • 0 级事件都是在冒泡阶段触发

  • 由于是属性赋值,会存在覆盖问题,所以只允许给元素的某个事件绑定一个事件处理程序

  • 默认属性值为 null

DOM 0 级事件处理程序的元素,就是将一个函数赋值给一个事件处理程序属性(浏览器会自行建立监听机制,当我们触发元素的某个行为,浏览器会自己把属性中赋的值去执行)

这个方法仍然被所有现代浏览器所支持

  • 简单

  • 具有跨浏览器的优势

要使用 JavaScript 执行事件处理程序,首先必须取得一个要操作的对象的引用,每个元素都有自己的事件处理程序属性。

  1. oDiv.onclick = function(){}

但要注意,在这些代码运行以前不会执行事件处理程序。

使用 DOM 0 级方法执行事件处理被认为是元素的方法,因此是在元素的作用域中运行的,所以程序中的 this 引用当前元素。

可以删除通过 DOM 0级方法指定的事件处理程序,只需要将元素的事件属性指定为 null 即可

  1. oDiv.onclick = null;

3. DOM 2 级事件处理程序

DOM 2 级事件处理程序

  • 通过 addEventListener('click', function(){}) 进行事件绑定

  • 通过 removeEventListener,传入相同的参数来移除相应的事件函数

  • 支持绑定多个函数

  • 只有三个参数都相同时,才会处理成重复绑定,这时就不会再往 事件池 添加

  1. oDiv.addEventListener('click', function() {
  2. }, false)

参数:

  • 事件类型

  • 对应要执行的函数

  • 布尔值,true 表示绑定的事件在捕获阶段触发,false 表示在冒泡阶段触发,默认为 false

  1. function f1() {
  2. console.log(1);
  3. }
  4. oDiv.addEventListener('click', f1, false);
  5. oDiv.addEventListener('click', f1, false);
  6. //=> 触发点击只执行一次,输出一个 1
  7. oDiv.addEventListener('click', f1, true);
  8. oDiv.addEventListener('click', f1, false);
  9. //=> 触发点击执行两次,分别在冒泡和捕获阶段执行,输出两个 1

这是旧版本 DOM 的规定,addEventListener()的第三个参数是一个布尔值表示是否在捕获阶段调用事件处理程序。
随着时间的推移,很明显需要更多的选项。于是就把第三个参数变成了一个选项对象,具有三个属性:

  • capture: Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。

  • once: Boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。

  • passive: Boolean,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。

这两个方法都是 EventTarget.prototype 上定义的,所以只要是 Event 的实例,都能使用这两个方法。而 Node / Window 类都继承自 EventTarget 的,所以所有元素都可以使用,window 也包括在内。

DOM 2 级完成事件绑定,是基于事件池机制完成的

  • 浏览器只有一个事件池

  • 当使用 addEventListener 绑定事件时,实际上是往事件池添加这个事件处理程序

  • 事件池记录:序号、元素、事件类型、方法、阶段…,以表格的形式

  • 当事件池中已经存在相同的处理程序时(元素、类型、方法、阶段),不会再往事件池添加,避免了重复

  • 当触发某个事件时,浏览器会到事件池中这个事件的所有处理程序按照顺序依次执行

    • 执行的时候会把事件对象传递给它

    • 事件处理程序中的 this 指向当前操作元素

通过 addEventListener 添加的事件处理程序只能使用 removeEventlistener 来移除,移除时,传入的参数必须与添加时的参数相同,也就意味着如果通过匿名函数绑定,那么永远不可能移除,所以,应当使用具名函数进行绑定。

大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度的兼容各种浏览器。最好只在需要在事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。

DOM 0 级与 DOM 2 级的区别:

  • 机制不一样

    • DOM 0 级采用的是给私有属性赋值,所以只能绑定一个方法

    • DOM 2 级采用的是事件池机制,所以能够绑定多个不同的方法

  • 移除的操作

    • DOM 0 级只能绑定一个,那么只需要把那个私有属性设置为 null 即可

    • DOM 2 级能绑定多个,在移除时,需要传入具体的函数,才能在事件池中移除掉,所以基于 DOM 2 级做事件绑定,要有瞻前顾后的思路,也就是绑定的时候考虑一下如何移除,不要绑定匿名函数,都绑定命名函数

  • DOM 2 级中增加了一些 DOM 0 级无法操作的事件,如:DOMContentLoaded 事件(当页面中的 HTML 结构加载完毕就会触发执行)

    • window.onload:当前页面中的资源都加载完毕(HTML 结构加载完成、CSS 和 JS 等资源加载完成等)才会触发执行

    • $(funciton(){})/$(document).ready(function(){}) :基于 DOMContentLoaded 完成的,HTML 结构加载完毕执行,IE 中使用 onreadystatechange 监听的,在 document.readyState === ‘complete’ 的时候执行函数,其中都使用了 window.onload 来保证函数的执行。基于 DOM 2事件绑定的,所以在同一个页面中可以执行多次,绑定多个方法。

  • DOM 0 级和 DOM 2 级可以同时绑定,没有优先级,谁先绑定就先执行谁

4. IE 事件处理程序

DOM 2 事件绑定的兼容
Chrome 和 IE 高版本都是 addEventListener
在移除事件绑定的时候,如果移除操作发生在正要执行的方法之前,如点击的时候,将要执行 fn8,但是在执行 fn4 的时候就把 fn8 从事件池移除了,Chrome 是立即移除生效,第一次也不再执行 fn8,而 IE 是当前本次不生效,下一次点击才生效,第一次还是会执行 fn8

IE 低版本的 DOM 2 级事件绑定

  • ele.attachEvent('onclick', function(){})

  • 通过 detachEvent 解除绑定

oDiv.attachEvent('onclick', function(){});

在 IE 中,这个方法与 DOM 2 级方法的主要区别:

  • 只有两个参数,第一个参数必须加上 on

  • 不会处理重复绑定,都会放进事件池中

  • 在 DOM 2 级中 this -> 当前元素

  • 这个方法中 this -> window

  • 添加的多个事件处理程序,不是按添加的顺序触发的,而是乱序执行的

IE 低版本浏览器出现的所有的问题都是由于本身自带的事件池机制不完整导致的。

5. 跨浏览器兼容

使用能力检测

function addHandler(ele, type, handler) {
  if (ele.addEventListener) {
    ele.addEventListener(type, handler, false);
  } else if (ele.attachEvent) {
    ele.attachEvent('on'+type, handler);
  } else {
    ele['on'+type] = handler;
  }
}

function removeHandler(ele, type, handler) {
  if (ele.removeEventListener) {
    ele.removeEventListener(type, handler, false);
  } else if (ele.detachEvent) {
    ele.detachEvent('on'+type, handler);
  } else {
    ele['on'+type] = null;
  }
}