JS与HTML的交互是通过事件实现的。
可以使用仅在事件发生时执行的监听器订阅事件。在传统软件工程领域,这个模型叫“观察者模式”,其能够做到页面行为与页面展示的分离
浏览器的事件系统非常复杂。即使所有主流浏览器都实现了DOM2 Events,规范也没有涵盖所有的事件类型。
BOM也支持事件,这些事件与DOM事件之间的关系由于长期以来缺乏文档,经常容易被混淆。而DOM3新增的事件API又让这些问题进一步复杂化了。
事件流
事件流描述了页面接收事件的顺序。DOM2 Events规范规定事件流分为3个阶段:事件捕获、到达目标和事件冒泡
事件捕获最先发生,为提前拦截事件提供了可能。然后,实现的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。
IE支持事件冒泡流,而Netscape Communicator将支持事件捕获流
所有现代浏览器都支持事件冒泡,只是在实现方式上会有一些变化。现代浏览器中的事件会一直冒泡到window对象
事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。
事件捕获实际上是为了在事件到达最终目标前拦截事件。
实际上,所有浏览器都是从window对象开始捕获事件,而DOM2 Events规范规定的是从document开始
由于旧版本浏览器不支持,因此实际当中几乎不会使用事件捕获。通常建议使用事件冒泡,特殊情况下可以使用事件捕获
事件处理程序
为响应事件而调用的函数被称为事件处理程序(或事件监听器),以on开头,比如onclick、onload。
特定元素支持的每个事件都可以使用事件处理程序的名字以HTML属性的形式来指定,比如:onclick="showMsg()"。作为事件处理程序执行的代码可以访问全局作用域中的一切。
以这种方式指定的事件处理程序会创建一个函数来封装属性的值。这个函数有一个特殊的局部变量event
,其中this值相当于事件的目标元素。另外,这个函数的作用域链会被扩展,document和元素自身的成员都可以当成局部变量来访问
这是通过使用
with实现的
with(document){ with(this) { // 属性值 } }这意味着事件处理程序可以更方便地访问自己的属性,例如:
<input type="button" value="click me" onclick="console.log(value)">
如果这个元素是一个表单输入框,则作用域链中还会包含表单元素,事件处理程序对应的函数等价于
function() {
with(document) {
with(this.form) {
with(this) {
// 属性值
}
}
}
}
本质上,经过这样的扩展,事件处理程序的代码就可以不必引用表单元素,而直接访问同一表单中的其他成员了。
<form method="post">
<input type="text" name="username" value="">
// 事件处理程序中的代码直接引用了username
<input type="button" value="Echo Username" onclick="console.log(username.value)">
</form>
在HTML中指定事件处理程序有一些问题:
-
时机问题
有可能HTML元素已经显示在页面上,用户都与其交互了,而事件处理程序的代码还无法执行,就会发生错误。
为此,大多数HTML事件处理程序会封装在
try/catch块中,以便在这种情况下静默失败 -
对事件处理程序作用域链的扩展在不同浏览器中可能导致不同的结果
不同JS引擎中标识符解析的规则存在差异,因此访问无限定的对象成员可能导致错误
-
HTML与JS强耦合
如果需要修改事件处理程序,则必须在HTML和JS中修改代码
这也是很多开发者不使用HTML事件处理程序,而使用JS指定事件处理程序的主要原因
在JS中指定事件处理程序的传统方式是把一个函数赋值给一个事件处理程序属性。
要使用JS指定事件处理程序,必须先取得要操作对象的引用。
每个元素都有通常小写的事件处理程序属性,比如onclick。只要把这个属性赋值为一个函数即可:
let btn = document.getElementById('myBtn');
btn.onclick = function() {
// 在此作用域中,this引用元素本身
...
}
// 注意前面的代码在运行之后才会给事件处理程序赋值。因此如果页面中上面的代码出现在按钮之后,则有可能出现用户点击按钮没有反应的情况
// 以这种方式添加事件处理程序是注册在事件流的冒泡阶段的。
// 移除事件处理程序
btn.onclick = null;
DOM2 Events为事件处理程序的赋值和移除定义了两个方法
-
addEventListener(eventName, fn, 是否在冒泡阶段调用事件处理程序)第三个参数默认值为
false,表示在冒泡阶段调用事件处理器btn.addEventListener('click', handler, false); -
removeEventListener(eventName, fn, 是否在冒泡阶段调用事件处理程序)第三个参数默认值为
false,表示在冒泡阶段调用事件处理器
这两个方法暴露在所有DOM节点上。
使用DOM2方式的主要优势是可以为同一个事件添加多个事件处理程序。多个事件处理程序以添加顺序来触发
使用addEventListener()添加的事件处理程序只能使用removeEventListener()并传入与添加时同样的参数来移除
大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,主要原因是跨浏览器兼容性好
IE实现了与DOM类似的方法,即
-
attachEvent(eventName, fn)因为IE8及更早版本只支持事件冒泡,所以使用
attachEvent()添加的事件处理程序会添加到冒泡阶段btn.attachEvent('onclick', function() { ... }) // 注意,这里是'onclick',而不是'click'在IE中使用
attachEvent()与使用DOM0方式的主要区别是事件处理程序的作用域。使用DOM0方式时,事件处理程序中的
this值等于目标元素而使用
attachEvent()时,事件处理程序是在全局作用域中运行的,因此this等于window与DOM方法不同,这里事件处理程序会以添加他们的顺序反向触发。
-
detachEvent(eventName, fn)
为了以跨浏览器兼容的方式处理事件,很多开发者会选择使用一个JS库,其中抽象了不同浏览器的差异。当然也可以使用以上方法自己编写跨浏览器事件处理代码
事件对象
在DOM中发生事件时,所有相关信息都会被收集并存储在一个名为event的对象中。
event对象只在事件处理程序执行期间存在,一旦执行完毕,就会被销毁
这个对象包含了一些基本信息,比如导致事件的元素、发生的事件类型,以及可能与特定事件相关的任何其他数据
在DOM合规的浏览器中,event对象是传给事件处理程序的唯一参数。
所有事件对象都会包含下表列出的这些公共属性和方法(这里只列出了常用部分)
-
bubbles表示事件是否冒泡 -
cancelable表示是否可以取消事件的默认行为 -
target事件目标 -
currentTaeget当前事件处理程序所在的元素在事件处理程序内部,
this对象始终等于currentTarget的值,而target只包含事件的实际目标如果事件处理程序直接添加在了意图的目标,则
this、currentTarget和target的值是一样的 -
trustedtrue表示事件是由浏览器生成的。false表示事件是开发者通过JS创建的 -
eventPhase表示调用事件处理程序的阶段:1代表捕获阶段,2代表到达目标,3代表冒泡阶段 -
preventDefault()用于取消事件的默认行为。只有cancelable为true才可以调用这个方法 -
stopImmediatePropagation()用于取消所有后续事件捕获或事件冒泡,并阻止调用任何后续事件处理程序 -
stopPropagation()用于取消所有后续事件捕获或事件冒泡。只有bubbles为true才可以调用这个方法
事件类型
Web浏览器中可以发生很多种事件
DOM3 Events定义了如下事件类型
-
用户界面事件 涉及与BOM交互的通用浏览器事件
-
焦点事件 在元素获得和失去焦点时触发
-
鼠标事件 使用鼠标在页面上执行某些操作时触发
-
滚轮事件 使用鼠标滚轮(或类似设备)时触发
-
输入事件 向文档中输入文本时触发
-
键盘事件 使用键盘在页面上执行某些操作时触发
-
合成事件 在使用某种IME输入字符时触发
除了这些事件类型之外,HTML5还定义了另一组事件,而浏览器通常在DOM和BOM上实现专有事件。
这些专有事件基本上都是根据开发者需求而不是按照规范增加的,因此不同浏览器的实现可能不同
用户界面事件
用户界面事件或UI事件不一定跟用户操作有关。
这类事件在DOM规范出现之前就已经以某种形式存在了,保留它们是为了向后兼容
-
load事件在
window对象上,Load事件会在整个页面(包括所有外部资源如图片、JS文件和CSS文件)加载完成后触发根据DOM2 Events,
load事件应该在document而非window上触发。可是为了向后兼容,所有浏览器都在window上实现了load事件// 第一种触发Load事件 window.addEventListener('load', event => { ... }) // 第二种触发 <body onload="console.log('loaded')"></body>图片上也会触发
load事件,还有一些元素也以非标准的方式支持load事件,<script>元素会在JS文件加载完成后触发load事件,从而可以动态检测。与图片不同,要下载JS文件必须同时指定
src属性并把<script>元素添加到文档中。因此指定事件处理程序和指定src属性的顺序在这里并不重要。IE和Opera支持
<link>元素触发load事件,因而支持动态检测样式表是否加载完成。 -
unload事件unload事件会在文档卸载完成后触发。unload事件一般是在从一个页面导航到另一个页面时触发,最常用于清理引用,以避免内存泄漏因为
unload事件是在页面卸载完成后触发的,所以不能使用页面加载后才有的对象。此时要访问DOM或修改页面外观都会导致错误 -
resize事件当浏览器窗口被缩放到新高度或宽度时,会触发
resize事件。这个事件在
window上触发,因此可以通过JS在window上或为<body>元素添加onresize属性来指定事件处理程序不同浏览器在决定何时触发
resize事件上存在重要差异。无论如何,都应该避免在这个事件处理程序中执行过多计算。否则可能由于执行过于频繁而导致浏览器响应明确变慢 -
scroll事件虽然
scroll事件发生在window上,但实际上反应的是页面中相应元素的变化。在混杂模式下,可以通过
<body>元素检测scrollLeft和scrollTop属性的变化。而在标准模式下,这些变化在除早期版的Safari之外的所有浏览器中都发生在<html>元素上window.addEventListener('scroll', event => { if(document.compatMode === 'CSS1Compat') { console.log(document.documentElement.scrollTop); } else { console.log(document.body.scrollTop); } })类似于
resize、scroll事件也会随着文档滚动而重复触发,因此最好保持事件处理程序的代码尽可能简单
焦点事件
焦点事件在页面元素获得或失去焦点时触发。
焦点事件中的两个主要事件是focus和blue,这两个事件在JS早期就得到了浏览器支持。
它们最大的问题是不冒泡。这导致IE后来又增加了focusin和focusout,Opera又增加了DOMFocusIn和DOMFocusOut。
IE新增的这两个事件已经被DOM3 Events标准化
当焦点从页面中的一个元素移到另一个元素上时,会依次发生如下事件:
1、focusout 在失去焦点的元素上触发。(是blur的通用版)
2、focusin 在获得焦点的元素上触发。(是focus的冒泡版)
3、blur 在失去焦点的元素上触发。(此事件不冒泡,所有浏览器都支持)
4、DOMFocusOut 在失去焦点的元素上触发 (是blue的通用版,只适用于Opera,推荐使用focusout)
5、focus 在获得焦点的元素上触发。(此事件不冒泡,所有浏览器都支持)
6、DOMFoucsIn 在获得焦点的元素上触发。(是focus的冒泡版。只适用于Opera,推荐使用focusin)
鼠标和滚轮事件
-
click在用户点击鼠标主键(通常是左键)或按键盘回车键时触发。 -
dblclick在用户双击鼠标主键(通常是左键)时触发。 -
mousedown在用户按下任意鼠标键时触发。 -
mouseup在用户释放鼠标键时触发 -
mouseenter在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发
-
mouseover在用户把鼠标光标从元素外部移动到元素内部时触发。 -
mouseleave在用户把鼠标光标从元素内部移到元素外部时触发这个事件不冒泡,也不会在光标经过后代元素时触发
-
mouseout在用户把鼠标光标从一个元素移到另一个元素上时触发。移到的元素可以是原始元素的外部元素,也可以是原始元素的子元素。
-
mousemove在鼠标光标在元素上移动时反复触发
页面中的所有元素都支持鼠标事件。
对mousedown和mouseup事件来说,event对象上会有一个button属性,表示按下或释放的是哪个按键。
DOM为这个button属性定义了3个值:0表示鼠标主键(左键)、1表示鼠标中键(通常也是滚轮键)、2表示鼠标副键(右键)
除了
mouseenter和mouseleave,所有鼠标事件都会冒泡,都可以被取消,而这会影响浏览器的默认行为
由于事件之间存在关系,因此取消鼠标事件的默认行为也会影响其他事件
比如,
click事件触发的前提是mousedown事件触发后,紧接着又在同一个元素上触发了mouseup事件。如果mousedown和mouseup中的任意一个事件被取消,那么click事件就不会触发。类似地,两次连续的
click事件会导致dblclick事件触发。只要有任何逻辑阻止了这两个click事件发生(比如取消其中一个click事件或取消mousedown或mouseup事件中的任一个),dblclick事件就不会发生。这4个事件永远会按照如下顺序触发:1、
mousedown2、mouseup3、click4、mousesown5、mouseup6、click7、dblclick
DOM通过event对象的relatedTarget属性提供了相关元素的信息。这个属性只有在mouseover和mouseout事件发生时才包含值。其他是有事件的这个属性的值都是null
相关元素:比如,对
Mouseover事件来说,事件的主要目标是获得光标的元素,相关元素是失去光标的元素
鼠标事件还有一个名为滚轮事件的子类别。滚轮事件只有一个事件mousewheel,反应的鼠标滚动或带滚轮的类似设备上滚轮的交互
mousewheel事件会在用户使用鼠标滚轮时触发,包括在垂直方向上任意滚动。
这个事件会在任何元素上触发,并(在IE8中)冒泡到document和(在所有现代浏览器中)window。
mousewheel事件的event对象包含鼠标事件的所有标准信息,此外还有一个名为wheelDelta的新属性。当鼠标滚轮向前滚动时,wheelDelta每次都是+120;而当鼠标滚轮向后滚动时,wheelDelta每次都是-120。
多数情况下只需知道滚轮滚动的方向,而这通过wheelDelta值的符号就可以知道
触摸屏设备
-
不支持
dblclick事件。双击浏览器窗口可以放大,但没有办法覆盖这个行为 -
单指点触屏幕上的可点击元素会触发
mousemove事件。如果操作会导致内容变化,则不会再触发其他事件。如果屏幕上没有变化,则会相继触发
mousedown、mouseup和click事件 -
mousemove事件也会触发mouseover和mouseout事件 -
双指点触屏幕并滑动导致页面滚动时会触发
mousewheel和scroll事件
键盘与输入事件
-
keydown用户按下键盘上某个键时触发,而且持续按住会重复触发 -
keypress用户按下键盘上某个键并产生字符时触发,而且持续按住会重复触发。Esc键也会触发这个事件DOM3 Events废弃了
keypress事件,而推荐textInput事件 -
keyup用户释放键盘上某个键时触发 -
textInput输入事件,这个事件是对keypress事件的扩展,用于在文本显示给用户之前更方便地截获文本输入textInput会在文本被插入到文本框之前触发作为对
keypress的替代,textInput事件的行为有些不一样。一个区别是keypress会在任何可以获得焦点的元素上触发,而textInput只在可编辑区域上触发。另一个区别是
textInput只在有新字符被插入时才会触发,而keypress对任何可能影响文本的键都会触发因为
textInput事件主要关注字符,所以在event对象上提供了一个data属性,包含要插入的字符(不是字符编码)
对于keydown和keyup事件,event对象的keyCode属性中会保存一个键码,对应键盘上特定的一个键。
浏览器在event对象上支持charCode属性,只有发生keypress事件时这个属性才会被设置值,包含的是按键字符对应的ASCII编码。
要以跨浏览器方式获取字符编码,首先要检查charCode属性是否有值,如果没有再使用keyCode。一旦有了字母编码,就可以使用String.fromCharCode()方法将其转换为实际的字符了。
event对象上还有一个名为inputMethod的属性,该属性表示向控件中输入文本的手段,可能的值如下:
-
0 表示浏览器不能确定是什么输入手段
-
1 表示键盘
-
2 表示粘贴
-
3 表示拖放操作
-
4 表示IME
-
5 表示表单选项
-
6 表示手写(如使用手写笔)
-
7 表示语音
-
8 表示组合方式
-
9 表示脚本
使用这些属性,可以确定用户是如何将文本输入到控件中的,从而可以辅助验证
HTML5 事件
-
contextmenu事件专门用于表示何时该显示上下文菜单,从而允许开发者取消默认的上下文菜单并提供自定义菜单
此事件冒泡,因此只要给
document指定一个事件处理程序就可以处理页面上的所有同类事件。事件目标是触发操作的元素。这个事件在所有浏览器中都可以取消,在DOM合规的浏览器中使用
event.preventDefault(),在IE8及更早版本中将event.returnValue设置为false. -
beforeunload事件beforeunload事件会在window上触发,用意是给开发者提供阻止页面被卸载的机会。这个事件会再页面即将从浏览器中卸载时触发,如果页面需要继续使用,则可以不被卸载。
这个事件不能取消,否则就意味着可以把用户永久阻拦在一个页面上。相反,这个事件会向用户显示一个确认框,其中的消息表明浏览器即将卸载页面,并请用户确认是希望关闭页面,还是继续留在页面上
-
DOMContentLoaded事件window的load事件会再页面完全加载后触发,因为要等待很多外部资源加载完成,所以会花费较长事件。而
DOMContentLoaded事件会再DOM树构建完成后立即触发,而不用等待图片、JS文件、CSS文件或其他资源加载完成。 相对于load事件,DOMContentLoaded可以让开发者在外部资源下载的同时就能指定事件处理程序,从而让用户能够更快地与页面交互DOMContentLoaded事件的event对象中不包含任何额外信息,该事件通常用于添加事件处理程序或执行其他DOM操作。对于不支持此事件的浏览器,可以使用超时为0的setTimeout()函数,通过其回调来设置事件处理程序(此代码最好是页面上的第一个超时代码。不过并不一定保证与DOMContentLoaded触发时机一致,也不能保证能在load事件之前执行超时回调) -
readystatechange事件这个有点神秘的事件旨在提供文档或元素加载状态的信息,但行为有时候并不稳定。
支持
readystatechange事件的每个对象都有一个readyState属性,该属性具有一个以下列出的可能的字符串值-
uninitialized对象存在并尚未初始化 -
loading对象正在加载数据 -
loaded对象已经加载完数据 -
interactive对象可以交互,但尚未加载完成 -
complete对象加载完成
看起来很简单,其实并非所有对象都会经历所有
readystate阶段。使用
readystatechange只能尽量模拟DOMContentLoaded,但做不到分毫不差。load事件和readystatechange事件发生的顺序在不同页面中是不一样的 -
-
pageshow&pagehideFirefox和Opera开发了一个名为往返缓存的功能,此功能旨在使用浏览器“前进”和“后退”按钮时加快页面之间的切换。
这个缓存不仅存储页面数据,也存储DOM和JS状态,实际上是把整个页面都保存在内存里。如果页面在缓存中,那么导航到这个页面时就不会触发
load事件。pageshow事件会在页面显示时触发,无论是否来自往返缓存。在新加载的页面上,pageshow会在load事件之后触发;在来自往返缓存的页面上,pageshow会在页面状态完全恢复之后触发。虽然这个事件的目标是
document,但事件处理程序必须添加到window上。除了常用的属性,
pageshow的event对象中还包含一个名为persisted的属性。这个属性是一个布尔值,如果页面存储了往返缓存中就是true,否则就是false。通过检测persisted属性可以根据页面是否取自往返缓存而决定是否采取不同的操作与
pageshow对应的事件是pagehide,这个事件会在页面从浏览器中卸载后,在unload事件之前触发。对于
pagehide事件来说,persisted为true表示页面在卸载之后会被保存在往返缓存中。因此,第一次触发pageshow事件时persisted始终是false,而第一次触发pagehide事件时persisted始终是true(除非页面不符合使用往返缓存的条件)
注册了onunload事件处理程序(即使是空函数)的页面会自动排除在往返缓存之外。 这是因为onunload事件典型的使用场景是撤销onload事件发生时所做的事情。
-
hashchange事件HTML5增加了
hashchange事件,用于在URL散列值(#后面的部分)发生变化时通知开发者。onhashchange事件处理程序必须添加给window,每次URL散列值发生变化时会调用它。event对象有两个新属性:oldURL和newURL。这两个属性分别保存变化前后的URL,而且是包含散列值的完整URL如果想确定当前的散列值,最好使用
location对象(location.hash)
设备事件
-
orientationchange事件Safari浏览器判断用户的设备是处于垂直模式还是水平模式
每当用户旋转设备改变了模式,就会触发
orientationchange事件,但event对象上没有暴露任何有用的信息,这是因为相关信息都可以从window.orientation属性中获取所有IOS设备都支持
orientationchange事件和window.orientation属性 -
deviceorientation事件如果可以获取设备的加速计信息,而且数据发生了变化,这个事件就会在
window上触发。要注意的是,
deviceorientation事件只反映设备在空间中的朝向,而不涉及移动相关的信息 -
devicemotion事件这个事件用于提示设备实际上在移动,而不仅仅是改变了朝向。
例如,
devicemotion事件可以用来确定设备正在掉落或正拿在一个行走的人手里
触摸及手势事件
-
touchstart手指放到屏幕上时触发 -
touchmove手指在屏幕上滑动时连续触发。在这个事件中调用preventDefault()可以阻止滚动 -
touchend手指从屏幕上移开时触发 -
touchcancel系统停止跟踪触摸时触发。文档中并未明确什么情况下停止跟踪
这些事件都会冒泡,也都可以被取消。尽管触摸事件不属于DOM规范,但浏览器仍然以兼容DOM的方式实现了他们。
当手指点触屏幕上的元素时,依次会发生如下事件:
1、touchstart 2、mouseover 3、mousemove 4、mousedown 5、mouseup 6、click 7、touchend
手势事件有以下3种:
-
gesturestart一个手指已经放在屏幕上,再把另一个手指放到屏幕上时触发 -
gesturechange任何一个手指在屏幕上的位置发生变化时触发 -
gestureend其中一个手指离开屏幕时触发
与触摸事件类似,每个手势事件的event对象都包含所有标准的鼠标事件属性: bubbles、cancelable、view、clientX、clientY、screenX、screenY、detail、altKey、shiftKey、ctrlKey和metaKey。新增的两个event对象属性是rotation和scale。
ratation属性表示手指距离变化(对捏)的程度。开始时为1,然后随着距离增大或缩小响应的增大或缩小
无障碍问题
以下是几条使用鼠标事件时应该遵循的无障碍建议:
-
使用
click事件执行代码因为屏幕阅读器无法触发
mousedown事件 -
不要使用
mouseover向用户显示新选项。因为屏幕阅读器无法触发
mousedown事件如果必须要通过这种方式显示新选项,那么可以考虑相同信息的键盘快捷键
-
不要使用
dblclick执行重要的操作。这是因为键盘不能触发这个事件
遵循这些简单的建议可以极大提升Web应用或网站对残障人士的无障碍性
客户端坐标 & 页面坐标 & 屏幕坐标
客户端坐标:event对象的clientX和clientY属性表示事件发生时鼠标光标在视口中的坐标
而页面坐标是事件发生时鼠标光标在页面上的坐标,通过event对象的pageX和pageY可以获取。
在页面没有滚动时,
clientX和clientY与pageX和pageY的值相同
屏幕坐标则代表鼠标光标在屏幕上的坐标,可以通过event对象的screenX和screenY属性获取。
内存与性能
“过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件
利用事件冒泡,事件委托可以解决限制事件处理程序数量的问题。
如果对页面中所有需要使用onclick事件处理程序的元素都如法炮制,结果就会出现大片雷同的只为指定事件处理程序代码。使用事件委托,只要给所有元素共同的祖先节点添加一个事件处理程序,就可以解决问题
只要可行,就应该考虑只给document添加一个事件处理程序,通过它处理页面中所有某种类型的事件。相对于之前的技术,事件委托具有如下优点:
-
document对象随时可用,任何时候都可以给它添加事件处理程序(不用等待DOMContentLoaded或load事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用 -
节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省DOM引用,也可以节省时间。
-
减少整个页面所需的内存,提升整体性能。
最适合使用事件委托的事件包括:click、mousedown、mouseup、keydown和keypress。
mouseover和mouseout事件冒泡,但很难适当处理,且经常需要计算元素位置。
除了通过事件委托来限制这种连接之外,还应该及时删除不用的事件处理程序。很多Web应用性能不佳都是由于无用的事件处理程序长驻内存导致的。
导致这个问题的原因主要有两个。第一个是删除带有事件处理程序的元素。比如通过真正的DOM方法removeChild()或replaceChild()删除节点。最常见的还是使用innerHTML整体替换页面的某一部分。这时候,被innerHTML删除的元素上如果有事件处理程序,就不会被垃圾收集程序正常清理。
如果知道某个元素会被删除,那么最好在删除它之前手工删除它的事件处理程序。 这样就可以确保内存被回收,按钮也可以安全地从DOM中删除。
但也要注意,在事件处理程序中删除按钮会阻止事件冒泡。只有事件目标仍然存在于文档中时,事件才会冒泡。
另一个可能导致内存中残留引用的问题是页面卸载。如果在页面卸载后事件处理程序没有被清理,则他们仍然会残留在内存中。之后,浏览器每次加载和卸载页面(比如通过前进、后退或刷新),内存中残留对象的数量都会增加,这是因为事件处理程序不会被回收。
一般来说,最好在onunload事件处理程序中趁页面尚未卸载先删除所有事件处理程序。这时候也能体现使用事件委托的优势,因为事件处理程序很少,所以很容易记住要删除哪些。
模拟事件
DOM3规范指明了模拟特定类型事件的方式。任何时候,都可以使用document.createEvent(要创建事件类型的字符串)方法创建一个event对象。
在DOM2中,所有这些字符串都是英文复数形式,但在DOM3中,又把他们改成了英文单数形式。
-
UIEvents(DOM3中是UIEvent) 通用用户界面事件(鼠标事件和键盘事件都继承自这个事件) -
MouseEvents(DOM3中是MouseEvent) 通用鼠标事件 -
HTMLEvents(DOM3中没有) 通用HTML事件(HTML事件已经分散到了其他事件大类中)
键盘事件不是在DOM2 Events中规定的,而是后来在DOM3 Events中增加的。
创建event对象之后,需要使用事件相关的信息来初始化。每种类型的event对象都有特定的方法,可以使用相应数据来完成初始化。
事件模拟的最后一步是触发事件。为此要使用dispatchEvent(要触发事件的event对象)方法,这个方法存在于所有支持事件的DOM节点之上。调用dispatchEvent()方法之后,事件就“转正”了,接着便冒泡并触发事件处理程序执行。
模拟鼠标事件
模拟鼠标事件需要先创建一个新的鼠标event对象【createEvent('MouseEvents')】,然后再使用必要的信息对其进行初始化。这个对象有一个initMouseEvent()方法,用于为新对象指定鼠标的特定信息。接收参数如下(一共有15个,这里是列了4个,他们是浏览器要用的,其他参数则是事件处理程序要用的):
-
type字符串,要触发的事件类型,如'click' -
bubbles布尔值,表示事件是否冒泡。为精确模拟鼠标事件,应该设置为true -
cancelable布尔值,表示事件是否可以取消。为精确模拟鼠标事件,应该设置为true -
view(AbstractView)与事件关联的视图。基本上始终是document.defaultView
event对象的target属性会自动设置为调用dispatchEvent()方法时传入的节点。
let btn = document.getElementById('myBtn');
// 创建event对象
let event = document.createEvent('MouseEvents');
// 初始化event对象
event.initMouseEvent('click', true, true, document.defaultView, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
// 触发事件
btn.dispatchEvent(event);
模拟键盘事件
在DOM3中创建键盘事件的方式是createEvent('KeyboardEvent'),返回一个event对象,这个对象有一个initKeyboardEvent()方法,接收以下参数:
-
type字符串,表示要触发的事件类型,如'keydown' -
bubbles布尔值,表示事件是否冒泡 -
cancelable布尔值,表示事件是否可以取消 -
view(AbstractView)与事件关联的视图 -
key字符串,按下按键的字符串代码 -
location -
modifiers -
repeat整数,连续按了这个键多少次
注意,DOM3 Events废弃了keypress事件,因此只能通过上述方式模拟keydown和keyup事件
模拟其他事件
模拟HTML事件要调用createEvent('HTMLEvents'),然后再使用返回对象的initEvent()方法来初始化
// 模拟在给定目标上触发focus事件
let event = document.createEvent('HTMLEvents');
event.initEvent('focus', true, false);
target.dispatchEvent(event);
HTML事件在浏览器中很少使用,因为他们用处有限
自定义DOM事件
DOM3增加了自定义事件的类型。自定义事件不会触发原生DOM事件,但可以让开发者定义自己的事件。
要创建自定义事件,需要调用createEvent('CustomEvent')。返回的对象包含initCustomEvent()方法,该方法接收以下4个参数:
-
type字符串,要触发的事件类型,如'myevent' -
bubbles布尔值,表示事件是否冒泡 -
cancelable布尔值,表示事件是否可以取消 -
detail对象,任意值。作为event对象的detail属性
自定义事件可以像其他事件一样在DOM中派发,比如:
let div = document.getElementById('myDiv');
let event;
div.addEventListener('myevent', event => {
console.log('DIV:' + event.detail);
})
document.addEventListener('myevent', event => {
console.log('DOCUMENT:' + event.detail);
})
if(document.implementation.hasFeature('CustomEvents', '3.0')) {
event = document.createEvent('CustomEvent');
event.initCustomEvent('myevent', true, false, 'Hello world!');
div.dispatchEvent(event);
}
这个例子创建了一个名为'myevent'的冒泡事件。event对象的detail属性就是一个简单的字符串,<div>元素和document都为这个事件注册了事件处理程序。因为使用initCustomEvent()初始化时将事件指定为可以冒泡,所以浏览器会负责把事件冒泡到document