事件

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

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

一、事件流

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

1.1、事件冒泡

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

<!DOCTYPE html>
<html>
  <head>
    <title>Event Bubbling</title>
  </head>
  <body>
    <div id="myDiv">click me</div>
  </body>
</html>

如果单机了页面的<div>元素,那么这个click事件会按照如下顺序传播:
(1):<div>
(2):<body>
(3):<html>
(4):<document>

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


事件冒泡

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

1.2、事件捕获

Netscape Communicator团队提出的另一种事件流叫做事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。单机<div>元素就会以下列顺序触发click事件。
(1):<document>
(2):<html>
(3):<body>
(4):<div>

下图展示事件捕获的过程

事件捕获

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

1.3、DOM事件流

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

触发顺序

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

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

二、事件处理程序

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

2.1、HTML事件处理程序

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

<input type="button" value="Click me" onclick="alert('clicked')" />

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

<script type="text/javascript">
  function showMessage() {
    alert('Hello World!')
  }
</script>
<input type="button" value="click me" onclick="showMessage()">

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

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

<!-- 输出事件类型,当前为 click -->
<input type="button" value="click me" onclick="alert(event.type)" />

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

<!-- 输出当前 input 的value,这里为 "click me" -->
<input type="button" value="click me" onclick="alert(this.value)" />

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

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

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

<!-- 输出 "click me" -->
<input type="button" value="click me" onclick="alert(value)" />

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

<form method="post">
  <input type="text" name="username" value="" />
  <!-- 输出第一个文本框中的value值 -->
  <input type="button" value="Echo Username" onclick="alert(username.value)" />
</form>

不过,在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 对象。

<input type="button" value="Click me" onclick="alert(event.type)" />

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 对象

<input type="button" value="Click me" onclick="alert(event.type)" />

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
    }
  }
}

使用

<body>

  <a href="www.baidu.com" id="link">点我</a>

<script src="./EventUtil.js"></script>
<script>

var link = document.getElementById('link')

var handler = function() {
  var event = EventUtil.getEvent() // 获取兼容的 event 对象

  EventUtil.preventDefault(event)// 阻止默认行为

  var getTarget = EventUtil.getTarget(event) // 触发事件的元素
  EventUtil.stopPropagation(event)// 阻止冒泡

  console.log('clicked') // clicked
  console.log(getTarget.tagName) // A
}

EventUtil.addHandler(link, 'click', handler)

document.body.onclick = function() {
  alert('clicked')
}
</script>

当点击 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 上面触发,当所有框架都加载完毕时在框架集上面触发,当图像加载完毕时在<img>元素上触发,或者当嵌入的内容加载完毕时在<object>元素上触发
  • unload:当页面完全卸载后再 window 上面触发,当所有框架都卸载后再框架集上面触发,或者当嵌入的内容卸载完毕后在<object>上面触发。
  • abort:当用户停止下载过程时,如果嵌入的页面没有加载完,则在<object>元素上面触发
  • error:当发生 JavaScript 错误时在 window 上面触发,当无法加载图片时在 <img> 元素上面触发,当无法加载嵌入内容时在 <object> 元素上面触发,或者当有一或多个框架无法加载时在框架集上面触发。
  • select:当用户选择文本框(<input> 或 <textarea>)中的一或多个字符时触发。
  • resize:当窗口或框架的大小变化时在window或框架上面触发
  • scroll:当用户滚动带滚动条的元素中的内容时,在该元素上面触发。<body>元素中包含所有加载页面的滚动条。

检测 浏览器是否支持 DOM2级 或 DOM3级事件

var isSupportedDOM2Event = document.implementation.hasFeature('HTMLEvents', '2.0')
var isSupportedDOM3Event = document.implementation.hasFeature('UIEvent', '3.0') 
4.1.1、load事件

当页面加载完成后(包括所有图像、JavaScript文本、CSS文件等外部资源),就会触发window 上面的 load 事件。

第一种定义方式是使用如下所示的JavaScript代码

EventUtil.addHandler(window, 'load', function() {
  alert('loaded!')
})

第二种指定 方式是通过为<body>元素添加一个 onload 特性

<body onload="alert('ok')">
</body>

实际上,后面这种方法只是为了保证向后兼容的一种权宜之计,但所有浏览器都能很好地支持这种方式,当然建议使用 JavaScript 的方式。


图像上面也可以触发 load 事件,无论是在 DOM种的图像元素还是在HTML中的图像元素。
在HTML中为任何图像指定onload事件处理程序

<img src="smile.gif" onload="alert('Image loaded!')" />

使用 JavaScript 实现

var img = document.getElementsByTagName('img')[0]
EventUtil.addHandler(img, 'load', function(event) {
  event = EventUtil.getEvent(event)
  alert(EventUtil.getTarget(event).src)
})

在创建新的<img>元素时,可以为其指定一个事件处理程序,以便图像加载完毕后给出提示。重要的是要在指定src属性之前先指定事件


EventUtil.addHandler(window, 'load', function() {
  var image = document.createElement('img')
  EventUtil.addHandler(image, 'load', function(event) {
    event = EventUtil.getEvent(event)
    alert(EventUtil.getTarget(event).src)
  })

  document.body.appendChild(image)
  image.src='smile.gif'
})

新图像元素不一样要从添加到文档后才开始下载,只要设置了 src 属性 就会开始下载。

也可以通过使用 DOM0 级的 Image 对象实现。可以像使用<img> 元素一样使用 Image 对象,只不过无法将其添加到DOM树中

EventUtil.addHandler(window, 'load', function() {
  var image = new Image()
  EventUtil.addHandler(image, 'load', function(event) {
    alert('Image loaded!')
  })
  image.src = 'smile.gif'
})

还有一些元素也以非标准的方式支持 load 事件。在IE9、Firefox、Opera、Chrome、Safari3+及更高的版本中, <script> 元素和 <link> 元素也会触发load事件,将它们添加到文档并且设置了 src/href 属性后,就会开始下载文件。

EventUtil.addHandler(window, 'load', function() {
  // script 元素
  var script = document.createElement('script')
  EventUtil.addHandler(script, 'load', function(event) {
    console.log('js loaded!')
  })
  script.src = 'example.js'
  document.body.appendChild(script)

  // link 元素
  var link = document.createElement('link')
  link.type = 'text/css'
  link.rel = 'stylesheet'
  EventUtil.addHandler(link, 'load', function(event) {
    console.log('css loaded!')
  })
  link.href = 'example.css'
  document.getElementsByTagName('head')[0].appendChild(link)
})
4.1.2、unload 事件

与 load 事件对于的是 unload 事件,这个事件在文档被完全卸载后触发。只要用户从一个页面切换到另一个页面,就会发生 unload 事件。而利用这个事件最多的情况就是清除引用,以避免内存泄漏。

EventUtil.addHandler(window, 'unload', function(event) {
  alert('unload')
})

与 load事件 一样,也可以 通过为 <body> 元素 添加 onunload 特性

需要注意的是:onload 事件是在一切都被卸载之后才触发,那么在页面加载后存在的那些对象,此时就不一定存在了。此时,操作DOM节点或者元素的样式就会导致错误

4.1.3 resize 事件

当浏览器窗口被调到一个新的高度或宽度时,就会触发 resize 事件。这个事件在 window 上面触发,因此可以通过 JavaScript 或者 <body> 元素中 的 onresize 特性来制定事件处理程序。

EventUtil.addHandler(window, 'resize', function(event) {
  console.log('onresize')
})

resize事件 可能会被频繁执行注意性能

4.1.4、scroll 事件

scroll 事件是在 window 对象上发生的,但它实际表示的则是页面中相应元素的变化。

EventUtil.addHandler(window, 'scroll', function(event) {
    console.log( document.body ? document.body.scrollTop : document.documentElement.scrollTop)
})

与resize 事件象实,scroll 事件也会在文档被滚动期间重复被触发,所以有必要尽量保持事件处理程序的代码简单

4.2 焦点事件

焦点事件会在页面元素获得或失去焦点时触发。利用这些事件并于 document.hasFocus() 方法及 document.activeElement 属性配合使用,可以知晓用户在页面上的行踪

  • blur:在元素市区焦点时触发。这个事件不会冒泡;所以浏览器都支持
  • focus:再元素获得焦点时触发。这个事件不会冒泡;所以浏览器都支持它
  • focusin:再元素获得焦点时触发。这个事件与HTML事件focus扽火箭,但它毛冒泡。支持这个事件的浏览器有 IE5.5+、Safari5.1+、Opera11.5、Chrome、Firefox52+
  • focusout:在元素市区焦点时触发。这个事件时 HTML事件 blur的通用版。支持冒泡。支持这个事件的浏览器有 IE5.5+、Safari5.1+、Opera11.5、Chrome、Firefox52+

当焦点聪慧页面的一个元素移动到另一个元素,会依次触发下列事件

  1. focusout 在失去焦点的元素上触发
  2. focusin 在获得焦点的元素上触发
  3. blur 在失去焦点的元素上触发
  4. focus 在获得焦点的元素上触发

检测浏览器是否支持这些事件

var isSupported = document.implementation.hasFeature('FocusEvent', '3.0')

4.3、鼠标与滚轮事件

鼠标事件是 Web 开发中最常用的一类事件,毕竟鼠标还是最主要的定位设备。DOM3 级事件中定义了 9个鼠标事件。如下:

  • click:在用户单击主鼠标按钮(一般是左边的按钮)或者按下回车键时触发。这一点对确保易访问性很重要,意味着 onclick 事件处理程序既可以通过键盘也可以通过鼠标执行。
  • dbclick:在用户双击主鼠标按钮(一般是左边的按钮)时触发。从技术上说,这个事件并不是DOM2级事件规范中规定的,但鉴于它得到了广泛支持,所以DOM3级事件将其纳入了标准。
  • mousedown:在用户按下了任意鼠标按钮时触发。不能通过键盘触发这个事件。
  • mouseenter:在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。DOM2级事件并没有定义 这个事件,但DOM3级事件将它纳入了规范。IE、Firefox 9+、Opera支持这个事件。
  • mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。DOM2级事件并没有定义 这个事件,但DOM3级事件将它纳入了规范。IE、Firefox 9+、Opera支持这个事件。
  • mousemove:当鼠标指针在元素内部移动时重复的触发。不能通过键盘触发这个事件
  • mouseout:在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。又移入的另一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件。
  • mouseover:在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。不能通过键盘触发这个事件。
  • mouseup:在用户释放鼠标按钮时触发。不能通过键盘触发这个事件。

页面上的所有元素都支持鼠标事件。处理mouseenter 和 mouseleave,所有鼠标事件都会冒泡,也可以被取消,而取消鼠标事件将会影响浏览器的默认行为。取消鼠标事件的默认行为还会影响其他事件,因为鼠标事件与其他事件是密不可分的关系。

在同一个元素上,上诉事件的触发过程

  1. mousedown
  2. mouseup
  3. click
  4. mousedown
  5. mouseup
  6. click
  7. dbclick

click 和 dbclick 事件都会依赖于其他先行事件的触发;而 mousedown 和 mouseup 则不受其他事件的影响。IE8及之前有一个 bug,在双击事件中,会跳过第二个mousedown 和 click 事件,IE9修复了这个bug

检测浏览器是否支持上面所有的事件

var isSupported = document.implementation.hasFeature('MouseEvent', '3.0')

鼠标事件中还有一类滚轮事件,即滚轮事件,其实就是一个 mousewheel 事件。这个事件跟踪鼠标滚轮,类似于Mac的触控板。

4.3.1、客户区坐标位置

鼠标事件都是在浏览器视口中的特定位置上发生的。这个位置信息保存在事件对象的clientXclientY 属性中。它们的值表示事件发生时鼠标指针在视口中水平和垂直坐标

客户区坐标

可以使用类似下列代码获取鼠标事件的客户端坐标信息

var div = document.getElementById('myDiv')
EventUtil.addHandler(div, 'click', function(event) {
  event = EventUtil.getEvent(event)
  console.log(event.clientX, event.clientY)
})
4.3.2、页面坐标位置

通过事件对象的 pageXpageY 属性,表示鼠标光标在页面中的位置,因此坐标是从页面本身而非视口计算。

var div = document.getElementById('div')
EventUtil.addHandler(div, 'click', function(event) {
  event = EventUtil.getEvent(event)
  console.log(event.pageX, event.pageY)
})

在IE8及更早版本不支持事件对象上面的页面坐标,,不过使用客户区坐标和滚动信息可以计算出来。这时候需要用到document.body(混杂模式) 或 doucment.documentElement(标准模式)中的scrollLef 和 scrollTop属性

var div = document.getElementById('div')
EventUtil.addHandler(div, 'click', function(event) {
  event = EventUtil.getEvent(event)
  var pageX = event.pageX
  var pageY = event.pageY

  if (pageX === undefined) {
    pageX = event.clienX + (document.body.scrollLeft || document.documentElement.scrollLeft)
  }
  if (pageY === undefined) {
    pageY = event.clientY + (document.body.scrollTop || document.documentElement.scrollTop)
  }

  console.log(pageX, pageY)
})

4.3.3、屏幕坐标位置相对于电脑屏幕的位置。通过 screenXscreenY

属性就可以确定鼠标事件发生时鼠标指针相对于整个屏幕的坐标信息。


屏幕坐标位置
var div = document.getElementById('div')
EventUtil.addHandler(div, 'click', function(event) {
  event = EventUtil.getEvent(event)
  console.log(event.screenX, event.screenY)
})
4.3.4 修改键

虽然鼠标事件主要是使用鼠标来触发的,但在按下鼠标时键盘上的牟谢建的状态也可以影响到所要采取的操作,这些键是Shift、Ctrl、Alt、Meta(windows键/Cmd键)。DOM为此规定了四个属性:shiftKey、ctrlKey、altKey、metaKey。这些属性中包含的都是布尔值,如果相应的键被按下了,则值为true,否则值为false。

var div = document.getElementById('div')
EventUtil.addHandler(div, 'click', function(event) {
  event = EventUtil.getEvent(event)
  var keys = new Array()
  
  if (event.shiftKey) { // shift
    keys.push('shift')
  }
  if(event.ctrlKey) { // ctrl
    keys.push('ctrl')
  }
  if(event.altKey) { // alt
    keys.push('alt')
  }
  if (event.metaKey) { // meta
    keys.push('meta')
  }

  console.log(keys.join(', '))
})

IE8及之前不支持 metaKey 属性

4.3.5、相关元素

在发生 mouseover 和 mouseout 事件时,还会涉及更多元素。这两个是事件都会涉及把鼠标指针从一个元素的边界之内移动到另一个元素的边界之内。

<body>
  <div id="myDiv" style="background-color: cyan; width:200px; height: 150px;"></div>
</body>

如上,如果鼠标指针一开始位于这个<div>元素上,然后移除出了这个元素,那么就会在 <div> 元素上触发 mouseout 事件,相关元素就是 <body> 元素。与此同时,<body>元素上面会触发 mouseover 事件,而相关元素变成了 <div>

DOM 通过 event 对象的 relatedTarget 属性提供了相关元素的信息。这个属性只对于 mouseover、mouseout事件才包含值:对于其他事件,这个属性的值 null。IE8之前版本不支持这个属性(IE9支持),在mouseover事件触发的时候,IE的 formElement 属性中保存了相关元素;在mouseout 事件触发时,IE的 toElement属性中保存着相关元素。
将相关元素的方法添加到 EventUtil 对象中

var EventUtil = {
  getRelatedTarget: function(event) {
    if (event.relatedTarget) {
      return event.relatedTarget
    } else if (event.toElement) {
      return event.toElement
    } else if (event.fromElement) {
      return event.fromElement
    } else {
      return null
    }
  }
}

可以像下面这样去使用 EventUtil.getRelatedTarget() 这个方法

var div = document.getElementById('div')
EventUtil.addHandler(div, 'mouseout', function(event) {
  event = EventUtil.getEvent(event)
  var target = EventUtil.getTarget(event)
  var relatedTarget = EventUtil.getRelatedTarget(event)

  console.log('Moused out of ' + target.tagName + ' to ' + relatedTarget.tagName)
})

4.3.6、鼠标事件

只有在注释表按钮被单击(或键盘回车键按下)时才会触发 click 事件,对于 mousedown 和 mouseup 事件来说,则在其 event 对象存在一个 button 属性,表示按下或释放的按钮。DOM 的 button 属性可能有 如下 3个值:

  • 0:表示主鼠标键(左键)
  • 1:表示中间的鼠标按钮(鼠标滚轮)
  • 2:表示次鼠标按钮(右键)

IE8及之前也提供了 与DOM button 有差异的 button 属性。

  • 0:表示没有按下按钮
  • 1:表示按下了主鼠标按钮。
  • 2:表示按下了次鼠标按钮。
  • 3:表示同时按下了主、次鼠标按钮
  • 4:表示按下了中间的鼠标按钮
  • 5:表示同时按下了主鼠标按钮和中间的鼠标按钮
  • 6:表示同时按下了次鼠标按钮和中间的鼠标按钮
  • 7:表示同时按下了三个鼠标按钮

DOM模型下的 button 属性比IE模型下的 button 属性更简单也更为实用,常见的做法就是将IE模型规范华为DOM方法,
由于单独使用能力监测无法确定差异(两种模型有同名的button属性),因此必须另辟蹊径,我们知道,支持DOM版鼠标事件的浏览器可以通过hasFeature()方法监测,所以可以再为 EventUtil 对象 添加如下的 getButton() 方法。

var EventUtil = {
  // 省略了其他代码
  getButton: function(event) {
    if (document.implementation.hasFeature('MouseEvents', '2.0')) { // 支持 Dom 版
      return event.button
    } else { // IE 低版本
      switch(event.button) {
        case 0:
        case 1:
        case 3:
        case 5:
        case 7:
          return 0
        case 2:
        case 6:
          return 2
        case 4:
          return 1
      }
    }
  }
}

使用该方法的实例:

var div = document.getElementById('div')
EventUtil.addHandler(div, 'mousedown', function(event) {
  event = EventUtil.getEvent(event)
  var button = EventUtil.getButton(event)
  console.log(button)
})

在<div> 元素中按下鼠标按钮时,会输出对应的数值

4.2.7、更多的事件信息

“DOM2级事件” 规范在event 对象中还提供了 detail 属性,用于给出有关事件更多的信息。对于鼠标事件来说,detail 中包含了一个数值,表示在给定位置上发生了多少次单击。在同一元素上相继地发生一次 mousedown 和 mouseup 事件后算作一次单击。detail 属性从1 开始计数,每次单击发生后都会递增。如果鼠标在 mousedown 和 mouseup 之间移动了位置,则detail 会被重置为0
IE 也通过下列属性为鼠标事件提供更多信息。

  • altLeft:布尔值,表示是否按下了Alt键。如果altleft 的值为true,则altKey 的值也为 true。
  • ctrlLeft:布尔值,表示是否按下了 Ctrl 键。如果 ctrlLeft 的值为 true,则 ctrlKey 的值也为 true
  • offsetX:光标相对于目标元素边界的X坐标
  • offsetY:光标相对于目标元素边界的Y坐标
  • shiftLeft:布尔值,表示是否按下了Shift键。如果shiftLeft的值为 true,则 shiftKey 的值也为 true。
    这些属性的用处并不大,原因一方面只是有IE支持他们,另一方面它们提供的信息要么 没有什么价值,要么可以通过其他方式计算得来。
4.3.8、鼠标滚轮事件

IE 6.0 首先实现了 mousewheel 事件。与 mousewheel事件对应的 event 对象除包含鼠标事件的所有标准信息外,还包含一个特性的wheelDelta属性。当向前滚动鼠标滚轮时,wheelDelta 是120的倍数;向后滚动鼠标滚轮时,wheelDelta 是 -120的倍数。

EventUtil.addHandler(document, 'mousewheel', function(event) {
  event = EventUtil.getEvent(event)
  console.log(event.wheelDelta)
})

多数情况下,只要知道鼠标滚轮滚动的方向就够了,而这通过检测 wheelDelta 的正负号就可以确定。


有一点需要注意:在 Opera 9.5 之前的版本中,wheelDelta 值的正负号是颠倒的。如果打算支持早期的 Opera 版本,就需要使用浏览器检测技术来确定实际的值,

// ...
var delta = (client.engine.opera && client.engine.opera < 9.5 ? -event.wheelDelta : event.wheelDelta)

上面用到的 client, 在前面的客户端检测中有提到,检测浏览器是不是早前的 Opera


Firefox 支持一个名为 DOMMouseScroll 的类似事件,也是在鼠标滚轮滚动时触发。与mouseWhell事件一样,包含与鼠标事件有关的所有特性。有关鼠标滚轮的信息则保存在 detail 属性中,当向前滚动鼠标滚轮时,这个属性的值是 -3 的倍数,当向后滚动鼠标滚轮时,这个属性的值是 3 的倍数

EventUtil.addHandler(document, 'DOMMouseScroll', function(event) {
  event = EventUtil.getEvent(event)
  console.log(event.detail)
})

测试后返回的 7(向下) 和 -7(向上)


要给出跨浏览器环境下的解决方案,第一步是创建一个能够取得鼠标滚轮增量值(delta)的方法。

  // 获取 鼠标滚轮 增量值 (delta)
  getWheelDelta: function(event) {
    var detail = 0
    if (event.wheelDelta) {
      detail = client.engine.opera && client.engine.opera < 9.5 ? -event.wheelDelta : event.wheelDelta
    } else {
      detail = -event.detail
    }

    return detail > 0 ? -1 : 1
  }

由于在实际测试时,两个属性给出的值飘忽不定,所以这里只返回 -1 或 1 编码滚动的方法即可,-1代表向 页面向下滚动, 1 代表页面向上滚动
使用

var mouseWheelHandler =  function(event) {
  event = EventUtil.getEvent(event)
  var delta = EventUtil.getWheelDelta(event)

  console.log(delta)
}
EventUtil.addHandler(document, 'DOMMouseScroll',mouseWheelHandler)
EventUtil.addHandler(document, 'mousewheel'mouseWheelHandler)

如上:在多个浏览器下,都会有一致的返回结果

4.3.9、触摸设备

iOS 和 Android 设备的实现非常特别,因为这些设备没有鼠标。在面向iPhone 和 iPad 中的 Safari 开发时,要记住以下几点

  • 不知此 dbclick 事件。双击浏览器窗口会放大画面,而且没有办法改变该行为
  • 轻击可单机元素会触发 mousemove 事件。如果此操作会导致内容变化,将不再有其他事件发生;如果屏幕没有因此变化,那么会依次发生 mousedown、mouseup、click事件。轻击不可单机元素不会触发任何事件。可单机元素是指那些单机可产生默认操作的元素(如链接),或者那些已经被指定了 onclick 事件处理程序的元素
  • mousemove 事件也会触发 mouseover 和 mouseout 事件
  • 两个手指放在屏幕上且页面随手指移动而滚动时会触发 mousewheel 和 scroll 事件
4.3.10、无障碍性问题

Web应用程序或网站要确保残疾人特别是那些使用屏幕阅读器的人能够访问,那么在使用鼠标事件时就要格外小心。通过键盘上的回车键可以触发 click 事件,但其它鼠标事件无法通过键盘来触发。为此,我们不建议使用 click 之外的其他鼠标事件来展示功能或引发代码执行。因为这样会给盲人或障碍用户造成极大不方便。以下是在使用鼠标事件时应该注意的几个易访问性问题。

  • 使用 click事件执行代码。有人指出通过 onmousedown 执行代码会让人绝对速度更快,对视力正常的人来说这是没错的。但是,在屏幕阅读器中,由于无法触发mousedown 事件,结构就会造成代码无法执行
  • 不要使用 onmouseover 向用户显示新的选项。原因同上,屏幕阅读器无法触发这个事件。如果确实非要通过这种方式来显示新选项,可以考虑添加显示相同信息的键盘快捷方式。
  • 不要使用 dbclick 执行重要操作。键盘无法触发这个事件。

遵照以上提示可以极大地提升残疾人在访问你的 Web 应用程序或网站时的 易访问性

4.4、键盘与文本事件

用户在使用键盘时会触发键盘事件。“DOM3级事件”为键盘事件制定了规范,IE9率先完全实现了该规范。

有3个键盘事件,简述如下

  • keydown:当用户按下键盘上的任意键时触发,而且如果按住不放的话,会重复触发此事件
  • keypress:当用户按下键盘上的字符键时触发,而且如果按住不放的话,会重复触发此事件。按下Esc 键也会触发这个事件。Safari 3.1之前的版本也会在用户按下非字符键时触发 keypress 事件。
  • keyup:当用户释放键盘上的键时触发

虽然所有元素都支持以上3个元素,但只有在用户通过文本框输入时才最常用到

此外还有一个文本事件:textInput。这个事件是对 keypress 的补充,用意是在将文本显示给用户之前更容易拦截文本。在文本插入文本框之前会触发 textInput 事件

如果用户按下一个字符键不放,就会重复触发 keydown 和 keypress 事件,直到用户松开改建为止。
如果按住这个非字符键不放,那么就会一直重复触发 keydow 事件

4.4.1、键码

在发生keydown、keyup事件时,event 对象的 keyCode 属性中会包含一个代码,与键盘上一个特定的键对应。DOM 和 IE 的 event对象都支持这个属性

var textBox = document.getElementById('myText')
EventUtil.addHandler(textbox, 'keydown', function(event) {
  event = EventUtil.getEvent(event)
  console.log(event.keyCode)
})

无论是 keydown 或 keyup 事件都会存在一些特殊情况。在Firefox 和 Opera 中,按分号键时 keyCode 值为 59,也就是 ASCII 中分号的编码;但IE 和 Safari 返回 186,即键盘中按键的键码。

4.4.2、字符编码

在所有浏览器中,按下能够插入或删除字符的键都会触发 keypress 事件;按下其他键能否发次事件因浏览器而异。
IE9、Firefox、Chrome、Safari 的 event 对象都支持一个 charCode 属性,这个属性只有在发生 keypress 事件时才包含值,这个值是按下的那个键所代表字符的 ASCII 编码。此时的 keyCode 通常等于 0 或者 也可能等于所按键的键码。IE8及之前版本和 Opera 则是在 KeyCode 中保存字符的 ASCII 编码。
以跨浏览器的方式取得字符编码

var EventUtil = {
  // ...
  getCharCode: function(event) {
    if (typeof event.charCode == 'number') {
      return event.charCode
    } else {
      return event.keyCode
    }
  }
}

使用这个方法

var textbox = document.getElementById('myText')
EventUtil.addHandler(textbox, 'keypress', function(event) {
  event = EventUtil.getEvent(event)
  console.log(event.keyCode)
  console.log(event.charCode)
})

在取得了字符编码之后,就可以使用 String.fromCharCode() 将其转换成实际的字符。

4.4.3、DOM3 级变化

尽管所有浏览器都实现了某种形式的键盘事件,DOM3级 事件做出了一些改变。比如,DOM3级事件中的键盘事件,不在包含 charCode 属性,而是包含两个新属性:key 和 code(已废弃)

key 属性是为了取代 keyCode 而新增的,它的值是一个字符串。在线下非字符键时,key的值是相应键的名

由于跨浏览器问题,目前不是推荐使用 key(兼容问题)、keyIdentifier(已弃用)、char(已弃用),在MDN上同时提到 keyCode 也不推荐使用,并给出了如下的推荐代码

window.addEventListener("keydown", function (event) {
  if (event.defaultPrevented) { // 检测是否设置了 preventDefault()
    return; // Should do nothing if the default action has been cancelled
  }

  var handled = false;
  if (event.key !== undefined) { // 支持 key
    // Handle the event with KeyboardEvent.key and set handled true.
  } else if (event.keyCode !== undefined) { // 支持 keyCode
    // Handle the event with KeyboardEvent.keyCode and set handled true.
  }

  if (handled) { // 都不支持
    // Suppress "double action" if event handled
    event.preventDefault();
  }
}, true);

DOM3级事件还添加了 location 属性,这是一个数值,表示按下了什么位置上的键

  • 0:默认键盘
  • 1:表示左侧位置(例如左位的Alt键)
  • 2:表示右侧位置(例如右侧的Shift键)
  • 3:表示数字小键盘
  • 4:表示移动设备键盘(也就是虚拟键盘)
  • 5:表示手柄

IE9支持这个属性。Safari 和 Chrome 支持名为 keyLocation 的等价属性,但有bug——值始终是 0,除非按下了数字键盘(3);否则,不会是1、2、4、5

var textbox = document.getElementById('myText')
EventUtil.addHandler(textbox, 'keypress', function(event) {
  event = EventUtil.getEvent(event)
  var loc = (typeof event.location === 'number') ? event.location : event.keyLocation
  if (typeof loc === 'number') {
    console.log(loc)
  }
})

与 key 属性一样,支持 location的浏览器也不多,所以在跨浏览器开发中不推荐使用。


getModifierState()方法。这个方法接收一个参数,即等于Shift、Control、AltGraph、Meta的字符串,表示要检测的修改键。如果指定的修改键是活动的(也就是处于被按下的状态),这个方法就会返回true,否则返回false

var textbox = document.getElementById('myText')
EventUtil.addHandler(textbox, 'keypress', function(event) {
  event = EventUtil.getEvent(event)
  if (event.getModifierState) {
    console.log(event.getModifierState('Shift'))
  }
})

实际上,通过 event 对象的 shiftKey、altKey、ctrlKey、metaKey属性以及可以取得类似的属性了。

4.4.4、textInput 事件

“DOM3级事件” 规范中引入了一个新事件,名叫 textInput。当用户在可编辑区域中输入字符时,就会触发这个事件。这个事件与 keypress 之间有如下区别。

  1. 任何可以获得焦点的元素都可以触发 keypress 事件,但只有可编辑区域才能触发 textInput 事件。
  2. textInput 事件只会在用户按下能够输入实际支付的键时才会被触发,而 keypress 事件则在按下那么能够影响文本显示的键也会触发

由于 textInput 事件主要考虑的是字符,因此它的 event 对象中还包含一个 data 属性,这个属性的值就是用户输入的字符。

var textbox = document.getElementById('myText')
EventUtil.addHandler(textbox, 'textInput', function(event) {
  event = EventUtil.getEvent(event)
  console.log(event.data)
})

event 对象上还有一个属性,叫 inputMethod(查不到,应该是废弃了),表示把文本输入到文本框中的方式

4.4.5、设备中的键盘事件

任天堂Wii 会在用户按下 Wii 遥控器上的按键时触发键盘事件。尽管没有办法访问 Wii遥控器中所有案件,但还是有一些键可以触发事件。

当用户按下十字键盘(键码为175~178)、减号(170)、加号(174)、1(172)、2(173)键时就会触发键盘事件。但没有办法得知用户是否按下了电源开发、A、B或主页键

4.5、复合事件

复合事件(composition event)是DOM3级事件中添加的一类事件,用于处理IME的输入序列IME(Input Method Editor,输入法编辑器)可以让用户输入在物理键盘上找不到的字符。

有以下三种复合事件

  • compositionstart:在IME的文本复合系统开发时触发,表示要开始输入了
  • compositionupdate:在向输入字段中插入新字符时触发
  • compositionend:在IME的文本复合系统关闭时触发,表示返回正常键盘输入状态

复合事件与文本事件在很多方法都很相似。在触发复合事件时,目标是接收文本的输入字段。但它比文本事件的事件对象多一个属性data,其中包含以下几个值中的一个:

  • 如果在 compositionstart 事件发生时访问,包含正在编辑的文本(例如,已经选中的需要马上替换的文本)
  • 如果在 compositionupdate 事件发生时访问,包含正插入的新字符
  • 如果在 compositionend 事件发生时访问,包含此次输入会话插入的所有字符
var textbox = document.getElementById('myText')
EventUtil.addHandler(textbox, 'compositionstart', function(event) {
  event = EventUtil.getEvent(event)
  console.log(event.data)
})

EventUtil.addHandler(textbox, 'compositionupdate', function(event) {
  event = EventUtil.getEvent(event)
  console.log(event.data)
})

EventUtil.addHandler(textbox, 'compositionend', function(event) {
  event = EventUtil.getEvent(event)
  console.log(event.data)
})

这个方法有一定的兼容问题,低版本浏览器大多不支持,可以使用以下代码检测浏览器是否支持复合事件

var isSupported = document.implementation.hasFeature('CompositionEvent', '3.0')

4.6、变动事件

DOM2级 的变动(mutation)事件及能在DOM中的某一部分发生变化时给出提示。变动事件是为XML或 HTML DOM 设计的,并不特定于某种语言。DOM2级定义了如下变动事件

  • DINSubtreeNodified:在DOM结构中发生任何变化时触发。这个事件在其他任何事件触发后都会触发
  • DOMNodeInserted:在一个节点作为子节点被插入到另一个节点中时触发。
  • DOMNodeInsertedIntoDocument:在一个节点被直接插入文档或通过子数间接插入文档之后触发。这个事件在DOMNodeInserted之后触发。
  • DOMNodeRemovedFromDocument:在一个节点被直接从文档中移除或通过子树间接从文档中移除之前触发。这个事件在 DOMNodeRemoved 之后触发。
  • DOMAttrModified:在特性被修改之后触发。
  • DOOMCharacterDataModified:在文本节点的值发生变化时触发。

这些方法在不久的将来会废弃,所以不推荐使用了。这里简单的提一下

4.7、HTML5事件

DOM 规范没有涵盖所以浏览器支持的所有事件。HTML5 详尽列出了浏览器应该支持的所有事件。本节只讨论其中得到浏览器玩上支持的事

4.7.1、contextmenu 事件

用以表示何时应该显示上下文菜单(在浏览器中鼠标右键打开的菜单列表),以便开发人员取消默认的上下文菜单而提高自定义的菜单。
这个事件的目标是发生用户操作的元素。在所有浏览器中都可以取消这个事件:在兼容DOM的浏览器中,使用 event.preventDefault();在IE中,将event.returnValue 的值设置为false

<body>
  <div id="myDiv"> Right click or Ctrl+click me to get a custom context menu. Click anywhere else to get the default conetext menu.</div>
<ul id="myMenu" style="position:absolute; visibility:hidden;background-color:silver">
  <li><a href="#">item 1</a></li>
  <li><a href="#">item 2</a></li>
  <li><a href="#">item 3</a></li>
</ul>
</body>

以上面的 HTML 结构为例

var div = document.getElementById('myDiv')

EventUtil.addHandler(div, 'contextmenu', function(event) {

  event = EventUtil.getEvent(event)
  EventUtil.preventDefault(event) // 取消默认行为 不显示默认菜单

  var menu = document.getElementById('myMenu')  
  // 定位
  menu.style.left = event.clientX + 'px'
  menu.style.top = event.clientY + 'px'
  // 显示菜单
  menu.style.visibility = 'visible'
})


EventUtil.addHandler(document, 'click', function(event) {// 隐藏菜单
  document.getElementById('myMenu').style.visibility = 'hidden'
})

这个例子很简单,但它却展示了 Web 上所有自定义上下文菜单的基本结构。只需为这个例子中的上下文菜单添加一些css样式,就可以得到非常的效果

4.7.2、beforeunload 事件

这个事件会在浏览器卸载页面之前触发,可以通过它来取消卸载并继续使用原有页面。但是,不能彻底取消这个事件,因为那就相当于让用户无法离开当前页面。为此,这个事件的意图是将控制权交给用户,显示的消息会告知用户页面将被卸载

为了显示对话框,提示用户,必须将 event.returnValue 的值设置为要显示给用户的字符串(对IE及Firefox而言),同时作为函数的值返回(对Safari和Chrome)

EventUtil.addHandler(window, 'beforeunload', function(event) {
  
  event = EventUtil.getEvent(event)
  var message = 'are you sure' 
  event.returnValue = message
  return message
})
4.7.3、DOMContentLoaded 事件

DOMContentLoaded事件在形成完整的DOM树之后就会触发,不理会图像、JavaScript文件、CSS文件或其他资源是否下载完毕。与load事件不同,DOMContentLoaded 支持在页面下载的早期添加事件处理程序,这也就意味着用户能够尽早地与页面进行交互
可以为 document 或 window 添加相应的事件处理程序(尽管这个事件会冒泡到 window,但它的目标实际上是 document)

EventUtil.addHandler(window, 'DOMContentLoaded', function(event) {
  alert('Content loaded')
})

这个事件始终都会在load事件之前触发,支持这个属性的有 IE9+、Firefox、Chrome、Safari3.1+、Opera9+

4.7.4、readystatechange 事件

提供与文档或元素的加载状态有关的信息,支持 readystatechange 事件的每个对象都有一个 readyState 属性,可能包含下列5个值中的一个。

  • uninitialized(未初始化):对象存在但尚未初始化
  • loading(正在加载):对象正在加载数据
  • loaded(加载完毕):对象加载数据完成
  • interactive(交互):可以操作对象了,但还没有完全加载
  • complete(完成):对象已经加载完毕。

但并非所有对象都会经历 readyState 的这几个阶段。如果某个阶段不适用某个对象,则该对象完全可能跳过该阶段;
对于 document 而言,值为"interactive"的 readyState 会在与 DOMContentLoaded 大致相同的时刻触发 readystatechange 事件。此时,DOM数已经加载完毕,可以安全操作它了,因此就会进入交互(interactive)阶段。

EventUtil.addHandler(document, 'readystatechange', function(event) {
  if (document.readyState == 'interactive' ) alert('Content loaded')
})

为了尽可能抢到先机,有必要同时检测交互和完成阶段

EventUtil.addHandler(document, 'readystatechange', function(event) {
  if (document.readyState == 'interactive' || document.readyState == 'complete') {
    EventUtil.removeHandler(document, 'readystatechange', arguments.callee)
    alert('Content loaded')
  }
})

当 readystatechange事件触发时,会检测 document.readyState 的值,看当前是否已经进入交互阶段或完成阶段。如果是,则移除相应的事件处理程序以及在其他阶段再执行。


此外,<script>(在IE11以下和Opera中)和<link>(仅IE11以下中)元素也会触发 readystatechange 事件,可以用来确定外部的 JavaScript 和 css 文件是否已经加载完成。

EventUtil.addHandler(window, 'load', function() {

  // 加载 JavaScript 文件
  var script = document.createElement('script')

  EventUtil.addHandler(script, 'readystatechange', function(event) {
    console.log(1)
    event = EventUtil.getEvent(event)
    var target = EventUtil.getTarget(event)

    if (target.readyState == 'loaded' || target.readyState == 'complete') {
      EventUtil.removeHandler(target, 'readystatechange', arguments.callee)
      alert('Script Loaded')
    }
  });

  script.src = 'client.js'
  document.body.appendChild(script)


  // 加载 css文件
  var link = document.createElement('link')
  link.type = 'text/css'
  link.rel = 'stylesheet'

  EventUtil.addHandler(link, 'readystatechange', function(event) {
    event = EventUtil.getEvent(event)
    var target = EventUtil.getTarget(event)

    if (target.readyState == 'loaded' || target.readyState == 'complete') {
      EventUtil.removeHandler(target, 'readystatechange', arguments.callee)
      alert('CSS Loaded')
    }
  })
  link.href = 'example.css'
  document.getElementsByTagName('head')[0].appendChild(link)
})
4.7.5、 pageshow 和 pagehide 事件

Firefox 和 Opera 有一个特性,名叫“往返缓存”(back-forward cache,或 bfcache),用户使用浏览器的 “后退” 和 “前进” 按钮时加快页面的转换速度。这个缓存中不仅包含着页面数据,实际上是将整个页面都保存在了内存里。如果页面位于 bfcache 中,那么再次打开该页面就不会触发 load 事件。

pageshow,这个事件在页面显示时触发,无论该页面是否来自 bfcache,在重新加载的页面中,pageshow 会在 load 事件触发后触发;而对于 bfcache 中的页面,pageshow 会在页面状态完全恢复的的那一刻触发。虽然这个事件的目标是document,但必须将其事件处理程序加到 window。

除了通常属性外,pageshow 事件中的 event 对象还包含一个名为persisted的布尔值属性,如果页面被保存在了 bfcache 中,这个属性则为true,反之为 false

;(function() {
  var showCount = 0
  EventUtil.addHandler(window, 'load', function(event) {
    console.log('Load fired')
  })

  EventUtil.addHandler(window, 'pageshow', function(event) {
    showCount++
    console.log(event.persisted)
    console.log('show has been fired ' + showCount)
  })
})()

pagehide会在浏览器卸载页面的时候触发,而且是在unload事件之前触发。与pageshow 事件一样,pagehide 在 document 上面触发,但其事件处理程序必须要添加到 window 对象。

EventUtil.addHandler(window, 'pagehide', function(event) {
  alert('Hiding. Persisted? ' + event.persisted )
})

对于 pagehide 事件,如果页面在卸载之后被保存在 bfcache 中,那么 persisted 的值也会被设置为 true。因此,当第一次触发 pageshow 时,persisted 的值一定是 false,而在第一次触发 pagehide 时,persisted 就会变成 true。

支持 pageshow 和 pagehide事件的浏览器有 Firefox、Safari5+、Chrome、Opera,IE9及之前版本不支持这两个事件

4.7.6、hashchange 事件

*hashchange8 事件,在URL的参数列表(及URL中“#” 号谋面的所有字符串发送变化时通知开发人员),必须把 hashchange 事件处理程序添加给 window 对象,然后URL 参数列表只要变化就会调用它。此时的event 对象应该 额外包含两个属性:oldURL 和 newURL。

EventUtil.addHandler(window, 'hashchange', function(event) {
  console.log(event.oldURL)
  console.log(event.newURL)
})

支持 hashchange 事件的浏览器有 IE8+、Firefox3.6+、Safari5+、Chrome、Opera10.6+。这些浏览器中,只有 Firefox6+、Chrome和Opera支持 oldURL 和 newURL属性,因此,最好还是使用 location对象来确定当前的参数列表。

可以用一下代码,检测浏览器是否支持 hashchange 事件

var isSupported = ('onhashchange' in window) && (document.documentMode === undefined || document.documentMode > 7)

4.8、设备事件

智能手机和平板电脑的普及,及用户与浏览器交互引入了一种新的方式,而一类新事件也应运而生。设备事件(device event)可以让开发人员确定用户在怎样使用设备。

4.8.1、orientationchange 事件

orientationchange 事件,能够确定用户何时将设备由横向查看模式切换为纵向查看模式、移动Safari的 window,orientation 属性可能包含三个值:

  • 0:表示肖像模式
  • 90: 表示向左旋转的横向模式(“主屏幕”按钮在右侧)
  • -90:表示向右旋转的横向模式(“主屏幕”按钮在左侧)

只要用户改变了设备的查看模式,就会出发 orientationchange 事件,唯一的相关信息可以通过 window.orientation 访问到。

EventUtil.addHandler(window, 'load', function(event) {
  var div = document.getElementById('myDiv')
  div.innerHTML = 'Current orientation is ' + window.orientation

  EventUtil.addHandler(window, 'orientationchange', function(event) {
    div.innerHTML = 'Current orientation is ' + window.orientation
  })
})

所有 iOS 设备都支持 orientationchange 事件 和 window.orientation 属性

4.8.2、deviceorientation 事件

deviceorientation事件时在加速加速计检测到设备防腐变化时在 window 对象上触发
设备在三维空间中时靠x、y 和 z轴来定位的。当设备禁止放在水平表示上时,这三个值都是0。x轴方向是从左往右,y轴方向是从上往下,z轴方向是从前往后

触发 deviceorientation 事件时,事件对象中包含着每个轴相对于设备静止状态下发生变化的信息。事件对象包含以下5个属性。

  • alpha:在围绕z轴旋转时(即左右旋转时),y轴的度数差;是一个介于0到360之间的浮点数
  • beta:在围绕x轴旋转时(即前后旋转时),z轴的度数差;是一个介于-180到180之间的浮点数
  • gamma:在围绕y轴旋转时(即扭转设备时),z轴的度数差;是一个介于-90到90之间的浮点数。
  • absolute:布尔值,表示设备的指南针是否校验过
  • compassCalibrated:布尔值,表示设备的指南针是否校准过。

通过这些信息,可以响应设备的方法,重新排列或修改屏幕上的元素。要响应设备方向的改变而旋转元素,可以参看如下代码

EventUtil.addHandler(window, "deviceorientation", function(event) {
  var arrow = document.getElementById('arrow')
  arrow.style.webkitTransfrom = 'rotate('+ Math.round(event.alpha) + 'deg)'
})

元素“arrow” 会随着 event.alpha值的变化而选择,给人一种指南针的感觉。为了保证循旋转平滑,这里CSS3变换使用了 舍入之后的值

4.8.3、 devicemotion 事件

devicemotion 事件。这个事件时要告诉开发人员设备什么时候移动,而不仅仅是设备方向如何改变。通过 devicemotion 能够检测到设备是不是正在 往下掉,或者是不是被走着的人拿在手里
触发 devicemotion 事件时,事件对象包含以下属性。

  • acceleration:一个包含X、Y 和 Z 属性的对象,在不考虑重力的情况下,告诉你在每个方法上的加速度。
  • accelerationIncludingGravity:一个包含x、y、z属性的对象,在考虑z轴自然重力加速度的情况下,告诉你在每个方向上的加速度。
  • interval:以毫秒表示的时间值,必须在另一个 devicemotion 事件出发前传入。这个值在每个事件中应该是一个常量
  • rotationRate:一个包含表示方向的 alpha、beta、gamma 属性的对象

如果读取不到 acceleration、accelerationIncludingGravivty、rotationRate值,则他们的值为null。

EventUtil.addHandler(window, 'devicemotion', function(event) {
  var output = document.getElementById('output')
  if (event.rotationRate !== null) {
    output.innerHTML += "Alpha=" + event.rotationRate.alpha + ",  Beta=" +
                                   event.rotationRate.beta + ", Gamma=" +
                                   event.rotationRate.gamma
  }
})

4.9、触摸与手势事件

iOS版 Safari 为了向开发人员 传达一些特殊信息,新增了一些专有事件。W3C制定了 Touch Events 规范,以下介绍的事件只针对触摸设备。

4.9.1、触摸事件

包含 iOS 2.0 软件的 iPhone 3G 发布时,也包含了一个新版本的 Safari 浏览器。这款新的移动Safari 提供了一些与 触摸(touch) 操作相关的事件。后来,Android 上的浏览器也实现了相同的事件。具体来说,有以下几个触摸事件。

  • touchstart:当手指触摸屏幕时触发;即使已经有一个手指放在了屏幕上也会触发。
  • touchmove:当手指在屏幕上滑动时连续地触发。在这个事件发生期间,调用preventDefault() 可以阻止滚动。
  • touchend:当手指从屏幕上移开时触发
  • touchcancel:当系统停止跟踪触摸时触发。

上面的几个事件都会冒泡
除了常见的DOM属性外,触摸事件还包含下列三个用于跟踪触摸的属性

  • touches:表示当前跟踪的触摸操作的 Touch 对象的数组
  • targetTouchs:特定于事件目标的 Touch 对象的数组
  • changeTouches:表示自上次触摸以来发生了什么改变的 Touch 对象的数组

每个 Touch 对象包含下列属性

  • clientX:触摸目标在视口中的x坐标
  • clientY:触摸目标在视口中的y坐标
  • identifier:标识触摸的唯一ID
  • pageX:触摸目标在页面中的x坐标
  • pageY:触摸目标在页面中的y坐标
  • screenX:触摸目标在屏幕中的x坐标
  • screenY:触摸目标在屏幕中的y坐标
  • target:触摸的DOM节点目标

使用这些属性就可以跟踪用户对屏幕的接触操作。

function handleTouchEvent(event) {

  // 值跟踪一次触摸
  if (event.touches.length == 1) {

    var output = document.getElementById('output')

    switch(event.type) {
      case 'touchstart':
        output.innerHTML = "Touch started (" + event.touches[0].clientX + "," + event.touches[0].clentY + ")"
        break;
      case 'touchend':
        output.innerHTML = "<br> Touch ended (" + event.changedTouches[0].clientX + "," + event.changedTouches[0].clientY + ")"
        break;
      case 'touchmove':
        event.preventDefault() // 阻止默认滚动
        output.innerHTML += "<br> Touch moved (" + event.changedTouches[0].clientX + "," + event.changedTouches[0].clientY + ")"
        break;
    }
  }
}

EventUtil.addHandler(document, 'touchstart', handleTouchEvent)
EventUtil.addHandler(document, 'touchend', handleTouchEvent)
EventUtil.addHandler(document, 'touchmove', handleTouchEvent)
4.9.2、手势事件

iOS2.0中的 Safari 还引入了一组手势事件。当两个手指触摸屏幕时就会产生手势,手势通常会改变显示项的大小,或者旋转显示项。有三个手势事件,分别介绍如下:

  • gesturestart:当一个手指已经按在屏幕上而另一个手指有触摸屏幕时触发
  • gesturechange:当触摸屏幕的任何一个手指的位置放生变化时触发。
  • getstureend:当任何一个手指从屏幕上面移开时触发

只有两个手指都触摸到事件的接受容器是才会触发这些事件。由于这些事件冒泡,所有将事件处理程序放在文档上也可以处理所有手势事件。事件的目标就是两个手指都位于其范围内的那个元素

与触摸事件一样,每个手势事件的 event 对象都包含着对标准的鼠标事件属性(bubbles、cancelable、clientX....)。此外,还包含两个额外的属性:rotation、scale

  • rotation 属性表示手指变化引起的旋转角度
    • 负值表示逆时针旋转
    • 正值表示顺时针旋转(该值从 0 开始)
  • scale 属性表示两个手指间距离的变化情况(例如向内收缩会缩短距离);这个值从1开始,并随距离拉大而增长,随距离缩短而减小。

下面是使用手势事件的一个示例

function handleGestureEvent(event) {
  var output = document.getElementById('output')
  switch(event.type) {
    case 'gesturestart':
      output.innerHTML = "Gesture started (rotation =" + event.rotation + ",scale=" + event.scale + ")"
      break;
    case 'gestureend':
      output.innerHTML += "<br>Gesture ended (rotation=" + event.rotation + ",scale=" + event.scale + ")"
      break;
    case 'gesturechange': 
      output.innerHTML += "<br>Gesture change (rotation=" + event.rotation + ",scale=" + event.scale + ")"
      break; 
  }
}

document.addEventListener('gesturestart', handleGestureEvent, false)
document.addEventListener('gestureend', handleGestureEvent, false)
document.addEventListener('gesturechange', handleGestureEvent, false)

五、内存和性能

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪时间。事实上,从如何利用好事件处理程序的角度出发,还是有一些方法能够提升性能的。

5.1、事件委托

对“事件处理程序过多”问题的解决方案就是 事件委托。事件委托利用冒泡原理。我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单机的元素分别添加事件处理程序。

<ul id="myLinks">
  <li id="goSomewhere">Go somewhere</li>
  <li id="doSomething">Do something</li>
  <li id="sayHi">Say hi</li>
</ul>  

使用事件委托,只需要在DOM 树中尽量最高的层此上添加一个事件处理程序。

var list = document.getElementById('myLinks')

EventUtil.addHandler(list, 'click', function(event) {
  event = EventUtil.getEvent(event)
  var target = EventUtil.getTarget(event)

  switch(target.id) {
    case 'goSomewhere':
      location.href = 'http://www.baidu.com'
      break;
    case 'doSomething':
      document.body.style.backgroundColor = 'pink'
      break;
    case 'sayHi':
      alert('hi')
      break;    
  }
})

如果可信的话,也可以考虑为 document 对象添加一个事件处理程序,用以处理页面上发生的某种特定类型的事件。这样做与采取传统的做法相比具有如下优点。

  • document对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序(无需等待 DOMContentLoaded 或 load 事件)。换句话说,只要可单机的元素呈现在页面上,就可以立即具备适当的功能
  • 在页面设置事件处理程序所需的事件更少。只添加一个事件处理程序所需的 DOM 引用更少,所花的时间也更少
  • 整个页面占用的内存空间更少,能够提升整体性能。

5.2、移除事件处理程序

每当事件处理程序指定给元素时,允许中的浏览器代码与支持页面交互的JavaScript代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。内存中留有那些过时不用的“空事件处理程序”(dangling event handler),也就是造成Web应用程序内存与性能问题的 主要原因

在两种情况下,可能会造成上诉问题。

  • 从文档中移除带有事件处理程序的元素时。这可能是通过纯粹的DOM操作,例如使用 removeChild() 和 replaceChild() 方法,

  • 使用innerHTML替换页面中某一部分的时候,如果带有事件处理程序的元素被 innerHTML 删除了,那么原来添加到元素中的事件处理程序极有可能无法被当做垃圾回收。

如上所述,如果你知道某个元素即将被移除,那么最好手工移除事件处理程序,

<body>
  <div id="myDib">
    <button id="btn">click</button>
</div>
<script>
var btn = document.getElementById('btn')
btn.onclick = function() {
  // ...

  btn.onclick = null  // 移除事件处理程序
  document.getElementById('myDiv').innerHTML = 'Processing'
}
</script>
</body>

这样就确保了内存可以被 ,再次利用,而从 DOM 中移除按钮也做到了干净利索。

导致 “空事件处理程序” 的另一种情况,就是卸载页面的时候。毫不奇怪,IE8及更早版本在这种情况下依然是问题最多的浏览器,经过其他浏览器或多或少也有类似的问题。在页面被卸载之前没有清理干净事件处理程序,那它们就会滞留在内存中。
最好的做法是在页面卸载之前,先通过 onunload 事件处理程序移除所有事件处理程序。在此,事件委托技术再次表现出它的优势——需要跟踪的事件处理程序越少,移除他们就越容易。对这种类似撤销的操作,我们可以把它想象成:只要是通过 onload 事件处理程序添加的东西,最后都要通过 onunload 事件处理程序将他们移除。

六、模拟事件

可以使用 JavaScript 在任意时刻来触发特定的事件,而此时的事件就如同浏览器创建的事件一样。

6.1、DOM中的事件模拟

在意在 document 对象上使用 createEvent() 方法创建 event 对象。MDN上面推荐 使用 event constructors来 代替这个方法。

createEvent() 这个方法接受一个参数,即表示要创建的事件类型的字符串。
这个字符串可以是下列字符串之一

  • UIEvents:以便化的UI事件。鼠标事件和键盘事件都继承自UI事件。DOM3级中时UIEvent
  • MouseEvents:一般化的鼠标事件。DOM3级中时 MouseEvent
  • MutationEvents:一般化的DOM 变动事件。DOM3级中时 MutationEvent。
  • HTMLEvents:以便化的HTML 事件。没有对应的DOM3级事件(HTML事件被分散到其他类别中)
6.1.1、模拟鼠标事件

为 createEvent() 传入字符串“MouseEvents”。返回的对象有一个名为 initMouseEvent() 方法,用于指定与该鼠标事件有关的信息。这个方法接受 15 个参数,分别与鼠标事件中每个典型的属性一一对应。
这些参数的含义如下:

  • type(字符串):表示要出发的事件类型,例如‘click’
  • bubbles(布尔值):表示事件是否应该冒泡。为精确地模拟鼠标事件,应该把这个参数设置为 true。
  • cancelable(布尔值):表示事件是否可以取消。为精确地模拟鼠标事件,应该把这个参数设置为true。
  • view(AbstractView):与事件关联的视图。这个参数几乎总要设置为 document.defaultVieiw。
  • detail(整数):与事件有关的详细信息。这个值一般只有事件处理程序使用,但通常都设置为0。
  • screenX(整数):事件相对于屏幕的X坐标
  • screenY(整数):事件相对于屏幕的Y坐标
  • clientX(整数):事件相对于视口的X坐标。
  • clientY(整数):事件相对于视口的Y坐标。
  • ctrlKey(布尔值):表示是否按下了 Ctrl 键。默认值为false
  • altKey(布尔值):表示是否按下了Alt 键,默认值为false
  • shiftKey(布尔值):表示是否按下了Shift 键,默认值为false
  • metaKey(布尔值):表示是否按下了 meta 键,默认值为false
  • button(整数):表示按下了哪一对鼠标键。默认值为0.
  • relatedTarget(对象):表示与事件相关的对象。这个从参数只在模拟mouseover 或 mouseout 时使用。

当把dispatchEvent() 方法时,这个对象 target属性会自动设置。

var btn = document.getElementById('myBtn')

// 创建事件对象
var event = document.createEvent('MouseEvents')

// 初始化事件对象
event.initMouseEvent('click', true, true, document.defaultView, 0, 0, 0, 0,
0, false, false, false, false, 0, null)

// 触发事件
btn.dispatchEvent(event)
6.1.2、模拟键盘事件

DOM3 级规定,调用 createEvent() 并传入 “KeyboardEvent” 就可以创建一个键盘事件。返回的事件对象包含一个 initKeyEvent() 方法,这个方法接受下列参数

  • type(字符串):表示要出发的事件类型,如‘keydown’
  • bubbles(布尔值):表示事件是否应该冒泡。为精确模拟鼠标事件,应该设置为 true
  • cancelable(布尔值):表示事件是否可以取消。为精确模拟鼠标事件,应该设置为 true
  • view(AbstractView):与事件关联的视图。这个参数几乎总是要设置为 document,defaultView
  • key(布尔值):表示按下的键的键码
  • location(整数):表示按下了哪里的键。0 表示默认的主键盘,1 表示左, 2 表示右, 3 表示数字键盘, 4 表示移动设备
  • modifiers(字符串):空格分割的修改键列表,如“shift”
  • repeat(整数):在一行中按了这个键多少次。

由于 DOM3级不提倡使用 keypress 事件,因此只能利用这种技术来模拟 keydown 和 keyup 事件。

var textbox = document.getElementById('myTextbox'), event

// 以DOM3 级方式创建事件对象
if (document.implementation.hasFeature('KeyboardEvents', '3.0')) {

  event = document.createEvent('KeyboardEvent')

  // 初始化事件对象
  event.initKeyboardEvent('keydown', true, true, document.defaultView, 'a', 0, 'shift', 0)

}
console.log(event)
// 触发事件
textbox.dispatchEvent(event)

这个例子模拟的是按住 Shift 的同时又 按下 A键


在 Firefox 中,调用 createEvent() 并传入“KeyEvents”就可以创建一个键盘事件。返回的事件对象会包含一个 initKeyEvent() 方法,这个方法接受下列 10个参数。

  • type(字符串):表示要出发的事件类型,如‘keydown’
  • bubbles(布尔值):表示事件是否应该冒泡。为精确模拟鼠标事件,应该设置为 true
  • cancelable(布尔值):表示事件是否可以取消。为精确模拟鼠标事件,应该设置为 true
  • view(AbstractView):与事件关联的视图。这个参数几乎总要设置为 document.defaultView。
  • ctrlKey(布尔值):表示是否按下了 Ctrl 键。默认值为false
  • altKey(布尔值):表示是否按下了 Alt 键。默认值为false
  • shiftKey(布尔值):表示是否按下了Shift 键,默认值为false
  • metaKey(布尔值):表示是否按下了 meta 键,默认值为false
  • keyCode(整数):被按下或释放的键的键码。这个参数对 keydown 和 keyup 事件有用,默认值为 0
  • charCode(整数):通过按键生成的字符的ASCII编码。这个参数对 keypress 事件有用,默认值为0

将创建event 对象传入到 dispatchEvent() 方法就可以出发键盘事件,如下:

// 只适用于Firefox
var textbox = document.getElementById('myTextbox')

// 创建事件对象
var event = document.createEvent('KeyEvents')

// 初始化事件对象
event.initKeyEvent('keypress', true, true, document.defaultView, false, false, false, false, 65, 65)

// 触发事件
textbox.dispatchEvent(event)

在 Firefox 中运行上面的代码,会在指定的文本框中输入字母A。统一,也可以依此模拟keyup 和 keydown 事件
在其它浏览器中,则需要创建一个通用的事件,然后再向事件对象中添加键盘事件特有的信息。例如:

var textbox = document.getElementById('myTextbox')

// 创建事件对象
var event = document.createEvent('Events')

// 初始化事件对象
event.initEvent(type, bubbles, cancelable)
event.view = document.defaultView
event.altKey = false
event.ctrlKey = false
event.shiftKey = false
event.metaKey = false
event.keyCode = 65
event.charCode = 65

// 触发事件
textbox.dispatchEvent(event)
6.1.3、模拟其他事件

虽然鼠标事件和键盘事件是在浏览器中最经常模拟的事件,但有时候同样需要模拟变动事件和HTML 事件。

模拟变动事件,可以使用 createEvent('MutationEvents') 创建一个包含 initMutationEvent() 方法的变动事件对象。这个方法接受的参数包括:type、bubbles、cancelable、relateNode、preValue、newValue、attrName、attrChange
模拟变动事件

var event = document.createEvent('HTMLevents')
event.initMutationEvent('DOMNodeeInserted', true, false, someNode, '', '', '', 0)
target.dispatchEvent(event)

要模拟HTML 事件,同样需要先创建一个 event对象——通过 createEvent("HTMLEvents"),然后再使用这个对象的initEvent() 方法来初始化它即可。

var event = document.createEvent('HTMLEvents')
even.initEvent('focus', true, false)
target.dispatchEvent(event)

这个例子展示了如何在给定目标上模拟 focus 事件。模拟其他HTML事件的方法也是这样

6.1.4、自定义DOM事件

DOM3级 还定义了“自定义事件”。创建自定义事件,可以调用 createEvent('CustomEvent')返回的对象有一个名为 initCustomEvent()的方法,接受如下4个参数

  • type(字符串):触发的事件类型
  • bubbles(布尔值):表示事件是否应该冒泡
  • cancelable(布尔值):表示事件是否可以取消
  • detail(对象):任意值,保存在event 对象的detail 属性中。

可以像分派其他事件一样在 DOM中分派创建的自定义事件对象。例如:

var div = document.getElementById('myDiv'), event

EventUtil.addHandler(div, "myevent", function(event) {
  alert("DIV:" + event.detail)
})
EventUtil.addHandler(document, "myevent", function(event) {
  alert("DOCUMENT:" + event.detail)
})
if (document.implementation.hasFeature("CustomEvents", "3.0")) {
  event = document.createEvent("CustomEvent")
  event.initCustomEvent("myevent", true, false, "Hello world!")
  div.dispatchEvent(event)
}

6.2、IE中时事件模拟

IE8 及之前版本中模拟事件与在DOM中模拟事件的思路相似:先创建 event 对象,然后为期指定相应的信息,然后再使用该对象来触发事件。

document.createEventObject() 方法可以在IE中创建event 对象。这个方法不接受参数,结果会返回一个通用的 event 对象。然后,你必须为证对象添加所有必要的信息。最后一步就是在目标上调用 fireEvent() 方法,这个方法接受两个参数:事件处理程序的名称 和 event 对象
在调用 fireEvent() 方法时,会自动为event 对象添加 srcElement 和 type 属性;

模拟在一个按钮上触发click 事件过程

var btn = document.getElementById('myBtn')

// 创建事件对象
var event = document.createEventObject()

// 初始化事件对象
event.screenX = 100
event.screenY = 0
event.clientX = 0
event.clientY = 0
event.ctrlKey = false
event.altKey = false
event.shiftKey = false
event.button = 0

// 触发事件
btn.fireEvent("onclick", event)

采用同样的模式 也可以模拟 出发 keypress 事件

var textbox = document.getElementById('myTextbox')

// 创建事件对象
var event = document.createEventObject()

// 初始化事件对象
event.ctrlKey = false
event.altKey = false
event.shiftKey = false
event.keyCode = 65

// 触发事件 
textbox.fireEvent('onkeypress', event)

其实,目前来说新出来了很多API用于模拟事件,比如 click() 方法,所以这些都很少见了

七、小结

在使用事件时,需要考虑如下一些内存与性能方面的问题

  • 在必要限制一个页面中事件处理程序的数量,数量太多会导致占用大量内存,而且也会让用户感觉页面反应不够灵敏
  • 建立在事件冒泡机制之上的事件委托技术,可以有效地减少事件处理程序的数量

事件时 JavaScript 中最重要的主题之一,深入理解事件的工作机制以及它们对性能的影响至关重要

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  •   JavaScript 与 HTML 之间的交互是通过事件实现的。   事件,就是文档或浏览器窗口中发生的一些特...
    霜天晓阅读 3,426评论 1 11
  • JavaScript 与 HTML 之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬...
    LemonnYan阅读 646评论 0 4
  • 事件流 JavaScript与HTML之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互...
    DHFE阅读 797评论 0 3
  • JavaScript 程序采用了异步事件驱动编程模型。在这种程序设计风格下,当文档、浏览器、元素或与之相关的对象发...
    劼哥stone阅读 1,232评论 3 11
  • JavaScript 与 HTML 之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬...
    小小的白菜阅读 284评论 0 0