第17章 事件

本章内容

  • 理解事件流
  • 使用事件处理程序
  • 了解不同类型的事件

JavaScript 与 HTML 的交互是通过事件实现的,事件代表文档或浏览器窗口中某个有意义的时刻。
可以使用仅在事件发生时执行的监听器(也叫处理程序)订阅事件。在传统软件工程领域,这个模型叫“观察者模式”,其能够做到页面行为(在 JavaScript 中定义)与页面展示(在 HTML 和 CSS 中定义)的分离。

1 事件流

1.1 事件冒泡

IE 事件流被称为事件冒泡,现代浏览器中的事件会一直冒泡到 window 对象。

1.2 事件捕获

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

1.3 DOM 事件流

DOM2 Events 规范规定事件流分为 3 个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生,为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。

2 事件处理程序

事件意味着用户或浏览器执行的某种动作。比如,单击(click)、加载(load)、鼠标悬停(mouseover)。为响应事件而调用的函数被称为事件处理程序(或事件监听器)。事件处理程序的名字以"on"开头,因此 click 事件的处理程序叫作 onclick,而 load 事件的处理程序叫作 onload。

2.1 HTML 事件处理程序

<input type="button" value="Click Me" onclick="console.log('Clicked')"/>

不能在未经转义的情况下使用 HTML 语法字符,比如和号(&)、双引号(")、小于号(<)和大于号(>)。可以这样:

<input type="button" value="Click Me" 
 onclick="console.log(&quot;Clicked&quot;)"/>

this 值相当于事件的目标元素,如下面的例子所示:

<!-- 输出"Click Me" --> 
<input type="button" value="Click Me" onclick="console.log(this.value)">

这里的作用域链被扩展了。在这个函数中,document 和元素自身的成员都可以被当成局部变量来访问。这是通过使用 with 实现的:

 with(document) { 
 with(this) { 
 // 属性值
 } 
 } 
}

在 HTML 中指定事件处理程序有一些问题。第一个问题是时机问题。有可能 HTML 元素已经显示在页面上,用户都与其交互了,而事件处理程序的代码还无法执行。(函数在按钮中代码的后面定义的)。为此,大多数 HTML 事件处理程序会封装在 try/catch 块中,以便在这种情况下静默失败,如下面的例子所示:

<input type="button" value="Click Me" onclick="try{showMessage();}catch(ex) {}">

2.2 DOM0 事件处理程序

let btn = document.getElementById("myBtn"); 
btn.onclick = function() { 
 console.log("Clicked"); 
};
btn.onclick = null; // 移除事件处理程序

2.3 DOM2 事件处理程序

addEventListener()和 removeEventListener()。这两个方法暴露在所有 DOM 节点上,它们接收 3 个参数:事件名、事件处理函数和一个布尔值,true 表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序。

通过 addEventListener()添加的事件处理程序只能使用 removeEventListener()并传入与添
加时同样的参数来移除。这意味着使用 addEventListener()添加的匿名函数无法移除,如下面的例
子所示:

let btn = document.getElementById("myBtn"); 
btn.addEventListener("click", () => { 
 console.log(this.id); 
 }, false);
// 其他代码
btn.removeEventListener("click", function() { // 没有效果!
 console.log(this.id); 
}, false);
let btn = document.getElementById("myBtn"); 
let handler = function() { 
 console.log(this.id); 
}; 
btn.addEventListener("click", handler, false);
// 其他代码
btn.removeEventListener("click", handler, false); // 有效果!

把事件处理程序注册到捕获阶段通常用于在事件到达其指定目标之前拦截事件。如果不需要拦截,则不要使用事件捕获。

2.4 IE 事件处理程序

attachEvent()和 detachEvent()。这两个方法接收两个同样的参数:事件处理程序的名字和事件处理函数。因为 IE8 及更早版本只支持事件冒泡,所以使用attachEvent()添加的事件处理程序会添加到冒泡阶段。

var btn = document.getElementById("myBtn"); 
var handler = function() { 
 console.log("Clicked"); 
}; 
btn.attachEvent("onclick", handler);  // 注意是onclick
// 其他代码
btn.detachEvent("onclick", handler);

2.5 跨浏览器事件处理程序

为了以跨浏览器兼容的方式处理事件,很多开发者会选择使用一个 JavaScript 库,其中抽象了不同
浏览器的差异。有些开发者也可能会自己编写代码,以便使用最合适的事件处理手段。自己编写跨浏览器事件处理代码也很简单,主要依赖能力检测。要确保事件处理代码具有最大兼容性,只需要让代码在冒泡阶段运行即可。

为此,需要先创建一个 addHandler()方法。这个方法的任务是根据需要分别使用 DOM0 方式、
DOM2 方式或 IE 方式来添加事件处理程序。这个方法会在 EventUtil 对象(本章示例使用的对象)上
添加一个方法,以实现跨浏览器事件处理。添加的这个 addHandler()方法接收 3 个参数:目标元素、
事件名和事件处理函数。

有了 addHandler(),还要写一个也接收同样的 3 个参数的 removeHandler()。这个方法的任务
是移除之前添加的事件处理程序,不管是通过何种方式添加的,默认为 DOM0 方式。

var EventUtil = { 
 addHandler: function(element, type, handler) { 
	 if (element.addEventListener) { 
	 element.addEventListener(type, handler, false); 
	 } else if (element.attachEvent) { 
	 element.attachEvent("on" + type, handler); 
	 } else { 
	 element["on" + type] = handler; 
	 } 
 }, 
 removeHandler: function(element, type, handler) { 
	 if (element.removeEventListener) { 
	 element.removeEventListener(type, handler, false); 
	 } else if (element.detachEvent) { 
	 element.detachEvent("on" + type, handler); 
	 } else { 
	 element["on" + type] = null; 
	 } 
 } 
};

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

let btn = document.getElementById("myBtn") 
let handler = function() { 
 console.log("Clicked"); 
}; 
EventUtil.addHandler(btn, "click", handler); 
// 其他代码
EventUtil.removeHandler(btn, "click", handler);

3 事件对象

在 DOM 中发生事件时,所有相关信息都会被收集并存储在一个名为 event 的对象中。这个对象包含了一些基本信息,比如导致事件的元素、发生的事件类型,以及可能与特定事件相关的任何其他数据。

3.1 DOM 事件对象

event 对象是传给事件处理程序的唯一参数。

let btn = document.getElementById("myBtn"); 
btn.onclick = function(event) { 
 console.log(event.type); // "click" 
}; 
btn.addEventListener("click", (event) => { 
 console.log(event.type); // "click" 
}, false);

3.2 IE 事件对象

与 DOM 事件对象不同, IE 事件对象可以基于事件处理程序被指定的方式以不同方式来访问。如果事件处理程序是使用 DOM0 方式指定的,则 event 对象只是 window 对象的一个属性,如下所示:

var btn = document.getElementById("myBtn"); 
btn.onclick = function() { 
 let event = window.event; 
 console.log(event.type); // "click" 
};

3.3 跨浏览器事件对象

虽然 DOM 和 IE 的事件对象并不相同,但它们有足够的相似性可以实现跨浏览器方案。DOM 事件对象中包含 IE 事件对象的所有信息和能力,只是形式不同。

4 事件类型

DOM3 Events 定义了如下事件类型。
 用户界面事件(UIEvent):涉及与 BOM 交互的通用浏览器事件。
 焦点事件(FocusEvent):在元素获得和失去焦点时触发。
 鼠标事件(MouseEvent):使用鼠标在页面上执行某些操作时触发。
 滚轮事件(WheelEvent):使用鼠标滚轮(或类似设备)时触发。
 输入事件(InputEvent):向文档中输入文本时触发。
 键盘事件(KeyboardEvent):使用键盘在页面上执行某些操作时触发。
 合成事件(CompositionEvent):在使用某种 IME(Input Method Editor,输入法编辑器)输入字符时触发。

4.1 用户界面事件

UI 事件主要有以下几种。

  • DOMActivate:元素被用户通过鼠标或键盘操作激活时触发(比 click 或 keydown 更通用)。这个事件在 DOM3 Events 中已经废弃。因为浏览器实现之间存在差异,所以不要使用它。
  • load:在 window 上当页面加载完成后触发,在窗套(frameset)上当所有窗格(frame)都加载完成后触发,在img元素上当图片加载完成后触发,在object元素上当相应对象加载完成后触发。
  • unload:在 window 上当页面完全卸载后触发,在窗套上当所有窗格都卸载完成后触发,在object元素上当相应对象卸载完成后触发。
  • abort:在object元素上当相应对象加载完成前被用户提前终止下载时触发。
  • error:在 window 上当 JavaScript 报错时触发,在img元素上当无法加载指定图片时触发,在object元素上当无法加载相应对象时触发,在窗套上当一个或多个窗格无法完成加载时触发。
  • select:在文本框(input或 textarea)上当用户选择了一个或多个字符时触发。
  • resize:在 window 或窗格上当窗口或窗格被缩放时触发。
  • scroll:当用户滚动包含滚动条的元素时在元素上触发。body元素包含已加载页面的滚动条。大多数 HTML 事件与 window 对象和表单控件有关。

1. load 事件

在 window 对象上,load 事件会在整个页面(包括所有外部资源如图片、JavaScript 文件和 CSS 文件)加载完成后触发。可以通过两种方式指定 load 事件处理程序。第一种是 JavaScript 方式,如下所示:

window.addEventListener("load", (event) => { 
 console.log("Loaded!"); 
});

第二种:

<body onload="console.log('Loaded!')"> 
</body>

一般来说,任何在 window 上发生的事件,都可以通过给元素上对应的属性赋值来指定,这是因为 HTML 中没有 window 元素。这实际上是为了保证向后兼容的一个策略,但在所有浏览器中都能得到很好的支持。实际开发中要尽量使用 JavaScript 方式。

2. unload 事件

unload 事件一般是在从一个页面导航到另一个页面时触发,最常用于清理引用,以避免内存泄漏。

window.addEventListener("unload", (event) => { 
 console.log("Unloaded!"); 
});
<body onunload="console.log('Unloaded!')">

3. resize 事件

当浏览器窗口被缩放到新高度或宽度时,会触发 resize 事件。

4. scroll 事件

在混杂模式下,可以通过body元素检测 scrollLeft 和 scrollTop 属性的变化。而在标准模式下,这些变化在除早期版的 Safari 之外的所有浏览器中都发生在html元素上(早期版的 Safari 在body上跟踪滚动位置)。下面的代码演示了如何处理这些差异:

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

4.2 焦点事件

焦点事件在页面元素获得或失去焦点时触发。这些事件可以与 document.hasFocus()和document.activeElement 一起为开发者提供用户在页面中导航的信息。焦点事件有以下 6 种。
 blur:当元素失去焦点时触发。这个事件不冒泡,所有浏览器都支持。
 DOMFocusIn:当元素获得焦点时触发。这个事件是 focus 的冒泡版。Opera 是唯一支持这个事件的主流浏览器。DOM3 Events 废弃了 DOMFocusIn,推荐 focusin。
 DOMFocusOut:当元素失去焦点时触发。这个事件是 blur 的通用版。Opera 是唯一支持这个事件的主流浏览器。DOM3 Events 废弃了 DOMFocusOut,推荐 focusout。
 focus:当元素获得焦点时触发。这个事件不冒泡,所有浏览器都支持。
 focusin:当元素获得焦点时触发。这个事件是 focus 的冒泡版。
 focusout:当元素失去焦点时触发。这个事件是 blur 的通用版。

当焦点从页面中的一个元素移到另一个元素上时,会依次发生如下事件。
(1) focuscout 在失去焦点的元素上触发。
(2) focusin 在获得焦点的元素上触发。
(3) blur 在失去焦点的元素上触发。
(4) DOMFocusOut 在失去焦点的元素上触发。
(5) focus 在获得焦点的元素上触发。
(6) DOMFocusIn 在获得焦点的元素上触发。
其中,blur、DOMFocusOut 和 focusout 的事件目标是失去焦点的元素,而 focus、DOMFocusIn和 focusin 的事件目标是获得焦点的元素。

4.3 鼠标和滚轮事件

DOM3 Events定义了 9 种鼠标事件。

 click:在用户单击鼠标主键(通常是左键)或按键盘回车键时触发。这主要是基于无障碍的考虑,让键盘和鼠标都可以触发 onclick 事件处理程序。
 dblclick:在用户双击鼠标主键(通常是左键)时触发。这个事件不是在 DOM2 Events 中定义的,但得到了很好的支持,DOM3 Events 将其进行了标准化。
 mousedown:在用户按下任意鼠标键时触发。这个事件不能通过键盘触发。
 mouseenter:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。mouseenter 事件不是在 DOM2 Events 中定义的,而是 DOM3 Events中新增的事件。
 mouseleave:在用户把鼠标光标从元素内部移到元素外部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。mouseleave 事件不是在 DOM2 Events 中定义的,而是 DOM3 Events中新增的事件。
 mousemove:在鼠标光标在元素上移动时反复触发。这个事件不能通过键盘触发。
 mouseout:在用户把鼠标光标从一个元素移到另一个元素上时触发。移到的元素可以是原始元素的外部元素,也可以是原始元素的子元素。这个事件不能通过键盘触发。
 mouseover:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不能通过键盘触发。
 mouseup:在用户释放鼠标键时触发。这个事件不能通过键盘触发。

除了 mouseenter 和 mouseleave,所有鼠标事件都会冒泡,都可以被取消,而这会影响浏览器的默认行为。比如,click 事件触发的前提是 mousedown 事件触发后,紧接着又在同一个元素上触发了mouseup事件。如果 mousedown 和 mouseup 中的任意一个事件被取消,那么 click 事件就不会触发。

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

1. 客户端坐标

鼠标事件都是在浏览器视口中的某个位置上发生的。这些信息被保存在 event 对象的 clientX 和clientY 属性中。这两个属性表示事件发生时鼠标光标在视口中的坐标,所有浏览器都支持。

可以通过下面的方式获取鼠标事件的客户端坐标:

let div = document.getElementById("myDiv"); 
div.addEventListener("click", (event) => { 
 console.log(`Client coordinates: ${event.clientX}, ${event.clientY}`); 
});

注意客户端坐标不考虑页面滚动,因此这两个值并不代表鼠标在页面上的位置。

2. 页面坐标

客户端坐标是事件发生时鼠标光标在客户端视口中的坐标,而页面坐标是事件发生时鼠标光标在页面上的坐标,通过 event 对象的 pageX 和 pageY 可以获取。这两个属性表示鼠标光标在页面上的位置,因此反映的是光标到页面而非视口左边与上边的距离。

可以像下面这样取得鼠标事件的页面坐标:

let div = document.getElementById("myDiv"); 
div.addEventListener("click", (event) => { 
 console.log(`Page coordinates: ${event.pageX}, ${event.pageY}`); 
});

3. 屏幕坐标

let div = document.getElementById("myDiv"); 
div.addEventListener("click", (event) => { 
 console.log(`Screen coordinates: ${event.screenX}, ${event.screenY}`); 
});

4. 修饰键

键盘上的修饰键 Shift、Ctrl、Alt 和 Meta 经常用于修改鼠标事件的行为。event.shiftKey、event.ctrlKey、event.altKey 和 event.metaKey。

5. 相关元素

对 mouseover 事件来说,事件的主要目标是获得光标的元素,相关元素是失去光标的元素。类似地,对 mouseout 事件来说,事件的主要目标是失去光标的元素,而相关元素是获得光标的元素。

6. 鼠标按键

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

7. 额外事件信息

DOM2 Events 规范在 event 对象上提供了 detail 属性包含一个数值,表示在给定位置上发生了多少次单击。如果鼠标在 mousedown
和 mouseup 之间移动了,则 detail 会重置为 0。

8. mousewheel 事件

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

9. 触摸屏设备

iOS 和 Android 等触摸屏设备的实现大相径庭,因为触摸屏通常不支持鼠标操作。
 不支持 dblclick 事件。双击浏览器窗口可以放大,但没有办法覆盖这个行为。
 单指点触屏幕上的可点击元素会触发 mousemove 事件。如果操作会导致内容变化,则不会再触发其他事件。如果屏幕上没有变化,则会相继触发 mousedown、mouseup 和 click 事件。点触不可点击的元素不会触发事件。可点击元素是指点击时有默认动作的元素(如链接)或指定了 onclick 事件处理程序的元素。
 mousemove 事件也会触发 mouseover 和 mouseout 事件。
 双指点触屏幕并滑动导致页面滚动时会触发 mousewheel 和 scroll 事件

10. 无障碍问题

 使用 click 事件执行代码。有人认为,当使用 onmousedown 执行代码时,应用程序会运行得更快。对视力正常用户来说确实如此。但在屏幕阅读器上,这样会导致代码无法执行,这是因为屏幕阅读器无法触发 mousedown 事件。
 不要使用 mouseover 向用户显示新选项。同样,原因是屏幕阅读器无法触发 mousedown 事件。如果必须要通过这种方式显示新选项,那么可以考虑显示相同信息的键盘快捷键。
 不要使用 dblclick 执行重要的操作,这是因为键盘不能触发这个事件。
遵循这些简单的建议可以极大提升 Web 应用或网站对残障人士的无障碍性。

4.4 键盘与输入事件

键盘事件包含 3 个事件:
 keydown,用户按下键盘上某个键时触发,而且持续按住会重复触发。
 keypress,用户按下键盘上某个键并产生字符时触发,而且持续按住会重复触发。Esc 键也会触发这个事件。DOM3 Events 废弃了 keypress 事件,而推荐 textInput 事件。
 keyup,用户释放键盘上某个键时触发。
当用户按下键盘上的某个字符键时,首先会触发 keydown 事件,然后触发 keypress 事件,最后触发 keyup 事件。注意,这里 keydown 和 keypress 事件会在文本框出现变化之前触发,而 keyup事件会在文本框出现变化之后触发。如果一个字符键被按住不放,keydown 和 keypress 就会重复触发,直到这个键被释放。
对于非字符键,在键盘上按一下这个键,会先触发 keydown 事件,然后触发 keyup 事件。如果按住某个非字符键不放,则会重复触发 keydown 事件,直到这个键被释放,此时会触发 keyup 事件。

1. 键码

对于 keydown 和 keyup 事件,event 对象的 keyCode 属性中会保存一个键码,对应键盘上特定的一个键。对于字母和数字键,keyCode 的值与小写字母和数字的 ASCII 编码一致。比如数字 7 键的keyCode 为 55,而字母 A 键的 keyCode 为 65,而且跟是否按了 Shift 键无关。

2. 字符编码

浏览器在 event 对象上支持 charCode 属性,只有发生 keypress 事件时这个属性才会被设置值,包含的是按键字符对应的 ASCII 编码。

3. DOM3 的变化

DOM3 Events 规范并未规定 charCode 属性,而是定义了 key 和 char 两个新属性。由于缺乏跨浏览器支持,因此不建议使用 key、keyIdentifier、,location 属性、和 char。

4. textInput 事件

一个区别是 keypress 会在任何可以获得焦点的元素上触发,而 textInput 只在可编辑区域上触发。另一个区别是 textInput 只在有新字符被插入时才会触发,而 keypress 对任何可能影响文本的键都会触发(包括退格键)。

let textbox = document.getElementById("myText"); 
textbox.addEventListener("textInput", (event) => { 
 console.log(event.data); 
});

event 对象上还有一个名为 inputMethod 的属性,该属性表示向控件中输入文本的手段。可能的值如下:
 0,表示浏览器不能确定是什么输入手段;
 1,表示键盘;
 2,表示粘贴;
 3,表示拖放操作;
 4,表示 IME;
 5,表示表单选项;
 6,表示手写(如使用手写笔);
 7,表示语音;
 8,表示组合方式;
 9,表示脚本。
使用这些属性,可以确定用户是如何将文本输入到控件中的。

4.5 合成事件

合成事件是 DOM3 Events 中新增的,用于处理通常使用 IME 输入时的复杂输入序列。IME 可以让用户输入物理键盘上没有的字符。例如,使用拉丁字母键盘的用户还可以使用 IME 输入日文。IME 通常需要同时按下多个键才能输入一个字符。合成事件用于检测和控制这种输入。合成事件有以下 3 种:
 compositionstart,在 IME 的文本合成系统打开时触发,表示输入即将开始;
 compositionupdate,在新字符插入输入字段时触发;
 compositionend,在 IME 的文本合成系统关闭时触发,表示恢复正常键盘输入。

let textbox = document.getElementById("myText"); 
textbox.addEventListener("compositionstart", (event) => { 
 console.log(event.data); // 正在编辑的文本(例如,已经选择了文本但还没替换
});

4.6 变化事件

DOM2 的变化事件(Mutation Events)是为了在 DOM 发生变化时提供通知。变化事件已经被Mutation Observers 所取代,

4.7 HTML5 事件

1. contextmenu 事件

contextmenu 事件,以专门用于表示何时该显示上下文菜单,从而允许开发者取消默认的上下文菜单并提供自定义菜单。

div.addEventListener("contextmenu", (event) => { 
 event.preventDefault(); 
 let menu = document.getElementById("myMenu"); 
 menu.style.left = event.clientX + "px"; 
 menu.style.top = event.clientY + "px"; 
 menu.style.visibility = "visible"; 
 });

2. beforeunload 事件

beforeunload 事件会在 window 上触发,用意是给开发者提供阻止页面被卸载的机会。这个事件会在页面即将从浏览器中卸载时触发。

3. DOMContentLoaded 事件

DOMContentLoaded 事件会在 DOM 树构建完成后立即触发,而不用等待图片、JavaScript文件、CSS 文件或其他资源加载完成。可以让开发者在外部资源下载的同时就能指定事件处理程序,从而让用户能够更快地与页面交互。DOMContentLoaded 事件通常用于添加事件处理程序或执行其他 DOM操作。这个事件始终在 load事件之前触发。

4. readystatechange 事件

提供文档或元素加载状态的信息,但行为有时候并不稳定。支持 readystatechange 事件的每个对象都有一个 readyState 属性,该属性具有一个以下列出的可能的字符串值。
 uninitialized:对象存在并尚未初始化。
 loading:对象正在加载数据。
 loaded:对象已经加载完数据。
 interactive:对象可以交互,但尚未加载完成。
 complete:对象加载完成

5. pageshow 与 pagehide 事件

Firefox 和 Opera 开发了一个名为往返缓存(bfcache,back-forward cache)的功能,此功能旨在使用浏览器“前进”和“后退”按钮时加快页面之间的切换。
Firefx提供一些事件,把往返缓存的行为暴露出来。
第一个事件是 pageshow,其会在页面显示时触发,无论是否来自往返缓存。在新加载的页面上,pageshow 会在 load 事件之后触发;在来自往返缓存的页面上,pageshow 会在页面状态完全恢复后触发。pageshow 的 event 对象中还包含一个名为 persisted 的属性。这个属性是一个布尔值,如果页面存储在了往返缓存中就是 true,否则就是 false。

与 pageshow 对应的事件是 pagehide,这个事件会在页面从浏览器中卸载后,在 unload 事件之前触发。pagehide 事件同样是在 document 上触发,但事件处理程序必须被添加到 window。

6. hashchange 事件

hashchange 事件,用于在 URL 散列值(URL 最后#后面的部分)发生变化时通知发者。这是因为开发者经常在 Ajax 应用程序中使用 URL 散列值存储状态信息或路由导航信息。

window.addEventListener("hashchange", (event) => { 
 console.log(`Old URL: ${event.oldURL}, New URL: ${event.newURL}`); 
});

4.8 设备事件

1. orientationchange 事件

移动 Safari 浏览器上创造了 orientationchange 事件用于判断用户的设备是处于垂直模式还是水平模式。0 表示垂直模式,90 表示左转水平模式(主屏幕键在右侧),–90 表示右转水平模式(主屏幕键在左)。

2. deviceorientation 事件

如果可以获取设备的加速计信息,而且数据发生了变化,这个事件就会在 window 上触发。要注意的是,deviceorientation 事件只反映设备在空间中的朝向,而不涉及移动相关的信息。
基于这些信息,可以随着设备朝向的变化重新组织或修改屏幕上显示的元素。例如,以下代码会随着朝向变化旋转一个元素:

window.addEventListener("deviceorientation", (event) => { 
 let arrow = document.getElementById("arrow"); 
 arrow.style.webkitTransform = `rotate(${Math.round(event.alpha)}deg)`; 
});

3. devicemotion 事件

这个事件用于提示设备实际上在移动。例如,devicemotion 事件可以用来确定设备正在掉落或者正拿在一个行走的人手里。

4.9 触摸及手势事件

本节介绍的事件只适用于触屏设备。

1. 触摸事件

 touchstart:手指放到屏幕上时触发(即使有一个手指已经放在了屏幕上)。
 touchmove:手指在屏幕上滑动时连续触发。在这个事件中调用 preventDefault()可以阻止滚动。
 touchend:手指从屏幕上移开时触发。
 touchcancel:系统停止跟踪触摸时触发。文档中并未明确什么情况下停止跟踪。

2. 手势事件

iOS 2.0 中的 Safari 还增加了一种手势事件。手势事件会在两个手指触碰屏幕且相对距离或旋转角度变化时触发。手势事件有以下 3 种。
 gesturestart:一个手指已经放在屏幕上,再把另一个手指放到屏幕上时触发。
 gesturechange:任何一个手指在屏幕上的位置发生变化时触发。
 gestureend:其中一个手指离开屏幕时触发。

5 内存与性能

在 JavaScript 中,页面中事件处理程序的数量与页面整体性能直接相关。原因有很多。首先,每个函数都是对象,都占用内存空间,对象越多,性能越差。其次,为指定事件处理程序所需访问 DOM 的次数会先期造成整个页面交互的延迟。

5.1 事件委托

“过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。例如,click 事件冒泡到 document。这意味着可以为整个页面指定一个 onclick 事件处理程序,而不用为每个可点击元素分别指定事件处理程序。比如有以下 HTML:

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

let list = document.getElementById("myLinks"); 
list.addEventListener("click", (event) => { 
 let target = event.target; 
 switch(target.id) { 
 case "doSomething": 
 document.title = "I changed the document's title"; 
 break; 
 case "goSomewhere": 
 location.href = "http:// www.wrox.com"; 
 break; 
 case "sayHi": 
 console.log("hi"); 
 break; 
 } 
});

事件委托具有如下优点:
 document 对象随时可用,任何时候都可以给它添加事件处理程序(不用等待 DOMContentLoaded或 load 事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。
 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省 DOM 引用,也可以节省时间。
 减少整个页面所需的内存,提升整体性能。

5.2 删除事件处理程序

应该及时删除不用的事件处理程序。导致这个问题的原因主要有两个。第一个是删除带有事件处理程序的元素。比如通过真正的 DOM方法 removeChild()或 replaceChild()删除节点。最常见的还是使用 innerHTML 整体替换页面的某一部分。这时候,被 innerHTML 删除的元素上如果有事件处理程序,就不会被垃圾收集程序正常清理。
另一个可能导致内存中残留引用的问题是页面卸载。如果在页面卸载后事件处理程序没有被清理,则它们仍然会残留在内存中。

6 模拟事件

通常,事件都是由用户交互或浏览器功能触发。事实上,可以通过 JavaScript 在任何时候触发任意事件,而这些事件会被当成浏览器创建的事件。这种能力在测试 Web 应用时特别有用。

6.1 DOM 事件模拟

任何时候,都可以使用 document.createEvent()方法创建一个 event 对象。这个方法接收一个参数,此参数是一个表示要创建事件类型的字符串。
 “UIEvents”(DOM3 中是"UIEvent"):通用用户界面事件(鼠标事件和键盘事件都继承自这个事件)。
 “MouseEvents”(DOM3 中是"MouseEvent"):通用鼠标事件。
 “HTMLEvents”(DOM3 中没有):通用 HTML 事件(HTML 事件已经分散到了其他事件大类中)。
事件模拟的最后一步是触发事件。为此要使用 dispatchEvent()方法,这个方法存在于所有支持事件的 DOM 节点之上。dispatchEvent()方法接收一个参数,即表示要触发事件的 event 对象。调用 dispatchEvent()方法之后,事件就“转正”了,接着便冒泡并触发事件处理程序执行。

1. 模拟鼠标事件

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);

2. 模拟键盘事件

let textbox = document.getElementById("myTextbox"), 
 event;
 // 按照 DOM3 的方式创建 event 对象
if (document.implementation.hasFeature("KeyboardEvents", "3.0")) { 
 event = document.createEvent("KeyboardEvent"); 
 // 初始化 event 对象
 event.initKeyboardEvent("keydown", true, true, document.defaultView, "a", 
 0, "Shift", 0); 
} 
// 触发事件
textbox.dispatchEvent(event);

3. 模拟其他事件

模 拟 HTML 事件要调用 createEvent()方法并传入"HTMLEvents",然后再使用返回对象的initEvent()方法来初始化:

let event = document.createEvent("HTMLEvents"); 
event.initEvent("focus", true, false); 
target.dispatchEvent(event);

注意 HTML 事件在浏览器中很少使用,因为它们用处有限

4. 自定义 DOM 事件

要创建自定义事件,需要调用 createEvent(“CustomEvent”) 。返回的对象包含initCustomEvent()方法,该方法接收以下 4 个参数。
 type(字符串):要触发的事件类型,如"myevent"。
 bubbles(布尔值):表示事件是否冒泡。
 cancelable(布尔值):表示事件是否可以取消。
 detail(对象):任意值。作为 event 对象的 detail 属性。

6.2 IE 事件模拟

var btn = document.getElementById("myBtn");

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

/// 初始化 event 对象
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);

7 小结

最常见的事件是在 DOM3 Events 规范或 HTML5 中定义的。围绕着使用事件,需要考虑内存与性能问题。例如:
 最好限制一个页面中事件处理程序的数量,因为它们会占用过多内存,导致页面响应缓慢;
 利用事件冒泡,事件委托可以解决限制事件处理程序数量的问题;
 最好在页面卸载之前删除所有事件处理程序。

  • 22
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值