js高程读书笔记 第13章 事件

[TOC]

本章内容

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

JavaScript与HTML之间的交互是通过事件实现的。

事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。可以使用侦听器来预订事件。

事件流

事件冒泡

IE的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点。

IE5.5冒泡会跳过html元素,现代浏览器会冒泡到windows对象

事件捕获

DOM2级事件 规范要求事件应该从document对象开始传播,现代浏览器一般从window对象开始捕获事件。

DOM事件流

事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。

事件处理程序

事件就是用户或浏览器自身执行的某种动作。click、load和mouseover都是事件的名字。事件处理程序的名字以“on”开头。

响应某个事件的函数就叫做事件处理程序(事件侦听器)

HTML事件处理程序

某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的HTML特征来指定。

1
2
3
4
5
<script type="text/javascript">
function showMessage(){
alert("Hello World!");
}
</script>
1
<input type="button" value="Click Me" onclick="showMessage()" />

事件处理程序中的代码在执行时,有权访问全局作用域。

用这种方式会创建一个封装着元素属性值的函数。这个函数中有一个局部变量event,也就是事件对象。

通过event变量,可以直接访问时间对象。

在这个函数内部,this值等于事件的目标元素。

1
<input type="button" value="Click Me" onclick="alert(this.value)">

可以在函数内部使用with拓展作用域。

HTML中指定事件处理程序有两个缺点。

  • 存在时差问题。未加载函数就按下按钮 解决方法:用try…catch…捕获错误
  • 作用域链在不同浏览器中不同

DOM0级事件处理程序

将一个函数赋值给一个事件处理程序属性。

使用DOM0级方法指定的事件处理程序被认为是元素的方法。事件处理程序是在元素的作用域中进行,程序中的this引用当前元素。这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。

DOM2级事件处理程序

addEventListener()和removeEventListener()

接收三个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。

1
2
3
4
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);

使用DOM2级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。

IE事件处理程序

attachEvent()和detachEvent()接收两个参数:事件处理程序名称与事件处理程序函数。

通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段。

1
2
3
4
var btn = document.getElementById("myBtn");
btn.attachEvent('click', function() {
alert("Clicked");
});

attachEvent与使用DOM0级的方法的主要区别在于事件处理程序的作用域。在使用DOM0级方法的情况下,事件处理程序会在其所属元素的作用域内运行;在使用attachEvent()方法的情况下,事件处理程序会在全局作用域中运行,因此this等于window

跨浏览器的事件处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}
},

};

事件对象

要阻止特定事件的默认行为,可以使用preventDefault()方法。

stopPropagation()方法用于立即停止事件处理程序在DOM层次中传播

事件对象的eventPhase属性,可以用来确定事件当前正位于事件流的哪个阶段。如果在捕获阶段调用的事件处理程序,那么等于1;如果事件处理程序处于目标对象上,则eventPhase等于2;如果是在冒泡阶段调用的事件处理程序,eventPhase等于3。

IE中的事件对象

访问DOM中的even对象不同,要访问IE中的event对象有几种不同的方式,取决于指定事件处理程序的方法。

在使用DOM0级方法添加事件处理程序时,event对象,event对象作为window对象的一个属性存在。

1
2
3
4
5
var btn = document.getElementById("myBtn");
btn.onclick = function() {
var event = window.event;
console.log(event.type);//click
}

跨浏览器的事件对象

这一节介绍了EventUtil.js的对象增强,以适应不同浏览器对事件的操作。

事件类型

  • ui事件
  • 焦点事件
  • 鼠标事件
  • 滚轮事件
  • 文本事件
  • 键盘事件
  • 合成事件
  • 变动事件 底层DOM结构发生变化

UI事件

  1. load事件
  2. unload事件 文档被完全卸载后或者从从一个页面切换到另外一个页面就会发生
  3. resize事件
  4. scroll事件

焦点事件

当焦点从页面中个一个元素移动到另一个元素,会依次触发下列事件:

  1. focusout
  2. focusin
  3. blur
  4. DOMFocusOut
  5. focus
  6. DOMFocusIn

鼠标与滚轮事件

click: 用户单机主鼠标按钮或者按下回车键触发

dblclick: 拥护双击主鼠标按钮时触发

mousedown: 在用户按下了任意鼠标按钮时触发。

mouseenter:在鼠标光标从元素外部首次移动到了元素范围内触发。

mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。

mousemove: 当鼠标指针在元素内部移动时重复地触发。

mouseout: 在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。

mouseover: 在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内触发

mouseup: 在用户释放鼠标按钮时触发

只有在一个元素上相继触发mousedown和mouseup事件,才回触发click事件;如果mousedown或者mouseup事件中一个被取消,就不会触发click事件。类似的只有触发两次click事件才会触发一次dbclick事件。这4个事件触发的顺序始终如下:

  1. mousedown
  2. mouseup
  3. click
  4. mousedown
  5. mouseup
  6. click

在IE8及之前有个小bug,会跳过第二个mousedown和click事件

鼠标事件中还有一类滚轮事件mousewheel。

客户区坐标位置

1
<div id="myDiv" style="background-color:red;height:100px;width:100px">Click me</div>
1
2
3
4
5
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "click", function(event){
event = EventUtil.getEvent(event);
alert("Client coordinates: " + event.clientX + "," + event.clientY);
});

页面坐标位置

1
<div id="myDiv" style="background-color:red;height:10000px;width:100px">Click me</div>

document.body(混杂模式) document.documentElement(标准模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "click", function(event){
event = EventUtil.getEvent(event);
var pageX = event.pageX,
pageY = event.pageY;

if (pageX === undefined){
pageX = event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft);
}

if (pageY === undefined){
pageY = event.clientY + (document.body.scrollTop || document.documentElement.scrollTop);
}

alert("Page coordinates: " + event.pageX + "|" + pageX + "," + event.pageY + "|"+pageY);
});

屏幕坐标位置

1
<div id="myDiv" style="background-color:red;height:100px;width:100px">Click me</div>
1
2
3
4
5
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "click", function(event){
event = EventUtil.getEvent(event);
alert("Screen coordinates: " + event.screenX + "," + event.screenY);
});

修改键

shift、ctrl、alt和meta(windows下是windows键,苹果中是cmd键)

1
<div id="myDiv" style="background-color:red;height:100px;width:100px">Click me while holding a modifier key</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "click", function(event){
event = EventUtil.getEvent(event);
var keys = new Array();

if (event.shiftKey){
keys.push("shift");
}

if (event.ctrlKey){
keys.push("ctrl");
}

if (event.altKey){
keys.push("alt");
}

if (event.metaKey){
keys.push("meta");
}

alert("Keys: " + keys.join(","));

});

相关元素

对于mouseover事件,事件的主目标是获得光标的元素,而相关元素就是那个失去光标的元素。

对于mouseout事件,事件的主目标是失去光标的元素,而相关元素则是获得光标的元素。

1
<div id="myDiv" style="background-color:red;height:100px;width:100px">Move the mouse from here to the white</div>

EventUtil中部分代码

1
2
3
4
5
6
7
8
9
10
11
12
getRelatedTarget: function(event){
if (event.relatedTarget){
return event.relatedTarget;
} else if (event.toElement){
return event.toElement;
} else if (event.fromElement){
return event.fromElement;
} else {
return null;
}

},

1
2
3
4
5
6
7
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "mouseout", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
var relatedTarget = EventUtil.getRelatedTarget(event);
alert("Moused out of " + target.tagName + " to " + relatedTarget.tagName);
});

鼠标按钮

对于mousedown和mouseup事件,在其event对象存在一个button属性,表示按下或释放的按钮。

DOM的button属性可能有如下三个值:0表示主鼠标按钮,1表示中间的按钮,2表示次鼠标按钮。

IE8及之前版本也提供了button属性,但属性的值与DOM的button属性有很大差异。

EventUtil中部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
getButton: function(event){
if (document.implementation.hasFeature("MouseEvents", "2.0")){
return event.button;
} else {
switch(event.button){
case 0://表示没有按下按钮
case 1://主按钮
case 3://同时按下主、次按钮
case 5://同时按下主鼠标和中间的鼠标按钮
case 7://同时按下三个按钮
return 0;
case 2://按下次鼠标按钮
case 6://同时按下次鼠标和中间鼠标按钮
return 2;
case 4: return 1;//按下了中间鼠标按钮
}//以上代码表示把主按钮优先级调到最高,次按钮其次,中间按钮最后
}
},

更多的事件信息

event对象中还提供了detail属性,在同一个元素上相继发生一次mousedown和mouseup事件算作一次单机。如果鼠标在mousedown和mouseup之间移动了位置,则detail会被重置。

鼠标滚轮事件

P376

触摸设备

  • 不支持dblclick事件
  • 轻击可单击元素会触发mousemove事件。
  • mousemove事件也会触发mouseover和mouseout事件。
  • 两个手指放在频幕上且页面随手指滚动而滚动时会触发mousewheel和scroll事件

键盘与文本事件

用户按了一下键盘上的字符键时,首先会触发keydown事件,然后紧跟着是keypress事件,最后会触发keyup事件。如果用户按着字符键不放,会重复触发keydown和keypress事件,直到用户松开该键为止。

如果按下的是非字符键,首先会触发keydown事件,然后是keyup事件。如果按住这个非字符键不放,就会一直触发keydown事件直到用户松开这个键。

  1. 键码

event对象的keyCode属性会报刊一个代码,与键盘上一个特定的键对应。键值与ASCIII对应

  1. 字符编码

现代浏览器使用charCode,只有使用keypress事件时才会包含值

  1. DOM3级变化

key和char

keyIdentifier 返回U+0000这类字符串

location属性,0表示默认键盘,1表示左侧键盘,2表示右侧位置,3表示数字小键盘,4表示移动设备键盘,5表示手柄。在IE9中支持。Safari和Chrome中支持名为keyLocation的等价属性。

  1. textInput事件
    这个用于替代keypress的textInput事件的行为稍有不同。区别之一就是任何可以获得焦点的元素都可以出发keypress事件,但只有可编辑区域才能出发textInput事件。区别之二是textInput事件只会在用户按下能够输入实际字符的键时才会被触发,而keypress事件则是那些能影响文本显示的键时也会触发。

textInput事件主要考虑的是字符,它的event对象中还包含一个data属性,这个属性的值就是用户输入的字符。

event对象上还有一个属性,叫inputMethod,表示把文本输入到文本框中的方式。
如1表示键盘输入的,2表示粘贴进来,3表示拖放进来等等。。

复合事件

变动事件

  • DOMSubtreeModified: 在DOM结构中发生任何变化时触发。
  • DOMNodeInserted: 在一个节点作为子节点被被插入到另一个节点中时触发。
  • DOMNodeRemoved: 在节点从其父节点中被移除时触发
  • DOMNodeInsertedIntoDocument: 在一个节点被直接插入文档或通过子树间接插入文档之后
  • DOMNodeRemovedFromDocument: 在一个节点被直接从文档中移除或者通过子树间接从文档中移除之前触发。
  • DOMAttrModified: 在特性被修改后触发
  • DOMCharacterDataModified: 在文本节点的值发生变化时触发

删除节点

使用removeChild()或replaceChild()从DOM中删除节点时,首先会触发DOMNodeRemoved事件。

如果被移除的节点包含子节点,那么在其所有子节点以及这个被移除的节点上会相继触发DOMNodeRemovedFrom事件,不过该事件不会冒泡,只有直接指定给其中一个子节点的事件处理程序才会被调用。

紧随其后触发的是DOMSubtreeModified事件。这个事件的目标是被移除节点的父节点。

插入节点

使用appendChild()、replaceChild()或insertBefore()向DOM插入节点时,首先会触发DOMNodeInserted事件。

紧接着会触发DOMNodeInsertedIntoDocument事件,不冒泡。

最后是DOMSubteeModified,触发于新插入节点的父节点。

HTML5事件

  1. contextmenu事件

用以表示何时应该显示上下文菜单,以便开发人员取消默认的上下文菜单而提供自定义的菜单。事件冒泡。

在兼容DOM的浏览器中,使用event.preventDefalut();取消默认事件

1
2
3
4
5
6
<div id="myDiv">Right click or Ctrl+click me to get a custom context menu. Click anywhere else to get the default context menu.</div>
<ul id="myMenu" style="position:absolute;visibility:hidden;background-color:silver">
<li><a href="http://www.nczonline.net">Nicholas' site</a></li>
<li><a href="http://www.wrox.com">Wrox site</a></li>
<li><a href="http://www.yahoo.com">Yahoo!</a></li>
</ul>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

EventUtil.addHandler(window, "load", function(event){
var div = document.getElementById("myDiv");

EventUtil.addHandler(div, "contextmenu", function(event){
event = EventUtil.getEvent(event);
EventUtil.preventDefault(event);

var menu = document.getElementById("myMenu");
menu.style.left = event.clientX + "px";
menu.style.top = event.clientY + "px";
menu.style.visibility = "visible";
});

EventUtil.addHandler(document, "click", function(event){
document.getElementById("myMenu").style.visibility = "hidden";
});
});
  1. beforeunload事件
    会在卸载页面的时候触发
1
<div id="myDiv">Try to navigate away from this page.</div>
1
2
3
4
5
6
EventUtil.addHandler(window, "beforeunload", function(event){
event = EventUtil.getEvent(event);
var message = "I'm really going to miss you if you go.";
event.returnValue = message;
return message;
});

测试了一下chrome好像不能自定义消息。

  1. DOMContentLoaded事件
    window的load事件会在页面中的一切都加载完毕后触发

DOMContentLoaded则在形成完整的DOM树后就会触发。不会理会图像、JavaScript文件和CSS文件或者其他资源是否已经下载完毕。

  1. readystatechange事件
    支持readystatechange事件的每个对象都有一个readyState属性。
  • uninitialized: 对象存在但未初始化
  • loading: 对象正在加载数据
  • loaded: 对象加载数据完成
  • interactive: 可以操作对象,但还没有完全加载
  • complete: 对象已经加载完毕

外部资源较多时会在load事件触发之前进入交互阶段;而在包含较少或较少的外部资源的页面中,很难说readystatechange事件会发生在load事件前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
EventUtil.addHandler(window, "load", function(){

//create a new <script/> element.
var script = document.createElement("script");
EventUtil.addHandler(script, "readystatechange", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
if (target.readyState == "loaded" || target.readyState == "complete"){//防止被执行两次
EventUtil.removeHandler(target, "readystatechange", arguments.callee);
alert("Script Loaded");
}
});
script.src = "example.js";
document.body.appendChild(script);

//create a new <link/> element
var link = document.createElement("link");
link.type = "text/css";
link.rel= "stylesheet";

EventUtil.addHandler(link, "readystatechange", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
if (target.readyState == "loaded" || target.readyState == "complete"){//防止被执行两次
EventUtil.removeHandler(target, "readystatechange", arguments.callee);
alert("CSS Loaded");
}
});
link.href = "example.css";
document.getElementsByTagName("head")[0].appendChild(link);

});
  1. pageshow和pagehide事件

firefox和opera有一个往返缓存,可以在用户使用浏览器的后退和前进按钮时加快页面的转换速度。

pageshow事件在页面显示时触发。pageshow事件还有一个persisted属性,如果页面被保存在bfcache中,则这个属性为ture。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

(function(){
var showCount = 0;
EventUtil.addHandler(window, "load", function(){
alert("Load fired");
});
EventUtil.addHandler(window, "pageshow", function(event){
showCount++;
alert("Show has been fired " + showCount + " times. Persisted? " + event.persisted);
});
EventUtil.addHandler(window, "pagehide", function(event){
alert("Hiding. Persisted? " + event.persisted);
});
})();
  1. hashchange事件

用户URL的参数列表(以及URL中#后面的所有字符串)发生变化时作用。

此时的event包含两个对象:oldURL和newURL。这两个属性分别保存着参数列表前后变化的完整URL。

1
2
3
4
5
6
<p>Click each of these links to change the URL hash.</p>
<ul>
<li><a href="#up">Up</a></li>
<li><a href="#down">Down</a></li>
</ul>
<p>This example only works in browsers that <code>onhashchange</code>.</p>
1
2
3
EventUtil.addHandler(window, "hashchange", function(event){
alert("Old URL: " + event.oldURL + "\nNew URL: " + event.newURL);
});

为了保证兼容性,最好使用location对象来确定当前参数列表

1
2
3
EventUtil.addHandler(window, "hashchange", function(event) {
alert("CUrrent hash: " + location.hash);
});

设备事件

  1. orientationchange事件

safari中

window.orientation属性中可能包含的值:0表示肖像模式,90表示左旋转(home键在右),-90则反之。

1
2
3
4
5
6
7
8
EventUtil.addHandler(window, "load", function(event){
var div = document.getElementById("myDiv");
div.innerHTML = "Current orientation is " + window.orientation;

EventUtil.addHandler(window, "orientationchange", function(event){
div.innerHTML = "Current orientation is " + window.orientation;
});
});
  1. MozOrientation事件
1
2
<p>This example only works in Firefox 3.6+ in devices such as Macbooks, Thinkpads, Windows Mobile, or Android.</p>
<div id="output"></div>
1
2
3
4
EventUtil.addHandler(window, "MozOrientation", function(event){
var output = document.getElementById("output");
output.innerHTML = "X=" + event.x + ", Y=" + event.y + ", Z=" + event.z + "<br>";
});
  1. deviceorientation事件
1
2
3
4
<p>This example only works in Chrome in devices such as Macbooks, Thinkpads, or Android, or on Safari for iOS 4.2+.</p>
<div id="output"></div>
<div id="arrow" style="background:#000;width:3px;height:300px;margin:0 auto;"></div>
<script>
1
2
3
4
5
6
EventUtil.addHandler(window, "deviceorientation", function(event){
var output = document.getElementById("output");
var arrow = document.getElementById("arrow");
arrow.style.webkitTransform = "rotate(" + Math.round(event.alpha) + "deg)";
output.innerHTML = "Alpha=" + event.alpha + ", Beta=" + event.beta + ", Gamma=" + event.gamma + "<br>";
});

触摸与手势事件 p399

  1. 触摸事件
  • touchstart
  • touchmove
  • touchend
  • touchcancel
  1. 手势事件

内存和性能

事件委托

1
2
3
4
5
 <ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function(){
var list = document.getElementById("myLinks");

EventUtil.addHandler(list, "click", function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);

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":
alert("hi");
break;
}
});

})();

我们使用事件委托只为ul元素添加了一个onclick事件处理程序。由于所有列表项都是这个元素的子节点,而且他们的事件会冒泡,所以单击事件最终会被这个函数处理。

如果可行的话,可以考虑为document对象添加一个事件处理程序,用以处理页面上发生的某种特定类型的事件。使用事件委托的好处:

  • document对象很快就可以访问
  • 在页面中设置事件处理程序所需时间更少。
  • 整个页面占用的内存更少,能够提升整体性能。

最适合采用事件委托技术的事件包括click、mousedown、mouseup、keydown、keyup和keypress

移除事件处理程序

如果带有事件处理程序的元素被innerHTML删除,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。

模拟事件

DOM中的事件模拟

在document对象上使用createEvent()方法创建event对象。这个方法接收一个参数,即表示要创建的事件类型的字符串。

创建了event对象后还需要使用与事件有关的信息对其进行初始化。最后一步是触发事件。使用dispatchEvent()方法。

  1. 模拟鼠标事件 参数p406
1
2
3
<input type="button" value="Click me" id="myBtn" />
<input type="button" value="Send click to the other button" id="myBtn2" />
<p>This example works in DOM-compliant browsers (not IE).</p>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function(){
var btn = document.getElementById("myBtn");
var btn2 = document.getElementById("myBtn2");

EventUtil.addHandler(btn, "click", function(event){
alert("Clicked!");
alert(event.screenX); //100
});

EventUtil.addHandler(btn2, "click", function(event){
//create event object
var event = document.createEvent("MouseEvents");

//initialize the event object
event.initMouseEvent("click", true, true, document.defaultView, 0, 100, 0, 0, 0, false,
false, false, false, 0, btn2);

//fire the event
btn.dispatchEvent(event);

});

})();
  1. 模拟键盘事件 参数p407

  2. 模拟其他事件

  3. 自定义DOM事件

小结

要尽量限制一个页面中事件处理程序的数量,数量太多会导致占用大量内存,而且也会有一定卡顿。建立在事件冒泡机制智商的事件委托技术,可以有效地减少事件处理程序的数量。在浏览器卸载页面之前最好可以移除页面中所有事件处理程序。