【温故而知新-Javascript】使用事件
1. 使用简单事件处理器
可以用几种不同的方式处理事件。最直接的方式是用事件属性创建一个简单事件处理器(simple event handler)。元素为它们支持的每一种事件都定义了一个事件属性。举个例子,onmouseover事件属性对应全局事件mouseover,后者会在用户把光标移动到元素占据的浏览器屏幕的上方时触发。(这是一种通用的模式:大多数事件都有一个对应的事件属性,其名称定义为 on<eventname>)
1.1 实现简单的内联事件处理器
使用某个属性最直接的方式是给它指派一组JavaScript语句。当该事件被触发后,浏览器就会执行提供的语句。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用内联JavaScript处理事件</title> <style> p {background: gray;color:white;padding: 10px;margin: 5px;border: thin solid orange;} </style> </head> <body> <p onmouseover="this.style.background='white';this.style.color = 'black'"> 你承受的苦难并不比他人多太多,痛苦主要来自敏感和脆弱。 </p> </body> </html>
此例中,指定了两条JavaScript语句用来想要mouseover事件。具体方式是设置它们作为文档里p元素onmouseover事件属性的值。这些语句如下:
this.style.background='white';
this.style.color = 'black';
这些是直接应用到元素style属性上的CSS属性。浏览器会把特殊变量 this 的值设置为代表触发事件元素的HTMLElement 对象,而style 属性会返回该元素的CSSStyleDeclaration 对象。
如果在浏览器中载入这个文档,style元素定义的初始样式就会被应用到p元素上。当把鼠标移至元素上方时,JavaScript语句就会被执行。
这种转变是单向的:当鼠标离开元素的屏幕区域时样式不会重置。许多事件是成双成对的。与mouseover相对的事件被称为 mouseout,可以通过onmouseout事件属性来处理这一事件。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用内联JavaScript处理事件</title> <style> p {background: gray;color:white;padding: 10px;margin: 5px;border: thin solid orange;} </style> </head> <body> <p onmouseover="this.style.background='white';this.style.color = 'black';" onmouseout="this.style.removeProperty('color');this.style.removeProperty('background');"> 你承受的苦难并不比他人多太多,痛苦主要来自敏感和脆弱。 </p> </body> </html>
添加这些代码后,此元素就能响应鼠标进入和离开它所占据的屏幕区域了。
1.2 实现一个简单的事件处理函数
为了在一定程度解决繁琐和重复添加问题,可以定义一个函数,并将函数名指定为元素事件属性的值。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用函数来处理事件</title> <style> p {background: gray;color:white;padding: 10px;margin: 5px;border: thin solid orange;} </style> </head> <body> <p onmouseover="handleMouseOver(this)" onmouseout="handleMouseOut(this)"> 你承受的苦难并不比他人多太多,痛苦主要来自敏感和脆弱。 </p > <p onmouseover="handleMouseOver(this)" onmouseout="handleMouseOut(this)"> 一些年轻人,通过高端消费来营造自己高端收入的形象 </p> <script> function handleMouseOver(elem){ elem.style.background='white'; elem.style.color = 'black'; } function handleMouseOut(elem){ elem.style.removeProperty('color'); elem.style.removeProperty('background'); } </script> </body> </html>
此例中,定义了一些JavaScript函数,它们所包含的语句是用来想要鼠标事件的。并且还在onmouseover和onmouseout属性里指定了这些函数。特殊值this 指的是触发事件的元素。
这种方式相对于之前的技巧有了进步。重复添加工作变得了更少了,代码也更易阅读了。但是,希望能让事件与HTML元素互相分离,要做到这一点,需要在访问DOM。
2. 使用DOM和事件对象
前面例子演示的简单事件处理器用于基本任务是可行的,但如果想要进行更为复杂的处理(以及更优雅地定义事件处理器),就需要使用DOM和JavaScript的Event对象了。下面例子展示了如何使用Event对象,以及如何用DOM来将某个函数与事件关联起来。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用DOM构建事件处理</title> <style> p {background: gray;color:white;padding: 10px;margin: 5px;border: thin solid orange;} </style> </head> <body> <p> 你承受的苦难并不比他人多太多,痛苦主要来自敏感和脆弱。 </p > <p> 一些年轻人,通过高端消费来营造自己高端收入的形象 </p> <script type="application/javascript"> var pElems = document.getElementsByTagName("p"); for(var i=0;i<pElems.length;i++){ pElems[i].onmousemove = handleMouseOver; pElems[i].onmouseout = handleMouseOut; } function handleMouseOver(e){ e.target.style.background='white'; e.target.style.color = 'black'; } function handleMouseOut(e){ e.target.style.removeProperty('color'); e.target.style.removeProperty('background'); } </script> </body> </html>
这段脚本(必须把他移到页尾,因为操作的是DOM)找到想要处理事件的所有元素,然后给事件处理器属性设置一个函数名。所有事件都拥有像这样的属性。它们的命名方式是一致的:以 on 开头,后接事件的名称。
PS:请注意这里使用函数的名称来将它注册成一个事件监听器。一个常见的错误是把括号加在函数名的后面,使 handleMouse变成handerMouse()。这样做的后果是函数会在脚本执行时(而不是事件触发时)被调用。
此例中那些处理事件的函数定义了一个名为e的参数。它会被设成浏览器所创建的一个Event对象,用于在事件触发时代表该事件。这个Event对象提供了所发生的事件信息,能够更加灵活地(相对于把代码放在元素属性中而言)对用户交互行为作出反应。在此例中,用target属性来获取触发事件的HTMLElement,这样就能使用样式属性来改变它的外观。
在展示事件对象之前,想演示另一种指定事件处理函数的方式。事件属性(名称以on开头的那些)一般来说是最容易的方式,但也可以使用addEventListener 方法,它由HTMLElement对象实现。还可以使用removeEventListener 方法来取消函数与事件之间的关联。可以在这两个方法中都能把事件类型和处理它们的函数表达为参数,如下面的例子所示:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用addEventListener和removeEventListener方法</title> <style> p {background: gray;color:white;padding: 10px;margin: 5px;border: thin solid orange;} </style> </head> <body> <p> 你承受的苦难并不比他人多太多,痛苦主要来自敏感和脆弱。 </p > <p id="block2"> 一些年轻人,通过高端消费来营造自己高端收入的形象 </p> <button id="btn">Press Me</button> <script type="application/javascript"> var pElem = document.getElementsByTagName("p"); for(var i=0;i<pElem.length;i++){ pElem[i].addEventListener("mouseover",handleMouseOver); pElem[i].addEventListener("mouseout",handleMouseOut); } document.getElementById("btn").onclick = function(){ document.getElementById("block2").removeEventListener("mouseout",handleMouseOut); } function handleMouseOver(e){ e.target.style.background='white'; e.target.style.color = 'black'; } function handleMouseOut(e){ e.target.style.removeProperty('color'); e.target.style.removeProperty('background'); } </script> </body> </html>
此例中的脚本使用addEventListener方法把handleMouseOver和handleMouseOut函数注册成p元素的事件处理器。当button被点击后,脚本用removeEventListener方法取消了id值为block2 的p元素与handleMouseOut函数之间的关联。
addEventListener 方法的优点在于它让你能够访问某些高级事件特性。下表介绍了Event对象的成员。
2.1 按类型区分事件
type 属性会告诉你正在处理的是哪种类型的事件。这个值以字符串的形式提供,比如 mouseover。有了探测事件类型的能力,就可以用一个函数来处理多个类型了。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用type属性</title> <style type="text/css"> p {background: gray;color: white;padding: 10px;margin: 5px;border: thin solid orange;} </style> </head> <body> <p> 你承受的苦难并不比他人多太多,痛苦主要来自敏感和脆弱。 </p > <p id="block2"> 一些年轻人,通过高端消费来营造自己高端收入的形象 </p> <script type="application/javascript"> var pElems = document.getElementsByTagName("p"); for(var i=0;i<pElems.length;i++){ pElems[i].onmouseover = handleMouseEvent; pElems[i].onmouseout = handleMouseEvent; } function handleMouseEvent(e){ if(e.type == "mouseover"){ e.target.style.background='white'; e.target.style.color = 'black'; }else if(e.type == "mouseout"){ e.target.style.removeProperty('color'); e.target.style.removeProperty('background'); } } </script> </body> </html>
此例中,只用了一个handleMouseEvent 这一个事件处理器,通过使用type属性来判断正在处理的是哪一种事件。
2.2 理解事件流
一个事件的生命周期包括三个阶段:捕捉(capture)、目标(target)和冒泡(bubbing)。
(1) 理解捕捉阶段
当某个事件被触发时,浏览器会找出事件涉及的元素,它被称为该事件的目标。浏览器会找出body元素和目标之间的所有元素并分别检查它们,看看它们是否带有事件处理器且要求获得其后代元素触发事件的通知。浏览器会先触发这些事件处理器,然后才会轮到目标自身的处理器。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>捕捉事件</title> <style type="text/css"> p {background: gray;color: white;padding: 10px;margin: 5px;border: thin solid orange;} span {background: white;color: black;padding: 2px;cursor: default;} </style> </head> <body> <p id="block1"> 你承受的苦难并不比他人多太多,痛苦主要来自<span id="point">敏感和脆弱</span>。 </p > <script type="application/javascript"> var point = document.getElementById("point"); var textBlock = document.getElementById("block1"); point.addEventListener("mouseover",handleMouseEvent); point.addEventListener("mouseout",handleMouseEvent); textBlock.addEventListener("mouseover",handleDescendantEvent,true); textBlock.addEventListener("mouseout",handleDescendantEvent,true); function handleDescendantEvent(e){ if(e.type == "mouseover" && e.eventPhase == Event.CAPTURING_PHASE){ e.target.style.border = "thick solid red"; e.currentTarget.style.border = "thick double blue"; }else if(e.type == "mouseout" && e.eventPhase == Event.CAPTURING_PHASE){ e.target.style.removeProperty("border"); e.currentTarget.style.removeProperty("border"); } } function handleMouseEvent(e){ if(e.type == "mouseover"){ e.target.style.background='black'; e.target.style.color = 'white'; }else if(e.type == "mouseout"){ e.target.style.removeProperty('background'); e.target.style.removeProperty('color'); } } </script> </body> </html>
此例中,定义了一个span作为p元素的子元素,然后注册了mouseover和mouseout事件的处理器。当注册父元素(即p元素)时,给addEventListener方法添加了第三个参数,就像这样:
textBlock.addEventListener("mouseover",handleDescendantEvent,true);
这个额外的参数告诉浏览器让p元素在捕捉阶段接收后代元素的事件。当mouseover事件被触发时,浏览器会从HTML文档的跟节点起步,一路沿着DOM向目标(也就是触发事件的元素)前进。对层级里的每一个元素,浏览器都会检查它是否对捕捉到的事件感兴趣。可以从下图看到示例文档的顺序。
对每一个元素,浏览器都会调用它所有启用捕捉的监听器。此例中,浏览器会找到并调用注册在p元素上的handleDescendantEvent函数。当handleDescendantEvent函数被调用时,Event对象包含了目标元素的信息(通过target),还有导致函数被调用的元素(通过currentTarget属性)。这里同时使用了这两个属性,这样就能修改p元素和其子元素span的样式了。
事件捕捉让目标元素的各个上级元素都有机会在事件传递到目标元素之前对其做出对应。上级元素的事件处理器可以阻止事件流向目标,方法是对Event对象调用stopPropagation或stopImmediatePropagation函数。这两个函数的区别在于,stopPropagation会确保调用当前元素上注册的所有事件监听器,而stopImmediatePropagation会忽略任何未触发的监听器。为了展示了如何给hanldDescendantEvent事件处理器添加stopPropagation函数,修改上面例子的JavaScript代码中的hanldDescendantEvent函数如下:
function handleDescendantEvent(e){ if(e.type == "mouseover" && e.eventPhase == Event.CAPTURING_PHASE){ e.target.style.border = "thick solid red"; e.currentTarget.style.border = "thick double blue"; }else if(e.type == "mouseout" && e.eventPhase == Event.CAPTURING_PHASE){ e.target.style.removeProperty("border"); e.currentTarget.style.removeProperty("border"); } e.stopPropagation(); }
做了这个改动后,浏览器的捕捉阶段就会在p元素上的处理器被调用时结束。浏览器不会检查其他任何元素,并且会跳过目标和冒泡阶段。对此例来说,这就意味着handleMouseEvent 函数里的样式变化不会被应用(以响应mouseover事件),如此例修改后的显示效果如下:
请注意此例在处理器里检查了事件类型,并用 eventPhase属性来确定事件所处的阶段,就像这样:
... if(e.type == "mouseover" && e.eventPhase == Event.CAPTURING_PHASE){ ...
在注册事件监听器时启动捕捉事件并不能停止针对元素自身的事件。此例中,p元素占据了浏览器屏幕空间,它同样会响应mouseover事件。为了避免这一点,进行了检查,确保只有在处理捕捉阶段的事件(只针对后代元素的事件,处理此事件完全是因为注册了启用捕捉的监听器)时才会应用样式改动。eventPhase属性会返回下表里展示的三个值之一。它们代表了事件生命周期的三个阶段。
(2) 理解目标阶段
目标阶段是三个阶段中最简单的。当捕捉阶段完成后,浏览器会触发目标元素上任何已添加的事件类型监听器,如下图所示:
这里要注意的唯一一点是可以多次调用addEventListener函数,因此某个给定事件类型可以有不止一个监听器。
PS:如果在目标阶段调用stopPropagation或stopImmediatePropagation函数,相当于终止了事件流,不再进入冒泡阶段。
(3) 理解冒泡阶段
完成目标阶段之后,浏览器开始转而沿着上级元素链朝 body元素前进。在沿途的每个元素上,浏览器都会检查是否存在针对该事件类型但没有启用捕捉的监听器(也就是说,addEventListener函数的第三个参数是 false)。这就是所谓的事件冒泡。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>事件冒泡</title> <style type="text/css"> p {background: gray;color: white;padding: 10px;margin: 5px;border: thin solid orange;} span {background: white;color: #017CB9;padding: 2px;cursor: default;} </style> </head> <body> <p id="block1"> The shortest mantra of this world is <span id="point">the name</span> of a person. </p > <script type="application/javascript"> var point = document.getElementById("point"); var textBlock = document.getElementById("block1"); point.addEventListener("mouseover",handleMouseEvent); point.addEventListener("mouseout",handleMouseEvent); function handleMouseEvent(e){ if(e.type == "mouseover"){ e.target.style.background='black'; e.target.style.color = 'white'; }else if(e.type == "mouseout"){ e.target.style.removeProperty('background'); e.target.style.removeProperty('color'); } } textBlock.addEventListener("mouseover",handleDescendantEvent,true); textBlock.addEventListener("mouseout",handleDescendantEvent,true); function handleDescendantEvent(e){ if(e.type == "mouseover" && e.eventPhase == Event.CAPTURING_PHASE){ e.target.style.border = "thick solid red"; e.currentTarget.style.border = "thick double blue"; }else if(e.type == "mouseout" && e.eventPhase == Event.CAPTURING_PHASE){ e.target.style.removeProperty("border"); e.currentTarget.style.removeProperty("border"); } } textBlock.addEventListener("mouseover",handleBubbleMouseEvent,false); textBlock.addEventListener("mouseout",handleBubbleMouseEvent,false); function handleBubbleMouseEvent(e){ if(e.type == "mouseover" && e.eventPhase == Event.BUBBLING_PHASE){ e.target.style.textTransform = "uppercase"; }else if(e.type == "mouseout" && e.eventPhase == Event.BUBBLING_PHASE){ e.target.style.textTransform = "none"; } } </script> </body> </html>
此例添加了一个名为handleBubbleMouseEvent的新函数,并把它附到文档的p元素上。现在p元素就有了两个事件监听器,一个启用了捕捉,另一个启用了冒泡。当使用了addEventListener方法时, 始终都处于这两种状态中的一种,这就意味着某个元素的监听器除了自身事件的通知,还会收到后代元素事件的通知。你要选择的是在后代元素事件的目标阶段之前还是之后调用监听器。
这些新代码带来的结果是,文档里的span元素在发生mouseover事件时会触发三个监听函数。handleDescendantEvent函数会在捕捉阶段被触发,handleMouseEvent会在目标阶段被调用,而handleBubbleMouseEvent则是在冒泡阶段。从下图可以看到它的效果。
现在,所有监听函数里的样式改动都会影响到元素的外观:
PS:不是所有事件都之处冒泡。可以用bubbles属性来检查某个事件能否冒泡。如果值是true,就说明此事件会冒泡,false 则意味这它不会冒泡。
2.3 使用可撤销事件
有些事件定义了一种默认的行为,会在事件被触发时执行。举个例子,a 元素click事件的默认行为是浏览器会载入 href设置所指定的URL的内容。当某一事件拥有默认行为时,它的cancelable 属性就会是true。 你可以调用 preventDefault 函数来阻止默认行为的执行。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>撤销默认的行为</title> <style type="text/css"> a {background: gray;color:white;padding: 10px;border: thin solid orange;} </style> </head> <body> <p> <a href="https://yexiaochao.github.io">Visit Luka's Blog</a> <a href="https://github.com/YeXiaoChao">Visit Luka's GitHub</a> </p> <script type="application/javascript"> function handleClick(e){ if(!confirm("Do you want to navigate to " + e.target.href + "?")){ e.preventDefault(); } } var elems = document.querySelectorAll("a"); for (var i=0;i<elems.length;i++){ elems[i].addEventListener("click",handleClick,false); } </script> </body> </html>
这个例子里,用 confirm 函数来提示用户考虑他们是否真的想导航到a元素指向的URL上。如果用户点击了Cancel按钮,就会调用 preventDefault函数。这就意味着浏览器不会再导航到该URL上。
请注意,调用 preventDefault 函数不会阻止事件流经历捕捉、目标和冒泡阶段。这些阶段仍然会进行,但是浏览器不会再冒泡阶段的最后执行默认行为。可以通过读取 defaultPrevented属性来检查 preventDefault函数是否已经被之前的某个事件处理器在事件上调用过了。如果返回true,那么preventDefault函数就已经被调用过了。
3. 使用HTML事件
3.1 文档和窗口事件
除了之前见过的那些功能,Document对象还定义了下表里介绍的事件。
Window对象定义了多种多样的事件,下表对其进行了介绍。可以通过body元素处理其中一些事件,但浏览器度这种方法 支持程度不太一致,一般而言使用Window对象会更可靠。
3.2 使用鼠标事件
前面已经有介绍过mouseover和mouseout事件了,下表展示了鼠标相关事件的完整集合。
当某个鼠标事件被 触发时,浏览器会指派一个MouseEvent对象。它是一个Event对象,但带有下表中展示的额外属性和方法。
下面的例子展示了如何使用MouseEvent对象所提供的额外功能。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用MouseEvent对象响应鼠标事情</title> <style> p {background: gray;color: white;padding: 10px;margin: 5px;border: thin solid orange;} table {margin: 5px;border-collapse: collapse;} th,td {padding: 4px;} </style> </head> <body> <p id="block1"> 你承受的苦难并不比他人多太多,痛苦主要来自敏感和脆弱。 </p > <table border="1"> <tr><th>Type:</th><td id="eType"></td></tr> <tr><th>X:</th><td id="eX"></td></tr> <tr><th>Y:</th><td id="eY"></td></tr> </table> <script type="application/javascript"> function $(id){ return document.getElementById(id); } function handleMouseEvent(e){ if (e.eventPhase == Event.AT_TARGET){ $("eType").innerHTML = e.type; $("eX").innerHTML = e.clientX; $("eY").innerHTML = e.clientY; if(e.type == "mousemove"){ e.target.style.background = 'white'; e.target.style.color = 'blue'; }else { e.target.style.removeProperty('background'); e.target.style.removeProperty('color'); } } } $("block1").addEventListener("mouseout",handleMouseEvent,false); $("block1").addEventListener("mousemove",handleMouseEvent,false); </script> </body> </html>
此例的脚本会更新表格的一些单元格,以此来响应两类鼠标事件。
3.3 使用键盘焦点事件
与键盘焦点相关的事件触发于元素获得和失去焦点之时。
FocusEvent对象代表了这些事件,相对于Event对象的核心功能,它还添加了下表里展示的属性。
下面的例子演示了如何使用键盘焦点事件。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用键盘焦点事件</title> <style> p {background: gray;color: white;padding: 10px;margin: 5px;border: thin solid orange;} </style> </head> <body> <form> <p> <label for="fave">Fruit: <input autofocus id="fave" name="fave" /></label> </p> <p> <label for="name">Name: <input id="name" name="name" /></label> </p> <button type="submit">Submit Vote</button> <button type="reset">Reset</button> </form> <script type="application/javascript"> function handleFocusEvent(e){ if (e.type == "focus"){ e.target.style.background = "lightgray"; e.target.style.border = "thick double red"; }else { e.target.style.removeProperty("background"); e.target.style.removeProperty("border"); } } var inputElems = document.getElementsByTagName("input"); for(var i=0;i<inputElems.length;i++){ inputElems[i].onfocus = handleFocusEvent; inputElems[i].onblur = handleFocusEvent; } </script> </body> </html>
此例的脚本用focus和blur事件来改变一对input元素的样式。
3.4 使用键盘事件
键盘事件由按键操作触发。下表展示了这一类事件。
keyboardEvent对象代表了这些事件,相对于Event对象的核心功能,它还增加了下表里展示的属性。
下面的例子展示了其中一些键盘事件的用法。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>键盘事件</title> <style> p {background: gray;color: white;padding: 10px;margin: 5px;border: thin solid orange;} </style> </head> <body> <form> <p> <label for="fave">Fruit: <input autofocus id="fave" name="fave" /></label> </p> <p> <label for="name">Name: <input id="name" name="name" /></label> </p> <button type="submit">Submit Vote</button> <button type="reset">Reset</button> </form> <span id="message"></span> <script type="application/javascript"> var inputElems = document.getElementsByTagName("input"); for(var i=0;i<inputElems.length;i++){ inputElems[i].onkeyup = handleKeyboardEvent; } function handleKeyboardEvent(e){ document.getElementById("message").innerHTML = "Key pressed: " + e.keyCode + " Char: " + String.fromCharCode(e.keyCode); } </script> </body> </html>
此例里的脚本通过改变某个span元素的内容来显示发送给一对input元素的按键。请注意此处有用String.fromCharCode函数把keyCode属性的值转换成了一个更有用的值。
3.5 使用表单事件
form元素定义了两种只适用于此元素的特殊事件。