DOM事件

209 阅读30分钟

JS与HTML的交互是通过事件实现的。

可以使用仅在事件发生时执行的监听器订阅事件。在传统软件工程领域,这个模型叫“观察者模式”,其能够做到页面行为与页面展示的分离

浏览器的事件系统非常复杂。即使所有主流浏览器都实现了DOM2 Events,规范也没有涵盖所有的事件类型。

BOM也支持事件,这些事件与DOM事件之间的关系由于长期以来缺乏文档,经常容易被混淆。而DOM3新增的事件API又让这些问题进一步复杂化了。

事件流

事件流描述了页面接收事件的顺序。DOM2 Events规范规定事件流分为3个阶段:事件捕获、到达目标和事件冒泡

事件捕获最先发生,为提前拦截事件提供了可能。然后,实现的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。

IE支持事件冒泡流,而Netscape Communicator将支持事件捕获流

所有现代浏览器都支持事件冒泡,只是在实现方式上会有一些变化。现代浏览器中的事件会一直冒泡到window对象

事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。

事件捕获实际上是为了在事件到达最终目标前拦截事件。

实际上,所有浏览器都是从window对象开始捕获事件,而DOM2 Events规范规定的是从document开始

由于旧版本浏览器不支持,因此实际当中几乎不会使用事件捕获。通常建议使用事件冒泡,特殊情况下可以使用事件捕获

事件处理程序

为响应事件而调用的函数被称为事件处理程序(或事件监听器),以on开头,比如onclickonload

特定元素支持的每个事件都可以使用事件处理程序的名字以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只包含事件的实际目标

    如果事件处理程序直接添加在了意图的目标,则thiscurrentTargettarget的值是一样的

  • trusted true表示事件是由浏览器生成的。false表示事件是开发者通过JS创建的

  • eventPhase 表示调用事件处理程序的阶段:1代表捕获阶段,2代表到达目标,3代表冒泡阶段

  • preventDefault() 用于取消事件的默认行为。只有cancelabletrue才可以调用这个方法

  • stopImmediatePropagation() 用于取消所有后续事件捕获或事件冒泡,并阻止调用任何后续事件处理程序

  • stopPropagation() 用于取消所有后续事件捕获或事件冒泡。只有bubblestrue才可以调用这个方法

事件类型

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>元素检测scrollLeftscrollTop属性的变化。而在标准模式下,这些变化在除早期版的Safari之外的所有浏览器中都发生在<html>元素上

    window.addEventListener('scroll', event => {
        if(document.compatMode === 'CSS1Compat') {
            console.log(document.documentElement.scrollTop);
        } else {
            console.log(document.body.scrollTop);
        }
    })
    

    类似于resizescroll事件也会随着文档滚动而重复触发,因此最好保持事件处理程序的代码尽可能简单

焦点事件

焦点事件在页面元素获得或失去焦点时触发。

焦点事件中的两个主要事件是focusblue,这两个事件在JS早期就得到了浏览器支持。

它们最大的问题是不冒泡。这导致IE后来又增加了focusinfocusout,Opera又增加了DOMFocusInDOMFocusOut

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 在鼠标光标在元素上移动时反复触发

页面中的所有元素都支持鼠标事件。

mousedownmouseup事件来说,event对象上会有一个button属性,表示按下或释放的是哪个按键。

DOM为这个button属性定义了3个值:0表示鼠标主键(左键)、1表示鼠标中键(通常也是滚轮键)、2表示鼠标副键(右键)

除了mouseentermouseleave,所有鼠标事件都会冒泡,都可以被取消,而这会影响浏览器的默认行为

由于事件之间存在关系,因此取消鼠标事件的默认行为也会影响其他事件

比如,click事件触发的前提是mousedown事件触发后,紧接着又在同一个元素上触发了mouseup事件。如果mousedownmouseup中的任意一个事件被取消,那么click事件就不会触发。

类似地,两次连续的click事件会导致dblclick事件触发。只要有任何逻辑阻止了这两个click事件发生(比如取消其中一个click事件或取消mousedownmouseup事件中的任一个),dblclick事件就不会发生。这4个事件永远会按照如下顺序触发:

1、mousedown 2、mouseup 3、click 4、mousesown 5、mouseup 6、click 7、dblclick

DOM通过event对象的relatedTarget属性提供了相关元素的信息。这个属性只有在mouseovermouseout事件发生时才包含值。其他是有事件的这个属性的值都是null

相关元素:比如,对Mouseover事件来说,事件的主要目标是获得光标的元素,相关元素是失去光标的元素

鼠标事件还有一个名为滚轮事件的子类别。滚轮事件只有一个事件mousewheel,反应的鼠标滚动或带滚轮的类似设备上滚轮的交互

mousewheel事件会在用户使用鼠标滚轮时触发,包括在垂直方向上任意滚动。

这个事件会在任何元素上触发,并(在IE8中)冒泡到document和(在所有现代浏览器中)window

mousewheel事件的event对象包含鼠标事件的所有标准信息,此外还有一个名为wheelDelta的新属性。当鼠标滚轮向前滚动时,wheelDelta每次都是+120;而当鼠标滚轮向后滚动时,wheelDelta每次都是-120

多数情况下只需知道滚轮滚动的方向,而这通过wheelDelta值的符号就可以知道

触摸屏设备

  • 不支持dblclick事件。双击浏览器窗口可以放大,但没有办法覆盖这个行为

  • 单指点触屏幕上的可点击元素会触发mousemove事件。如果操作会导致内容变化,则不会再触发其他事件。

    如果屏幕上没有变化,则会相继触发mousedownmouseupclick事件

  • mousemove事件也会触发mouseovermouseout事件

  • 双指点触屏幕并滑动导致页面滚动时会触发mousewheelscroll事件

键盘与输入事件

  • keydown 用户按下键盘上某个键时触发,而且持续按住会重复触发

  • keypress 用户按下键盘上某个键并产生字符时触发,而且持续按住会重复触发。Esc键也会触发这个事件

    DOM3 Events废弃了keypress事件,而推荐textInput事件

  • keyup 用户释放键盘上某个键时触发

  • textInput 输入事件,这个事件是对keypress事件的扩展,用于在文本显示给用户之前更方便地截获文本输入

    textInput会在文本被插入到文本框之前触发

    作为对keypress的替代,textInput事件的行为有些不一样。一个区别是keypress会在任何可以获得焦点的元素上触发,而textInput只在可编辑区域上触发。

    另一个区别是textInput只在有新字符被插入时才会触发,而keypress对任何可能影响文本的键都会触发

    因为textInput事件主要关注字符,所以在event对象上提供了一个data属性,包含要插入的字符(不是字符编码)

对于keydownkeyup事件,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 事件

    windowload事件会再页面完全加载后触发,因为要等待很多外部资源加载完成,所以会花费较长事件。

    DOMContentLoaded事件会再DOM树构建完成后立即触发,而不用等待图片、JS文件、CSS文件或其他资源加载完成。 相对于load事件,DOMContentLoaded可以让开发者在外部资源下载的同时就能指定事件处理程序,从而让用户能够更快地与页面交互

    DOMContentLoaded事件的event对象中不包含任何额外信息,该事件通常用于添加事件处理程序或执行其他DOM操作。对于不支持此事件的浏览器,可以使用超时为0setTimeout()函数,通过其回调来设置事件处理程序(此代码最好是页面上的第一个超时代码。不过并不一定保证与DOMContentLoaded触发时机一致,也不能保证能在load事件之前执行超时回调)

  • readystatechange 事件

    这个有点神秘的事件旨在提供文档或元素加载状态的信息,但行为有时候并不稳定。

    支持readystatechange事件的每个对象都有一个readyState属性,该属性具有一个以下列出的可能的字符串值

    • uninitialized 对象存在并尚未初始化

    • loading 对象正在加载数据

    • loaded 对象已经加载完数据

    • interactive 对象可以交互,但尚未加载完成

    • complete 对象加载完成

    看起来很简单,其实并非所有对象都会经历所有readystate阶段。

    使用readystatechange只能尽量模拟DOMContentLoaded,但做不到分毫不差。load事件和readystatechange事件发生的顺序在不同页面中是不一样的

  • pageshow & pagehide

    Firefox和Opera开发了一个名为往返缓存的功能,此功能旨在使用浏览器“前进”和“后退”按钮时加快页面之间的切换。

    这个缓存不仅存储页面数据,也存储DOM和JS状态,实际上是把整个页面都保存在内存里。如果页面在缓存中,那么导航到这个页面时就不会触发load事件。

    pageshow事件会在页面显示时触发,无论是否来自往返缓存。在新加载的页面上,pageshow会在load事件之后触发;在来自往返缓存的页面上,pageshow会在页面状态完全恢复之后触发。

    虽然这个事件的目标是document,但事件处理程序必须添加到window上。

    除了常用的属性,pageshowevent对象中还包含一个名为persisted的属性。这个属性是一个布尔值,如果页面存储了往返缓存中就是true,否则就是false。通过检测persisted属性可以根据页面是否取自往返缓存而决定是否采取不同的操作

    pageshow对应的事件是pagehide,这个事件会在页面从浏览器中卸载后,在unload事件之前触发。

    对于pagehide事件来说,persistedtrue表示页面在卸载之后会被保存在往返缓存中。因此,第一次触发pageshow事件时persisted始终是false,而第一次触发pagehide事件时persisted始终是true(除非页面不符合使用往返缓存的条件)

注册了onunload事件处理程序(即使是空函数)的页面会自动排除在往返缓存之外。 这是因为onunload事件典型的使用场景是撤销onload事件发生时所做的事情。

  • hashchange 事件

    HTML5增加了hashchange事件,用于在URL散列值(#后面的部分)发生变化时通知开发者。

    onhashchange事件处理程序必须添加给window,每次URL散列值发生变化时会调用它。event对象有两个新属性:oldURLnewURL。这两个属性分别保存变化前后的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对象都包含所有标准的鼠标事件属性: bubblescancelableviewclientXclientYscreenXscreenYdetailaltKeyshiftKeyctrlKeymetaKey。新增的两个event对象属性是rotationscale

ratation属性表示手指距离变化(对捏)的程度。开始时为1,然后随着距离增大或缩小响应的增大或缩小

无障碍问题

以下是几条使用鼠标事件时应该遵循的无障碍建议:

  • 使用click事件执行代码

    因为屏幕阅读器无法触发mousedown事件

  • 不要使用mouseover向用户显示新选项。

    因为屏幕阅读器无法触发mousedown事件

    如果必须要通过这种方式显示新选项,那么可以考虑相同信息的键盘快捷键

  • 不要使用dblclick执行重要的操作。

    这是因为键盘不能触发这个事件

遵循这些简单的建议可以极大提升Web应用或网站对残障人士的无障碍性

客户端坐标 & 页面坐标 & 屏幕坐标

客户端坐标event对象的clientXclientY属性表示事件发生时鼠标光标在视口中的坐标

页面坐标是事件发生时鼠标光标在页面上的坐标,通过event对象的pageXpageY可以获取。

在页面没有滚动时,clientXclientYpageXpageY的值相同

屏幕坐标则代表鼠标光标在屏幕上的坐标,可以通过event对象的screenXscreenY属性获取。

内存与性能

“过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件

利用事件冒泡,事件委托可以解决限制事件处理程序数量的问题。

如果对页面中所有需要使用onclick事件处理程序的元素都如法炮制,结果就会出现大片雷同的只为指定事件处理程序代码。使用事件委托,只要给所有元素共同的祖先节点添加一个事件处理程序,就可以解决问题

只要可行,就应该考虑只给document添加一个事件处理程序,通过它处理页面中所有某种类型的事件。相对于之前的技术,事件委托具有如下优点:

  • document对象随时可用,任何时候都可以给它添加事件处理程序(不用等待DOMContentLoadedload事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用

  • 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省DOM引用,也可以节省时间。

  • 减少整个页面所需的内存,提升整体性能。

最适合使用事件委托的事件包括:clickmousedownmouseupkeydownkeypress

mouseovermouseout事件冒泡,但很难适当处理,且经常需要计算元素位置。

除了通过事件委托来限制这种连接之外,还应该及时删除不用的事件处理程序。很多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事件,因此只能通过上述方式模拟keydownkeyup事件

模拟其他事件

模拟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