JavaScript高级程序设计(4)
第17章 事件
### 17.1 事件流 490 ##### 17.1.1 事件冒泡 - > IE 事件流被称为事件冒泡,这是因为事件被定义为从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)。现代浏览器中的事件会一直冒泡到 window 对象。 > > ```javascript > <!DOCTYPE html> > <html> > <head> > <title>Event Bubbling Example</title> > </head> > <body> > <div id="myDiv">Click Me</div> > </body> > </html> > //在点击页面中的<div>元素后,click 事件会以如下顺序发生:(1) <div> (2) <body> (3) <html> (4) document > ``` ##### 17.1.2 事件捕获 - > 事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。事件捕获实际上是为了在事件到达最终目标前拦截事件。实际上,所有浏览器都是从 window 对象开始捕获事件,而 DOM2 Events规范规定的是从 document 开始。由于旧版本浏览器不支持,因此实际当中几乎不会使用事件捕获。通常建议使用事件冒泡,特殊情况下可以使用事件捕获。 ##### 17.1.3 DOM事件流 - > DOM2 Events 规范规定事件流分为 3 个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生,为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。 ### 17.2 事件处理程序 493 > 事件意味着用户或浏览器执行的某种动作。比如,单击(click)、加载(load)、鼠标悬停(mouseover)。为响应事件而调用的函数被称为事件处理程序(或事件监听器)。事件处理程序的名字以"on"开头,因此 click 事件的处理程序叫作 onclick,而 load 事件的处理程序叫作 onload。 ##### 17.2.1HTML事件处理程序 - > ```html > //点击这个按钮后,控制台会输出一条消息 > <button value="Click Me" onclick="console.log('Clicked')"/> > > //如果确实需要使用双引号,则要把代码改成下面这样 > <button value="Click Me" onclick="console.log("Clicked")"/> > > //以调用在页面其他地方定义的脚本 > <input type="button" value="Click Me" onclick="showMessage()"/> > > //输出"click" > <input type="button" value="Click Me" onclick="console.log(event.type)"> > > //输出"Click Me" > <input type="button" value="Click Me" onclick="console.log(this.value)"> > <input type="button" value="Click Me" onclick="console.log(value)"> > > //本质上,经过这样的扩展,事件处理程序的代码就可以不必引用表单元素,而直接访问同一表单中 > 的其他成员了 > <sctipt> > function() { > with(document) { > with(this.form) { > with(this) { > // 属性值 > } > } > } > } > </sctipt> > <form method="post"> > <input type="text" name="username" value=""> > <input type="button" value="Echo Username" onclick="console.log(username.value)"> > </form> > > //大多数 HTML 事件处理程序会封装在 try/catch 块中 > <input type="button" value="Click Me" onclick="try{showMessage();}catch(ex) {}"> > ``` ##### 17.2.2 DOM0事件处理程序 - >```javascript >let btn = document.getElementById("myBtn"); >btn.onclick = function() { >console.log(this.id); // "myBtn" >}; >btn.onclick = null; // 移除事件处理程序 >``` ##### 17.2.3 DOM2事件处理程序 - > addEventListener()和 removeEventListener(),这两个方法暴露在所有 DOM 节点上,它们接收 3 个参数:事件名、事件处理函数和一个布尔值,true 表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序。使用 DOM2方式的主要优势是可以为同一个事件添加多个事件处理程序。 > > 大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,主要原因是跨浏览器兼容性好。把事件处理程序注册到捕获阶段通常用于在事件到达其指定目标之前拦截事件。如果不需要拦截,则不要使用事件捕获。 > > ```javascript > let btn = document.getElementById("myBtn"); > btn.addEventListener("click", () => { > console.log(this.id); > }, false); > > btn.addEventListener("click", () => { > console.log("Hello world!"); > }, false); > > //要取消事件监听,必须使用相同的 handler > let handler = function() { > console.log(this.id); > }; > btn.addEventListener("click", handler, false); > btn.removeEventListener("click", handler, false); // 有效果! > ``` ##### 17.2.4 IE事件处理程序 - > attachEvent()和 detachEvent()。这两个方法接收两个同样的参数:事件处理程序的名字和事件处理函数。使用attachEvent()添加的事件处理程序会添加到冒泡阶段。 > > ```javascript > var btn = document.getElementById("myBtn"); > btn.attachEvent("onclick", function() { > console.log("Clicked"); > }); > > //要取消事件监听,必须使用相同的 handler > var handler = function() { > console.log("Clicked"); > }; > btn.attachEvent("onclick", handler); > btn.detachEvent("onclick", handler); > ``` ### 17.3 事件对象 499 ##### 17.3.1 DOM事件对象 - > DOM 合规的浏览器中,event 对象是传给事件处理程序的唯一参数。不管以哪种方式(DOM0 或 DOM2)指定事件处理程序,都会传入这个 event 对象。event 对象只在事件处理程序执行期间存在,一旦执行完毕,就会被销毁。 > > 所有事件对象都会包含下表列出的这些公共属性和方法: > > - bubbles 表示事件是否冒泡 > - cancelable 表示是否可以取消事件的默认行为 > - currentTarget 当前事件处理程序所在的元素 > - defaultPrevented 表示已经调用 preventDefault()方法 > - detail 事件相关的其他信息 > - eventPhase 表示调用事件处理程序的阶段:1 代表捕获阶段,2 代表到达目标,3 代表冒泡阶段 > - preventDefault() 用于取消事件的默认行为。只有 cancelable 为 true 才可以调用这个方法 > - stopImmediatePropagation() 用于取消所有后续事件捕获或事件冒泡,并阻止调用任何后续事件处理程序 > - stopPropagation() 用于取消所有后续事件捕获或事件冒泡。只有 bubbles为 true 才可以调用这个方法 > target 事件目标 > - trusted 表示事件是由浏览器生成的。false 表示事件是开发者通过 JavaScript 创建的 > - type 被触发的事件类型 > - View 与事件相关的抽象视图。等于事件所发生的 window 对象 > > ```javascript > let btn = document.getElementById("myBtn"); > btn.onclick = function(event) { > console.log(event.currentTarget === this); // true > console.log(event.target === this); // true > //阻止特定事件的默认动作 > event.preventDefault(); > //于立即阻止事件流在 DOM 结构中传播,取消后续的事件捕获或冒泡 > event.stopPropagation(); > }; > ``` ### 17.4 事件类型 505 >DOM3 Events 定义了如下事件类型: > >- 用户界面事件(UIEvent):涉及与 BOM 交互的通用浏览器事件。 >- 焦点事件(FocusEvent):在元素获得和失去焦点时触发。 >- 鼠标事件(MouseEvent):使用鼠标在页面上执行某些操作时触发。 >- 滚轮事件(WheelEvent):使用鼠标滚轮(或类似设备)时触发。 >- 输入事件(InputEvent):向文档中输入文本时触发。 >- 键盘事件(KeyboardEvent):使用键盘在页面上执行某些操作时触发。 >- 合成事件(CompositionEvent):在使用某种编辑器输入字符时触发。 ##### 17.4.1 用户界面事件 - **load** 事件 > 在 window 对象上,load 事件会在整个页面(包括所有外部资源如图片、JavaScript 文件和 CSS 文件)加载完成后触发。可以通过两种方式指定 load 事件处理程序。 > > ```javascript > //第一种是 JavaScript 方式 > window.addEventListener("load", (event) => { > console.log("Loaded!"); > }); > > //第二种是向<body>元素添加 onload 属性 > <body onload="console.log('Loaded!')"></body> > > //图片上也会触发load事件 > <img src="smile.gif" onload="console.log('Image loaded.')"> > > let image = document.getElementById("myImage"); > image.addEventListener("load", (event) => { > console.log(event.target.src); > }); > > //<script>元素会在 JavaScript 文件加载完成后触发 load 事件,从而可以动态检测。 > window.addEventListener("load", () => { > let script = document.createElement("script"); > script.addEventListener("load", (event) => { > console.log("Loaded"); > }); > script.src = "example.js"; > document.body.appendChild(script); > }); > ``` - **unload** 事件 > unload 事件会在文档卸载完成后触发。unload 事件一般是在从一个页面导航到另一个页面时触发,最常用于清理引用,以避免内存泄漏。 > > ```javascript > //第一种是 JavaScript 方式, > window.addEventListener("unload", (event) => { > console.log("Unloaded!"); > }); > > //第二种方式给<body>元素添加 onunload 属性 > <body onunload="console.log('Unloaded!')"></body> > ``` - **resize** 事件 > 当浏览器窗口被缩放到新高度或宽度时,会触发 resize 事件。这个事件在 window 上触发,因此可以通过 JavaScript 在 window 上或者为<body>元素添加 onresize 属性来指定事件处理程序。优先使用 JavaScript 方式: > > ```javascript > window.addEventListener("resize", (event) => { > console.log("Resized"); > }); > ``` - **scroll** 事件 > 当用户滚动包含滚动条的元素时在元素上触发。<body>元素包含已加载页面的滚动条。大多数 HTML 事件与 window 对象和表单控件有关。scroll 事件也会随着文档滚动而重复触发,因此最好保持事件处理程序的代码尽可能简单。 > > ```javascript > window.addEventListener("scroll", (event) => { > if (document.compatMode == "CSS1Compat") { > console.log(document.documentElement.scrollTop); > } else { > console.log(document.body.scrollTop); > } > }); > ``` - abort > 在<object>元素上当相应对象加载完成前被用户提前终止下载时触发。 - error > 在 window 上当 JavaScript 报错时触发,在<img>元素上当无法加载指定图片时触发,在<object>元素上当无法加载相应对象时触发,在窗套上当一个或多个窗格无法完成加载时触发。 - select > 在文本框(<input>或 textarea)上当用户选择了一个或多个字符时触发。 ##### 17.4.2 焦点事件 - > - blur:当元素失去焦点时触发。这个事件不冒泡,所有浏览器都支持。 > - focus:当元素获得焦点时触发。这个事件不冒泡,所有浏览器都支持。 > - focusin:当元素获得焦点时触发。这个事件是 focus 的冒泡版。 > - focusout:当元素失去焦点时触发。这个事件是 blur 的通用版。 ##### 17.4.3 鼠标和滚轮事件 - > - click:在用户单击鼠标主键(通常是左键)或按键盘回车键时触发。 > - dblclick:在用户双击鼠标主键(通常是左键)时触发。 > - mousedown:在用户按下任意鼠标键时触发。这个事件不能通过键盘触发。 > - mouseenter:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。 > - mouseleave:在用户把鼠标光标从元素内部移到元素外部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。 > - mousemove:在鼠标光标在元素上移动时反复触发。这个事件不能通过键盘触发。 > - mouseout:在用户把鼠标光标从一个元素移到另一个元素上时触发。移到的元素可以是原始元素的外部元素,也可以是原始元素的子元素。这个事件不能通过键盘触发。 > - mouseover:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不能通过键盘触发。 > - mouseup:在用户释放鼠标键时触发。这个事件不能通过键盘触发。 ##### 17.4.4 键盘与输入事件 - > - keyup,用户释放键盘上某个键时触发。 > > - keydown,用户按下键盘上某个键时触发,而且持续按住会重复触发。 > > - keypress,用户按下键盘上某个键并产生字符时触发,而且持续按住会重复触发。Esc 键也会触发这个事件。DOM3 Events 废弃了 keypress 事件,而推荐 textInput 事件。 > > - textInput ,在字符被输入到可编辑区域时触发。 > > ```javascript > let textbox = document.getElementById("myText"); > textbox.addEventListener("textInput", (event) => { > console.log(event.data); > }); > ``` ##### 17.4.5 合成事件 - > 合成事件是 DOM3 Events 中新增的,用于处理通常使用 IME 输入时的复杂输入序列。IME 可以让用户输入物理键盘上没有的字符。例如,使用拉丁字母键盘的用户还可以使用 IME 输入日文。IME 通常需要同时按下多个键才能输入一个字符。合成事件用于检测和控制这种输入。 > > - compositionstart,在 IME 的文本合成系统打开时触发,表示输入即将开始; > > - compositionupdate,在新字符插入输入字段时触发; > > - compositionend,在 IME 的文本合成系统关闭时触发,表示恢复正常键盘输入。 ##### 17.4.6 变化事件 - > 这些事件已经被废弃,浏览器已经在有计划地停止对它们的支持。变化事件已经被Mutation Observers 所取代,可以参考第 14 章中的介绍。 ##### 17.4.7 HTML5事件 - **contextmenu** 事件 >用于表示何时该显示上下文菜单,从而允许开发者取消默认的上下文菜单并提供自定义菜单。 - **beforeunload** 事件 > beforeunload 事件会在 window 上触发,用意是给开发者提供阻止页面被卸载的机会。这个事件会在页面即将从浏览器中卸载时触发,如果页面需要继续使用,则可以不被卸载。 - **DOMContentLoaded** 事件 - **readystatechange** 事件 - **pageshow** 与 **pagehide** 事件 - **hashchange** 事件 ##### 17.4.8 设备事件 - **orientationchange** 事件 > 判断用户的设备是处于垂直模式还是水平模式。移动 Safari 在 window 上暴露了 window.orientation 属性,它有以下 3 种值之一:0 表示垂直模式,90 表示左转水平模式(主屏幕键在右侧),–90 表示右转水平模式(主屏幕键在左)。 - **deviceorientation** 事件 >如果可以获取设备的加速计信息,而且数据发生了变化,这个事件就会在 window 上触发。要注意的是,deviceorientation 事件只反映设备在空间中的朝向,而不涉及移动相关的信息。 - **devicemotion** 事件 >这个事件用于提示设备实际上在移动,而不仅仅是改变了朝向。可以用来确定设备正在掉落或者正拿在一个行走的 > >人手里。 ##### 17.4.9 触摸及手势事件 - 触摸事件 > 当手指放在屏幕上、在屏幕上滑动或从屏幕移开时,触摸事件即会触发。触摸事件有如下几种 > > - touchstart:手指放到屏幕上时触发(即使有一个手指已经放在了屏幕上)。 > - touchmove:手指在屏幕上滑动时连续触发。在这个事件中调用 preventDefault()可以阻止滚动。 > - touchend:手指从屏幕上移开时触发。 > - touchcancel:系统停止跟踪触摸时触发。文档中并未明确什么情况下停止跟踪。 > > ```javascript > function handleTouchEvent(event) { > // 只针对一个触点 > if (event.touches.length == 1) { > let output = document.getElementById("output"); > switch(event.type) { > case "touchstart": > output.innerHTML += `<br>Touch started:` + > `(${event.touches[0].clientX}` + > ` ${event.touches[0].clientY})`; > 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; > } > } > } > document.addEventListener("touchstart", handleTouchEvent); > document.addEventListener("touchend", handleTouchEvent); > document.addEventListener("touchmove", handleTouchEvent); > ``` - 手势事件 > 手势事件会在两个手指触碰屏幕且相对距离或旋转角度变化时触发。手势事件有以下 3 种 > > - gesturestart:一个手指已经放在屏幕上,再把另一个手指放到屏幕上时触发。 > - gesturechange:任何一个手指在屏幕上的位置发生变化时触发。 > - gestureend:其中一个手指离开屏幕时触发。 > > 触摸事件和手势事件存在一定的关系。当一个手指放在屏幕上时,会触发 touchstart 事件。当另一个手指放到屏幕上时,gesturestart 事件会首先触发,然后紧接着触发这个手指的 touchstart事件。如果两个手指或其中一个手指移动,则会触发 gesturechange 事件。只要其中一个手指离开屏幕,就会触发 gestureend 事件,紧接着触发该手指的 touchend 事件。 > > ```javascript > function handleGestureEvent(event) { > let output = document.getElementById("output"); > switch(event.type) { > case "gesturestart": > output.innerHTML += `Gesture started: ` + > `rotation=${event.rotation},` + > `scale=${event.scale}`; > break; > case "gestureend": > output.innerHTML += `Gesture ended: ` + > `rotation=${event.rotation},` + > `scale=${event.scale}`; > break; > case "gesturechange": > output.innerHTML += `Gesture changed: ` + > `rotation=${event.rotation},` + > `scale=${event.scale}`; > break; > } > } > document.addEventListener("gesturestart", handleGestureEvent, false); > document.addEventListener("gestureend", handleGestureEvent, false); > document.addEventListener("gesturechange", handleGestureEvent, false); > ``` ### 17.5 内存与性能 540 ##### 17.5.1 事件委托 - >“过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。例如,click 事件冒泡到 document。这意味着可以为整个页面指定一个 onclick 事件处理程序,而不用为每个可点击元素分别指定事件处理程序。 > >最适合使用事件委托的事件包括:click、mousedown、mouseup、keydown 和 keypress。 > >只要可行,就应该考虑只给 document 添加一个事件处理程序,通过它处理页面中所有某种类型的事件。相对于之前的技术,事件委托具有如下优点: > >- document 对象随时可用,任何时候都可以给它添加事件处理程序。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。 >- 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省 DOM 引用,也可以节省时间。 >- 减少整个页面所需的内存,提升整体性能。 ```html <ul id="myLinks"> <li id="goSomewhere">Go somewhere</li> <li id="doSomething">Do something</li> <li id="sayHi">Say hi</li> </ul> <script> //以前的做法 let item1 = document.getElementById("goSomewhere"); let item2 = document.getElementById("doSomething"); let item3 = document.getElementById("sayHi"); item1.addEventListener("click", (event) => { location.href = "http:// www.wrox.com"; }); item2.addEventListener("click", (event) => { document.title = "I changed the document's title"; }); item3.addEventListener("click", (event) => { console.log("hi"); }); //事件委托做法 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; } }); </script> ``` ##### 17.5.2 删除事件处理程序 - > 把事件处理程序指定给元素后,在浏览器代码和负责页面交互的 JavaScript 代码之间就建立了联系。这种联系建立得越多,页面性能就越差。除了通过事件委托来限制这种连接之外,还应该及时删除不用的事件处理程序。很多 Web 应用性能不佳都是由于无用的事件处理程序长驻内存导致的。 > > - 如果知道某个元素会被删除,那么最好在删除它之前手工删除它的事件处理程序 > > ```javascript > <script type="text/javascript"> > let btn = document.getElementById("myBtn"); > btn.onclick = function() { > // 执行操作 > btn.onclick = null; // 删除事件处理程序 > document.getElementById("myDiv").innerHTML = "Processing..."; > }; > </script> > ``` > > - 另一个可能导致内存中残留引用的问题是页面卸载,如果在页面卸载后事件处理程序没有被清理,则它们仍然会残留在内存中。之后,浏览器每次加载和卸载页面(比如通过前进、后退或刷新),内存中残留对象的数量都会增加,这是因为事件处理程序不会被回收。一般来说,最好在 onunload 事件处理程序中趁页面尚未卸载先删除所有事件处理程序。 ### 17.6 模拟事件 543 - 模拟鼠标事件 ```javascript 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); ``` - 模拟键盘事件 ```javascript 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); ``` - 模拟其他事件 ```javascript let event = document.createEvent("HTMLEvents"); event.initEvent("focus", true, false); target.dispatchEvent(event); ``` - 自定义 DOM 事件 ```javascript let div = document.getElementById("myDiv"), 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。 ```
顶部
收展
底部
[TOC]
目录
第1章 JavaScript简介
第2章 在 HTML中使用JavaScript
第3章 语言基础(1)语法变量
第3章 语言基础(2)数据类型
第3章 语言基础(3)操作符
第3章 语言基础(4)语句
第4章 变量、作用域与内存
第5章 基本引用类型
第6章 集合引用类型
第7章 迭代器与生成器
第8 章对象、类与面向对象编程
第9章 代理与反射
第10章 函数
第11章 期约与异步函数
第12章 BOM
第13章 客户端检测
第14章 DOM
第15章 DOM 扩展
第16章 DOM2 和 DOM3
第17章 事件
第18章 动画与 Canvas 图形
第19章 表单脚本
第20章 JavaScript API
第21章 错误处理与调试
第22章 处理 XML
第23章 JSON
第24章 网络请求与远程资源
第25章 客户端存储
第26章 模块
第27章 工作者线程
第28章 最佳实践
相关推荐
WebSocket