事件

本章内容:理解事件流、使用事件处理程序、不同的事件类型

JavaScript与HTML之间的交互是通过事件实现的。事件,就是文档或者浏览器窗口发生的一些特定的交互瞬间。可以使用侦听器(或处理程序)来预定事件,以便事件发生时执行相应的代码。这种在传统软件工程中被称为观察员模式,支持页面的行为与页面的外观之间的松散耦合。

一、事件流

当浏览器发展到第四代的时候(IE4及 Netscape Communicator 4),浏览器开发团队遇到了一个很有意思的问题:页面的哪一部分会拥有某个特定的事件?要明白这个问题问的是什么,可以想象画在一张纸上的一组同心圆。如果你把手指放在圆心上,那么你的手指指向的不是一个圆,而是纸上的所有圆。同样:当你单机某个按钮,他们都认为单机事件不仅仅发生在按钮上,在单机按钮的同时,你也单机了按钮的容器元素,甚至单机了整个页面。
事件流描述的是从页面接收事件的顺序。但有意思的是,IE 和 Netscape 开发团队提出了差不多是完全相反的事件流概念。IE的事件流是事件冒泡流,而Netscape Communicator 的事件流是事件捕获流。

1.1、事件冒泡

IE的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点(文档)。



  
    Event Bubbling
  
  
    
click me

如果单机了页面的

元素,那么这个click事件会按照如下顺序传播:
(1):

(2):
(3):
(4):

下图展示了事件冒泡的过程


事件冒泡

所有现代浏览器都支持事件冒泡,在具体实现上还是有一些差别。IE5.5及更早版本中的事件冒泡会跳过元素(直接到 Document)。IE9、Firefox、Chrome、Safari则将事件一直冒泡到window对象

1.2、事件捕获

Netscape Communicator团队提出的另一种事件流叫做事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。单机

元素就会以下列顺序触发click事件。
(1):
(2):
(3):
(4):

下图展示事件捕获的过程

事件捕获

IE9、Safari、Chrome、Opera、Firefox目前都支持这种事件流模型
由于有点老版本的浏览器不支持,因此较少使用事件捕获。建议放心使用事件冒泡,在有特殊需要时再使用事件捕获

1.3、DOM事件流

"DOM2级事件" 规定的事件流包括三个阶段:事件捕获阶段、目标阶段、事件冒泡阶段。首先发生的是事件捕获阶段,为截取事件提供了机会;然后是实际的目标接收到事件;最后是冒泡阶段,可以在这个阶段对事件做出响应。
以前面的HTML代码为例,当点击

元素后的触发事件顺序

触发顺序

"目标阶段",事件在

上发生,会在事件处理程序中被看成冒泡阶段的一部分

"DOM2级事件" 规范明确要求捕获阶段不会涉及事件目标,但IE9、Safari、Chrome、Firefox、Opera9.5级更高版本都会在捕获阶段触发事件对象上的事件。结构,就是有两个机会在目标对象上面操作事件

二、事件处理程序

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

2.1、HTML事件处理程序

某个元素支持的每种事件,都可以使用一个与相应事件处理程序相同的HTML特性指定。
例如:在按钮被单击时执行一些JavaScript


在 HTML 中定义的事件,也可以调用在页面其他地方定义的脚本



事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码

这样指定事件处理程序具有一些独到之处。首先,这样会创建一个封装着元素属性值的函数。这个函数中有一个全局变量 event,也就是事件对象。



通过 event 变量,可以直接访问事件对象,你不同自己定义它,也不用从函数的参数列表中读取。在这个函数内部,this 值等于事件的目标元素,例如:



关于这个动态创建的函数,另一个有意思的地方是它扩展作用域的方式。在这个函数内部,可以向访问局部变量一样访问 document及改元素本身的成员。这个函数使用 with 像下面这样扩展作用域:

function() {
  with(document) {
    with(this) {
      // 元素属性值
    }
  }
}

如下一来,事件处理程序要访问自己的属性就简单多了



实际上,这样的扩展作用域的方式,无法就是想让事件处理程序无需引用表单元素就能访问其它表单字段,例如:

不过,在HTML 中指定事件处理程序有几个缺点

  1. 存在一个时差问题。用户可能在HTML元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。
  2. 这样扩展事件处理程序的作用域链在不同浏览器中会导致不同的结果。不同 JavaScript引擎准许的标识符解析规则略有差异,很可能会访问非限定对象成员时出错。
  3. HTML 与 JavaScript代码紧密耦合

2.2、DOM0 级事件处理程序

通过JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。一直沿用至今,具有 简单、跨浏览器的优势。

每个元素都有自己的事件处理程序属性,这些属性通常全部小写,例如onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序

var btn = document.getElementById('myBtn')
btn.onclick = function() {
  alert('clicked')
}

使用DOM0 级方法指定的事件处理程序被认为是元素的方法,因此,这时候的事件处理程序是在 元素的作用域中运行的;换句话说,程序中的this引用当前元素

btn.onclick = function() {
  alert(this.id) // myBtn
}

以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理
也可以删除通过DOM0 级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设为null即可。

btn.onclick = null // 删除事件处理程序

2.3、DOM2 级事件处理程序

"DOM2级事件" 定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()removeEventListener()。它们接受三个参数:

  1. 要处理的事件名
  2. 作为事件处理程序的函数
  3. 一个布尔
    • true,表示在捕获阶段调用事件处理程序;
    • false,表示在冒泡阶段调用事件处理程序;

例如为按钮添加 click 事件

var btn = document.getElmentById('myBtn')
btn.addEventListener('click', function() {
  alert(this.id)
}, false)

使用 DOM2 级方法添加事件处理程序的主要好处是可以为一个事件添加多个事件处理程序

var btn = document.getElementById('myBtn')
btn.addEventListener('click', function() {
  alert(this.id)
}, false)

btn.addEventListener('click', function() {
  alert('Nice to see you')
}, false)

这两个事件会依次触发,myBtn Nice to see you


通过 addEventListener() 注册的事件处理程序 只能使用 removeEventListener() 来移除;移除时传入的参数与添加处理程序时传入的参数相同。这意味着通过 addEventListener() 添加的匿名函数将无法移除

btn.addEventListener('click', function() {
  alert('Hello')
}, false)

btn.removeEventListener('click', function() { // 无效
  alert('Hello')
}, false)

虽然函数看上去是一致的,但实际上两个函数不是同一个。
移除事件必须传入相同的 事件处理程序函数。如下:

var handle = function() {
  alert('Hello')
}

btn.addEventListener('click', handle, false)
btn.removeEventListener('click', handle, false)

大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度的兼容各种浏览器。如果不是特别需要,不建议在事件捕获阶段注册事件处理程序

2.4、IE事件处理程序

IE(IE11废弃了下面的两个方法支持DOM标准,IE11以下都支持下面的两个方法,IE8及以下不支持DOM标准中提供的两个方法)实现了与DOM中类似的两个方法:attachEvent()、deleteEvent()。这两个方法接受相同的两个参数:

  1. 事件处理名称
  2. 事件处理函数。
    由于IE8及更早版本只支持事件冒泡,所以通过 attachEvent() 添加的事件处理程序都会被添加到冒泡阶段
    使用 attachEvent() 为按钮添加一个事件处理程序:
btn.attachEvent('onclick', function() {
  alert('See you')
})

在 IE 中使用 attachEvent() 与使用 DOM0 级方法的主要区别在于事件处理程序的作用域
在使用DOM0 级方法的情况下,事件处理程序会在其所属元素的作用域内运行;
attachEvent() 方法的情况下,事件处理程序会在全局作用域中运行,因此 this 等于 window。

btn.attachEvent('onclick', function() {
  alert(this === window) // true
})

类是的,attachEvent() 也可以为同一个事件 添加多个事件处理程序。

btn.attachEvent('onclick', function() {
  alert('clicked')
})

btn.attachEvent('onclick', function() {
  alert('****')
})

不过与DOM方法不同的是,这些事件处理程序不是以添加它们的顺序执行,而是以相反的顺序被触发。以上会先看到 **** 然后看到 clicked

使用 attachEvent() 添加的事件可以通过 detachEvent() 来移除,条件是必须提供相同的参数(与DOM一样,不能使用匿名函数)

var handle = function() {
  alert('Hello')
}
btn.attachEvent('onclick', handle)
btn.detachEvent('onclick', handle)

2.5、跨浏览器的事件处理程序

根据 DOM 和 IE 封装兼容函数,如下

var EventUtil = {
  addHandler: function(element, type, handler) {
    if (element.addEventListener) { // 支持 addEventListener
      element.addEventListener(type, handler, false)
    }else if (element.attachEvent) { // IE
      element.attachEvent('on' + type, handler)
    } else { // 都不支持的情况下,默认使用 DOM0 级
      element['on' + type] = handler
    }
  },
  removeHandler: function(element, type, handler) {
    if (element.removeEventListener) { // 支持 DOM
      element.removeEventListener(type, handler, false)
    } else if (element.detachEvent) { // IE
      element.detachEvent('on' + type, handler)
    } else {
      element['on' + type] = null
    }
  }
}

可以像下面这样使用 EventUtil 对象:

var btn = document.getElementById('myBtn')
var handler = function() {
  alert('Hello')
}
// 注册事件
EventUtil.addHandler(btn, 'click', handler)
// 移除事件
EventUtil.removeHandler(btn, 'click', handler)

addHandler() 和 removeHandler() 并没有考虑到所有的浏览器问题,例如在IE中的作用域问题。此外还需要注意,DOM0 级对每个事件只支持一个事件处理程序。不过只支持DOM0 级的浏览器已经几乎灭绝了,所以不是上面问题

三、事件对象

触发DOM上的某个事件时,会产生一个事件对象 event,这个对象中包含着所有与事件相关的信息。

3.1、DOM中的事件对象

兼容DOM的浏览器会将一个 event 对象传入到事件处理程序中。

var btn = document.getElementById('myBtn')
btn.onclick = function(event) {
  alert(event.type) // click
}
btn.addEventListener('click', function(event) {
  alert(event.type) // click
}, false)

在通过 HTML 特性指定事件处理程序时,变量 event 中保存着 event 对象。


event 对象包含于创建它的特定事件有关的属性和方法,所有的事件都会有下表列出的成员。

属性/方法 类型 读写 说明
bubbles Boolean 只读 表明事件是否冒泡
cancelable Boolean 只读 表明是否可以取消事件的默认行为
currentTarget Element 只读 其他事件处理程序当前正在处理事件的那个元素
defaultPrevented Boolean 只读 为true表示已经调用了 preventDefault()
detail Integer 只读 与事件相关的细节信息
eventPhase Integer 只读 调用事件处理程序的阶段:1表示捕获阶段,2表示处于目标阶段,3表示处于冒泡阶段
preventDefault() Function 只读 取消事件的默认行为。如果cancelable是true,则可以使用这个方法
stopImmediatePropagation() Function 只读 取下事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用
stopPropagation() Function 只读 取消事件的进一步捕获或冒泡。如果bubbles 为 true,则可以使用这个方法
target Element 只读 事件的目标
trusted Boolean 只读 为true表示事件是浏览器生成的。为false表示事件是由开发人员通过JavaScript创建的
type String 只读 被触发的事件类型
view Abstractview 只读 与事件关联的抽象视图。等同于发生事件的window对象

在事件处理程序内部,对象 this 始终等于 currentTarget 的值,而 target 则只包含事件的实际目标。如果直接将事件处理程序指定给了目标元素,则 this、currentTarget、target包含相同的值。

var btn = document.getElementById('btn')
btn.onclick = function (event) {
  console.log(event.currentTarget === this) // true
  console.log(event.target === this) // true
}

在需要通过一个函数处理多个事件时,可以利用 type 属性实现

var btn = document.getElementById('myBtn')
var handler = function(event) {
  switch (event.type) {
    case 'click':
      alert('clicked')
      break;
    case 'mouseover':
      event.target.style.backgroundColor = 'red'
      break;
    case 'mouseout':
      event.target.style.backgroundColor = ''
      break;
  }
}

btn.onclick = handler
btn.onmouseover = handler
btn.onmouseout = handler

要阻止特定事件的默认行为,可以使用 preventDefault() 方法。例如,取消链接的默认行为

var link = document.getElementsByTagName('a')[0]
link.onclick = function(event) {
  event.preventDefault() // 取消默认行为

  // todo
}

只有 cancelable 属性设置为 true的事件,才可以使用preventDefault() 来取消其默认行为。


stopPropagation() 方法用于立即停止事件在 DOM 层次中的传播,即取消进一步的事件捕获或冒泡。

var btn = document.getElementById('myBtn')
btn.onclick = function(event) {
  alert('clicked')
  event.stopPropagation() // 阻止冒泡
}
document.body.onclick = function(event) {
  alert('Body clicked')
}

当点击 btn 的时候,只会触发 btn的点击事件,而不会通过冒泡同时去触发 body 的click事件


事件对象的 eventPhase属性,可以用来确定事件当前正位于事件流的哪个阶段。

  • 捕获阶段调用的事件处理程序,eventPhase 等于 1
  • 事件处理程序处于目标身上,则 event-Phase 等于 2
  • 冒泡阶段调用的事件处理程序,eventPhase 等于 3

需要注意的是,尽管”处于目标阶段“发生在冒泡阶段,但 eventPhase 仍然会等于2

var btn = document.getElementById('myBtn')
btn.onclick = function (event) {
  alert (event.eventPhase) // 2
}
document.body.addEventListener('click', function(event) {
  alert(event.eventPhase) // 1
}, true) // 设置为 事件捕获
document.body.onclick = function (event) {
  alert(event.eventPhase)
}

3.2、IE中的事件对象(IE8-)

与访问DOM中的event 对象不同,要访问IE中的 event 对象有几种不同的方式,取决于指定事件处理程序的方法。

使用DOM0级方法添加事件处理程序时,event 对象作为window 对象的一个属性存在。

var btn = document.getElementById(myBtn')
btn.onclick = function() {
  var event = window.event
  alert(event.type)
}

如果事件处理程序是使用 attachEvent() 添加的,那么就会有一个 event 对象作为参数被传入事件处理程序函数中。

var btn = document.getElementById('myBtn')
btn.attachEvent('onclick', function(event) {
  alert(event.type) // click
})

如果是通过 HTML 特性指定的事件处理程序,那么还可以通过一个名叫 event的变量来访问 event 对象


IE 的 event 对象同样也包含以创建它的事件相关的属性和方法。下列是所有事件对象都会包含的属性和方法

属性/方法 类型 读/写 说明
cancelBubble Boolean 读/写 默认值为false,但将其设置为 true 就可以取消事件冒泡(与DOM中的 stopPropagation() 方法的作用相同 )
returnValue Boolean 读/写 默认值为 true,但将其设置为 false 就可以取消事件的默认行为(与 DOM 中的 preventDefault() 方法的作用相同 )
srcElement Element 只读 事件的目标(与DOM中的target属性相同)
type String 只读 被触发的事件的类型

因为事件处理程序的作用域是根据指定它的方式来确定的,所以不能认为 this 会始终等于事件目标。故而,最好还是使用 event.srcElement 比较保险。

var btn = document.getElement('myBtn')
btn.onclick = function() {
  alert(window.event.srcElement === this) // true
}

btn.attachEvent('onclick', function(event) {
  alert(event.srcElement === this) // false
})

returnValue 属性相当于 DOM 中的 preventDefault() 方法,他们的作用域都是取消给定事件的默认行为。只要将 returnValue 设置为 false,就可以阻止默认行为。

var link = document.getElementsByTagName('a')[0]
link.onclick = function() {
  window.event.returnValue = false
}

cancelBubble 属性 与 DOM 中的 stopPropagation() 方法作用相同,都是用来停止事件冒泡的。由于IE不支持事件捕获,因而只能取消事件冒泡;但stopPropagation() 可以同时取消 事件冒泡和捕获

var btn = document.getElementById('myBtn')
btn.onclick = function() {
  alert('clicked')
  window.event.cancelBubble = true
}
document.body.onclick = function() {
  alert('Body clicked')
}

3.3、跨浏览器的事件对象

根据上面介绍的属性,对前面的 EventUtil 对象加以增强

var EventUtil = {
   // 注册事件
  addHandler: function(element, type, handler) {
    if (element.addEventListener) { // 支持 addEventListener
      element.addEventListener(type, handler, false)
    }else if (element.attachEvent) { // IE
      element.attachEvent('on' + type, handler)
    } else { // 都不支持的情况下,默认使用 DOM0 级
      element['on' + type] = handler
    }
  },
  // 移除事件
  removeHandler: function(element, type, handler) {
    if (element.removeEventListener) { // 支持 DOM
      element.removeEventListener(type, handler, false)
    } else if (element.detachEvent) { // IE
      element.detachEvent('on' + type, handler)
    } else {
      element['on' + type] = null
    }
  },

  // 获取 event 对象
  getEvent: function(event) {
    return event ? event : window.event
  },

  // 获取目标元素
  getTarget: function(event) {
    return event.target || event.srcElement
  },

  // 阻止默认行为
  preventDefault: function(event) {
    if (event.preventDefault) {
      event.preventDefault()
    } else {
      event.returnValue = false
    }
  },

  // 阻止冒泡
  stopPropagation: function(event) {
    if (event.stopPropagation) {
      event.stopPropagation()
    } else {
      event.cancelBubble = true
    }
  }
}

使用



  点我



当点击 a 标签的时候,不会触发 body 的点击事件。只会输出 'clicked' 及 'A'(标签名)

四、事件类型

“DOM3级事件” 规定了以下几类事件

  • UI(User Interface,用户界面)事件,当用户与页面上的元素交互时触发
  • 焦点事件,当元素获得或失去焦点时触发
  • 鼠标事件,当用户通过鼠标在页面上执行操作时触发
  • 滚轮事件,当使用鼠标滚轮(或类似设备)时触发
  • 文本事件,当在文档中输入文本时触发
  • 键盘事件,当用户通过键盘在页面上执行操作时触发
  • 合成事件,当为IME(Input Method Editor,输入法编辑器)输入字符时触发
  • 变动(mutation)事件,当底层DOM结构发送变化时触发

除了这几类事件之外,HTML5也定义了一组事件,有些浏览器还会在DOM和BOM中实现其他专有事件,这些专有的事件一般都是根据开发人员需求定制的,没有什么规范,因此不同浏览器的实现有可能不一致。

DOM3 级事件模块在 DOM2级事件模块基础上重新定义了这些事件,也添加了一些新事件。包括IE9在内的所有主流浏览器都支持DOM2 级事件。IE9也支持DOM3级事件

4.1、UI 事件

UI事件指的是那么不一定于用户操作有关的事件

  • load:当页面完全加载后 window 上面触发,当所有框架都加载完毕时在框架集上面触发,当图像加载完毕时在元素上触发,或者当嵌入的内容加载完毕时在元素上触发
  • unload:当页面完全卸载后再 window 上面触发,当所有框架都卸载后再框架集上面触发,或者当嵌入的内容卸载完毕后在上面触发。
  • abort:当用户停止下载过程时,如果嵌入的页面没有加载完,则在元素上面触发
  • error:当发生 JavaScript 错误时在 window 上面触发,当无法加载图片时在 元素上面触发,当无法加载嵌入内容时在 元素上面触发,或者当有一或多个框架无法加载时在框架集上面触发。
  • select:当用户选择文本框(