【JavaScript高级程序设计】事件01
JavaScript与HTML的交互是通过事件实现的,事件代表文档或浏览器窗口中某个有意义的时刻。
可以使用仅在事件发生时执行的监听器(即处理程序)订阅事件。=>观察者模式 => 页面行为(JavaScript中定义)与页面展示(HTML和CSS定义)的分离。
最早的事件是为了把某些表单处理工作从服务器转移到浏览器上来。DOM2开始尝试以符合逻辑的方式来标准化DOM事件。IE8是最后一个使用专有事件系统的主流浏览器。
事件流
页面哪个部分拥有特定的事件呢?当你点击一个按钮时,实际上不光点击了这个按钮,还点击了它的容器以及整个页面。
事件流描述了页面接收事件的顺序。
IE支持事件冒泡流,Netscape Communicator支持事件捕获流
事件冒泡
从最具体的元素开始触发,然后向上传播至没有那么具体的元素(document)。
点击事件:被点击的元素最先触发click事件,然后click事件沿DOM树一路向上,在经过的每个节点上依次触发,直至到达document对象。
现代浏览器中的事件会一直冒泡到window对象。
事件捕获
最不具体的节点(document)最先收到事件,而最具体的节点最后收到事件。=>为了在事件到达最终目标前拦截事件。
点击事件:最先由document元素捕获,然后沿DOM树依次向下传播,直至到达实际的目标元素。
现代浏览器都是从window对象开始捕获事件。DOM2 Events规范规定的是从document开始。
由于旧版本浏览器不支持,通常建议使用事件冒泡,特殊情况下可以使用事件捕获。
DOM事件流
DOM2 Events规范规定事件流分为3个阶段:事件捕获、到达目标和事件冒泡。
事件捕获:最先发生,为提前拦截事件提供了可能;
实际的目标元素接收到事件;
冒泡:最迟要在这个阶段响应事件。
在DOM事件流中,实际的目标在捕获阶段不会接收到事件。下一阶段会在实际目标元素上触发事件的“到达目标”阶段,通常在事件处理时被认为是冒泡阶段的一部分;然后冒泡阶段开始,事件反向传播至文档。
虽然DOM2 Events规范明确捕获阶段不命中事件目标,但现代浏览器都会在捕获阶段在事件目标上触发事件。=> 在事件目标上有两个机会来处理事件。
所有现代浏览器都支持DOM事件流,只有IE8及更早版本不支持。
capture phase | | / \ bubbling up
-----------------| |--| |-----------------
| element1 | | | | |
| -------------| |--| |----------- |
| |element2 \ / | | | |
| -------------------------------- |
| W3C event model |
------------------------------------------
事件处理程序
事件是用户或浏览器执行的某种动作。如click、load等。
为响应事件而调用的函数被称为事件处理程序(或事件监听器)。事件处理程序的名字以“on”开头。
有多种方式可以指定事件处理程序。
HTML事件处理程序
特定元素支持的每个事件都可以使用事件处理程序的名字(onxxx)以HTML属性的形式来指定。此时属性的值必须是能够执行的JavaScript代码。
因为属性的值是JavaScript代码,所以不能在未经转义的情况下使用HTML语法字符,如&、"、<和>。为避免使用HTML实体,可以使用单引号代替双引号,或者使用\"。
在HTML中定义的事件处理程序可以包含精确的动作指令,也可以调用在页面其他地方定义的脚本。作为事件处理程序执行的代码可以访问全局作用域中的一切。
以这种方式指定的事件处理程序有一些特殊的地方:
- 会创建一个函数来封装属性的值。这个函数有一个特殊的局部变量event,即event对象;
- 在这个函数中,this值相当于事件的目标元素;
- 这个动态创建的包装函数,其作用域链被扩展了。=> document和元素自身的成员都可以被当成局部变量来访问。这是通过使用with实现的。
// 实际上的包装函数是onclick属性的值
function () {
with(document) {
with(this) {
// ... HTML事件处理程序属性值
}
}
}
=> 事件处理程序可以更方便地访问自己的属性(不用带this.)
如果元素是一个表单输入框,则作用域链中还会包含表单元素 => 事件处理程序的代码可以不必引用表单元素,而直接访问同一表单中的其他成员了(通过name属性)。
在HTML中指定事件处理程序存在的问题:
时机问题。有可能HTML元素已经显示在页面上,但事件处理程序的代码还无法执行。=> 大多数HTML事件处理程序会封装在try/catch块中,以便在这种情况下静默失败。
<input type="button" value="Click Me" onclick="try{doSomething();}catch(ex){}">
对事件处理程序作用域链的扩展在不同浏览器中可能导致不同的结果。不同JavaScript引擎中标识符解析的规则存在差异 => 访问无限定的对象成员可能导致错误。
HTML与JavaScript的强耦合。(如果要修改,必须在HTML和JavaScript中都修改代码)
DOM0事件处理程序
把一个函数赋值给(DOM元素的)一个事件处理程序属性。=> 简单
要使用JavaScript指定事件处理程序,必须先取得要操作对象的引用。
每个元素(包括window和document)都有通常小写的事件处理程序属性。
赋值代码运行之后才会给事件处理程序赋值。
所赋函数被视为元素的方法。=> 事件处理程序会在元素的作用域中运行,即this等于元素。
let btn = document.querySelector('#myBtn');
btn.onclick = function() {
console.log(this.id); // "myBtn"
}
以这种方式添加事件处理程序是注册在事件流的冒泡阶段的。
通过将事件处理程序属性的值设置为null,可以移除通过DOM0方式添加的事件处理程序。(在HTML中指定的事件处理程序,也可以通过JavaScript将相应属性设置为null来移除)
btn.onclick = null;
DOM2事件处理程序
DOM2 Events为事件处理程序的赋值和移除定义了两个方法:addEventListener()和removeEventListener()。暴露在所有DOM节点上。
接收3个参数:事件名、事件处理函数和一个表示是否在捕获阶段调用处理函数的布尔值(默认值为false,在冒泡阶段调用)。
这个事件处理程序同样在被附加到的元素的作用域中运行:this等于元素。
主要优势:可以为同一个事件添加多个事件处理程序。多个事件处理程序以添加顺序来触发。
通过addEventListener()添加的事件处理程序只能使用removeEventListener()并传入与添加时同样的参数来移除。(事件处理函数必须是同一个)
大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,主要原因是跨浏览器兼容性好。(除非需要在事件到达其指定目标之前拦截事件)
IE事件处理程序
IE实现了与DOM类似的方法,attachEvent()和detachEvent()。
接收2个参数:事件处理程序的名字和事件处理函数。因为IE8及更早版本只支持事件冒泡,所以使用attachEvent()添加的事件处理程序会添加到冒泡阶段。
在IE中使用attachEvent()与使用DOM0方式的主要区别在于事件处理程序的作用域。使用attachEvent()时,事件处理程序是在全局作用域中运行的,因此this等于window。
attachEvent()方法也可以给一个元素添加多个事件处理程序。以添加它们的顺序反向触发。
使用attachEvent()添加的事件处理程序将使用detachEvent()来移除,只要提供相同的参数(处理函数是相同的函数引用)。
let btn = document.querySelector('#myBtn');
var handler = function() {
console.log("Clicked");
};
btn.attachEvent("onclick", handler);
btn.detachEvent("onclick", handler);
跨浏览器事件处理程序
以跨浏览器兼容的方式处理事件。
自己编写跨浏览器事件处理代码主要依赖能力检测。要确保最大兼容性,只要让代码在冒泡阶段运行即可。
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 { // 默认DOM0方式
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 { // 默认DOM0方式
element["on"+type] = null;
}
}
};
// 使用
let btn = document.querySelector('#myBtn');
let handler = function() {
console.log("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
EventUtil.removeHandler(btn, "click", handler);
没有解决的存在的问题:
1、IE的作用域问题。(attachEvent()中this等于window)
2、多个事件处理程序执行顺序问题。DOM2以添加顺序,IE为添加顺序的反向
3、DOM0只支持给一个事件添加一个处理程序。(DOM0浏览器已经很少人使用,问题应该不大)
事件对象event
在DOM中发生事件时,所有相关信息(如事件目标元素、事件类型)都会被收集并存储在一个名为event的对象中。
DOM事件对象
在DOM合规的浏览器中,event对象是传给事件处理程序的唯一参数。不管以哪种方式(DOM0或DOM2)指定事件处理程序,都会传入这个event对象。在通过HTML属性指定的事件处理程序中,同样可以使用变量event引用事件对象。
所有事件对象包含的公共属性和方法:
- bubbles。布尔值。是否冒泡
- cancelable。布尔值。是否可以取消事件的默认行为
- currentTarget。元素。当前事件处理程序所在的元素
- defaultPrevented。布尔值。true表示已经调用preventDefault()方法。(DOM3 Events新增)
- detail。整数(??)。事件相关的其他信息
- eventPhase。整数。表示调用事件处理程序的阶段:1-捕获阶段;2-到达目标;3-冒泡阶段
- target。元素。事件目标元素
- trusted。布尔值。true表示事件由浏览器生成;false表示事件由开发者通过JavaScript创建。(DOM3 Events新增)
- type。字符串。被触发的事件类型
- View。AbstractView。与事件相关的抽象视图;等于事件所发生的window对象
- preventDefault()。函数。用于取消事件的默认行为 => cancelable为true时才可调用
- stopImmediatePropagation()。函数。用于取消所有后续事件捕获或事件冒泡,并阻止调用任何后续事件处理程序。(DOM3 Events新增)
- stopPropagation()。函数。用于取消所有后续事件捕获或事件冒泡 => bubbles为true时才可调用
在事件处理程序内部,this始终等于currentTarget。this === event.currentTarget。
如果事件处理程序直接添加在意图的目标,则this、currentTarget和target三者相等。
type属性在一个处理程序处理多个事件时很有用:根据事件类型,做出不同的响应。
preventDefault()可阻止特定事件的默认动作,如链接的默认行为是被单击时导航到href属性指定的URL。任何可调用preventDefault()取消默认行为的事件,其event对象的cancelable属性都会设置为true。
stopPropagation()用于立即阻止事件流在DOM结构中传播,取消后续的事件捕获或冒泡。
eventPhase属性可用于确定事件流当前所处的阶段。如果事件处理程序在目标上被调用,则eventPhase等于2 => 虽然”到达目标“是在冒泡阶段发生的,但eventPhase等于2。=> 当eventPhase等于2,this、currentTarget和target三者相等。
event对象只在事件处理程序执行期间存在,一旦执行完毕,就会被销毁。
IE事件对象
IE事件对象可以基于事件处理程序被指定的方式以不同的方式来访问。
- 如果使用DOM0方式指定,则event对象是window的一个属性
- 如果使用attachEvent()指定,则event对象会作为唯一的参数传给处理函数。此时event对象仍然是window的属性,出于方便也将其作为参数传入
- 在通过HTML属性方式指定的事件处理程序中,同样可以使用变量event引用事件对象。
所有IE事件对象都会包含的公共属性和方法:
- cancelBubble。布尔值。读/写。true表示取消冒泡(默认false),与stopPropagation()方法效果相似
- returnValue。布尔值。读/写。false表示取消事件默认行为(默认true),与preventDefault()效果相同
- srcElement。元素。事件目标,即target
- type。字符串。触发的事件类型
事件处理程序的作用域取决于指定它的方式,所以更好的方式是使用事件对象的srcElement属性代替this。(DOM0方式下,this等于元素;attachEvent()方式下,this等于window)
与DOM不同,无法通过JavaScript确定事件是否可以被取消。
cancelBubble属性与stopPropagation()方法用途相似,但IE8及更早版本不支持捕获阶段,所以只会取消冒泡。
跨浏览器事件对象
DOM事件对象中包含IE事件对象的所有信息和能力,只是形式不同。这些共性可让两种事件模型之间的映射成为可能。
var EventUtil = {
addHandler: function(element, type, handler) {
// ...
},
removeHandler: function(element, type, handler) {
// ...
},
getEvent: function(event) { // IE事件中以DOM0方式指定事件处理程序时,event对象是window的一个属性
return event ? event || window.event;
},
getTarget: function(event) {
return event.target || event.srcElement;
},
preventDefault: function(event) {
if(event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},
stopPropagation: function(event) { // 可能会停止事件冒泡,也可能既会停止事件冒泡也停止事件捕获
if(event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};
// 使用
let btn = document.querySelector('#myBtn');
btn.onclick = function(event) {
event = EventUtil.getEvent(event);
let target = EventUtil.getTarget(event);
EventUtil.preventDefault(event); // 阻止事件的默认行为
EventUtil.stopPropagation(event); // 阻止事件冒泡
};
事件类型
所发生事件的类型决定了事件对象中会保存什么信息。DOM3 Events定义的事件类型:
用户界面事件(UIEvent):与BOM交互的通用浏览器事件
焦点事件(FocusEvent):元素获得和失去焦点时触发
鼠标事件(MouseEvent):鼠标在页面上执行某些操作时触发
滚轮事件(WheelEvent):使用鼠标滚轮(或类似设备)时触发
输入事件(InputEvent):向文档中输入文本时触发
键盘事件(KeyboardEvent):键盘在页面上执行某些操作时触发
合成事件(CompositionEvent):使用某种IME(Input Method Editor,输入法编辑器)输入字符时触发
HTML5还定义了另一组事件
浏览器通常在DOM和BOM上实现专有事件:根据开发者需求,不同浏览器的实现可能不同
DOM3 Events在DOM2 Events基础上重新定义了事件,并增加了新的事件类型。所有主流浏览器都支持DOM2 Events和DOM3Events。
用户界面事件 UIEvent
不一定跟用户操作有关。保留它们是为了向后兼容。主要有以下几种:
- DOMActivate(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对象和表单控件有关。除了DOMActivate,其他在DOM2 Events中都被归为HTML Events。(DOMActivate是UI事件)
焦点事件 FocusEvent
页面元素获得或失去焦点时触发。可以与document.hasFocus()和document.activeElement一起为开发者提供用户在页面中导航的信息。
焦点事件有以下6种:
blur。失去焦点时触发。不冒泡,所有浏览器都支持
DOMFocusIn(DOM3 Events中已经废弃,推荐focusin)。获得焦点时触发。focus的冒泡版。Opera唯一支持
DOMFocusOut(DOM3 Events中已经废弃,推荐focusout)。失去焦点时触发,blur的通用版。Opera唯一支持
focus。获得焦点时触发。不冒泡,所有浏览器都支持
focusin。获得焦点时触发。focus的冒泡版
focusout。失去焦点时触发。blur的通用版
两个主要事件是focus和blur,它们最大的问题是不冒泡。
当焦点从页面中的一个元素A移到另一个元素B上,会依次发生如下事件(测试,与书中不一致):
1)A:blur
2)A:focusout
3)B:focus
4)B:focusin
DOMFocusOut和DOMFocusIn未验证
鼠标和滚轮事件MouseEvent
鼠标是用户的主要定位设备。DOM3 Events定义了9种鼠标事件:
click。用户单击鼠标主键(通常是左键)或按键盘回车键时触发。
dblclick。用户双击鼠标主键(通常是左键)时触发(DOM3 Events中标准化)
mousedown。用户按下任意鼠标键时触发。不能通过键盘触发
mouseenter。用户把鼠标光标从元素外部移到元素内部时触发。不冒泡,也不会在光标经过后代元素时触发。(DOM3 Events中新增)
mouseleave。用户把鼠标光标从元素内部移到元素外部时触发。不冒泡,也不会在光标经过后代元素时触发。(DOM3 Events中新增)
mousemove。鼠标光标在元素上移动时反复触发。不能通过键盘触发
mouseout。用户把鼠标光标从一个元素移到另一个元素上(外部元素或子元素)时触发。不能通过键盘触发
mouseover。用户把鼠标光标从元素外部移到元素内部时触发。不能通过键盘触发
mouseup。用户释放鼠标键时触发。不能通过键盘触发
页面中所有元素都支持鼠标事件。除了mouseenter和mouseleave,其他鼠标事件都会冒泡,都可以被取消,这会影响浏览器的默认行为。由于事件之间存在关系,因此取消鼠标事件的默认行为也会影响其他事件。
双击鼠标主键会按如下顺序触发事件:
1)mousedown
2)mouseup
3)click
4)mousedown
5)mouseup
6)click
7)dblclick
click和dblclick在触发前都依赖其他事件触发,mousedown和mouseup则不会受其他事件影响。
IE8和更早的版本的实现中存在问题,会导致双击事件跳过第二次mousedown和click事件。
1)mousedown
2)mouseup
3)click
4)mouseup
5)dblclick
DOM3 Events中鼠标事件对应的类型是”MouseEvent“(单数形式)
鼠标事件还有一个名为滚轮事件的子类别。滚轮事件只有一个事件mousewheel,对应鼠标滚轮或带滚轮的类似设备上滚轮的交互。
鼠标事件event对象的一些属性:
1.客户端坐标
注:客户端坐标不考虑页面滚动
2.页面坐标
在页面上的位置。表示事件发生时鼠标光标在页面上的坐标,通过event对象的pageX和pageY属性获取。
反映的是光标到页面而非视口左边与上边的距离。
3.屏幕坐标
鼠标光标在屏幕上的坐标,通过event对象的screenX和screenY属性获取。
4、修饰键
有时要确定用户想实现的操作,还要考虑键盘按键的状态。
键盘上的修饰键Shift、Ctrl、Alt和Meta(win的window键,mac的command键)经常用于修改鼠标事件的行为。
4个属性来表示这几个修饰键的状态:shiftKey、ctrlKey、altKey、metaKey。(被按下为true,否则为false)
现在浏览器支持所有4个修饰键,IE8及更早版本不支持metaKey属性。
相关元素
对mouseover和mouseout事件而言,还存在与事件相关的其他元素。
鼠标按键
对mousedown和mouseup事件来说,event对象上会有一个button属性,表示按下或释放的是哪个按键。DOM为button属性定义了3个值:0-主键;1-中键(通常是滚轮键);2-副键。
IE8及更早版本也提供了button属性,考虑了同时按多个键的情况。
额外事件信息
DOM2 Events规范在event对象上提供了detail属性,以给出关于事件的更多信息。对鼠标事件来说,detail包含一个数值,表示在给定位置上发生了多少次单击(连续单击)。每次单击会加1。连续点击中断会重置为0。
IE还为每个鼠标事件提供了以下额外信息:
altLeft,布尔值,是否按下了左Alt键(如果为true,则altKey也为true)
ctrlLeft,布尔值,是否按下左Ctrl键(如果为true,则ctrlKey也为true)
offsetX,光标相对于目标元素边界的x坐标
offsetY,光标相对于目标元素边界的y坐标
shiftLeft,布尔值,是否按下了左Shift键(如果为true,则shiftKey也为true)
滚轮mousewheel事件
在用户使用鼠标滚轮时触发,包括在垂直方向上任意滚动。会在任何元素上触发,并(在IE8中)冒泡到document和(所有现代浏览器中)window。
event对象包含鼠标事件的所有标准信息,此外还有一个名为wheelDelta的属性。
多数情况下只需知道滚轮滚动的方向,而这通过wheelDelta值的符号就可以知道。(向前滚动一次+120,向后滚动一次-120)
触摸屏设备
触摸屏通常不支持鼠标操作。
不支持dblclick事件。(测试一加三可以)
单指点触屏幕上的可点击元素会触发mousemove事件。(测试一加三不行)可点击元素是指点击时有默认动作的元素(如链接)或指定了onclick事件处理程序的元素
mousemove事件也会触发mouseover和mouseout事件。(还未测试)
双指点触屏幕并滑动导致页面滚动时会触发mousewheel和scroll事件。(还未测试)
无障碍问题
如果Web应用或网站要考虑残障人士,特别是使用屏幕阅读器的用户,那么必须小心使用鼠标事件(除了回车键可以触发click事件,其他鼠标事件不能通过键盘触发)。建议不要使用click事件之外的其他鼠标事件向用户提示功能或触发代码执行。=> 会严格妨碍盲人或视障用户使用。
几条使用鼠标事件时应该遵循的无障碍建议:
使用click事件执行代码。当使用onmousedown执行代码时,应用程序会运行得更快,但屏幕阅读器无法触发mousedown事件
不要使用mouseover向用户显示新选项。无法触发。可以考虑键盘快捷键
不要使用dblclick执行重要的操作。无法触发
更多网站无障碍的信息,可以参考WebAIM网站。