js与HTML之间的交互是通过事件实现的。可以使用侦听器来预订事件,以便事件发生时执行相应的代码,这种模式被称为观察者模式。

1. 事件流

事件流描述的是从页面中接收事件的顺序。IE的事件流是事件冒泡流,而Netscape Communicator的事件流失事件捕获流。

  • 事件捕获
    由不太具体的节点更早接收到事件,而最具体的节点最后接收到事件。事件捕获的用意在于,在事件到达预定目标之前捕获它。

    1
    document -> <html> -> <body> -> <div>
  • 事件冒泡
    即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。

    1
    <div>  ->  <body>  -> <html> -> document

2. 绑定事件

2.1 <div onclick="">

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

1
2
3
4
5
<input type="button" onclick="alert('clicked!')">

<!-- onclick有权访问全局作用域中的任何代码 -->
<input type="button" onclick="showMessage()">
<input type="button" onclick="try{showMessage();}catch(ex){}">

假设showMessage()函数是在按钮下方,页面的最底部定义的。如果用户在页面解析showMessage()之前就单机了按钮,就会引发错误。为此,很多HTML事件处理程序都会被装在一个try-catch块中。

2.2 elem.onclick

DOM0级事件绑定,使用elem.onclick=function(){}绑定事件,被认为是元素的方法。这时候,事件处理程序是在元素的作用域中进行。

1
2
3
4
5
var btn = document.getElementById('myBtn');
btn.onclick = function(){
alert(this.id); // 'myBtn',this引用当前元素
}
btn.onclick = null; // 删除事件处理程序

2.3 addEventListener

DOM2级事件绑定: addEventListener() 和 removeEventListener()。接受3个参数:要处理的事件名,事件处理函数,和一个布尔值(true: 在捕获阶段调用事件, false: 在冒泡阶段调用)。为同一个元素添加两次addEventListener,两个事件会按添加顺序依次触发。

1
2
3
4
5
var handler = function(){
alert(this.id);
}
btn.addEventListener('click', handler, false)
btn.removeEventListener('click', handler, false)

2.4 attachEvent

IE实现了两个类似的方法: attachEvent() 和 detachEvent()。与addEventListener不同,attachEvent,事件处理程序会在全局作用域中运行。

1
2
3
4
5
var handle = function(){
alert(this === window); // true
}
btn.attachEvent('onclick', handler);
btn.detachEvent('onclick', handler);

3. event对象

在触发dom上的某个事件时,会产生一个事件对象event || window.event(IE),这个对象中包含着所有与事件有关的信息。包含导致事件的元素、事件的类型以及其他与特定事件相关的信息。如,鼠标操作导致的事件中,会包含鼠标位置的信息。键盘操作的事件中,会包含与按下的键有关的信息。

属性/方法 说明
bubbles Boolean 事件是否冒泡
cancelable Boolean 是否可以取消事件的默认行为
currentTarget Element 当前正在处理事件的那个元素
defaultPrevented Boolean true,表示已经调用了preventDefautl()
detail Interger 与事件相关的细节信息
eventPhase Interger 调用事件处理程序的阶段:1-捕获阶段,2-处于目标,3-冒泡阶段
preventDefault() Function 取消事件的默认行为
stopImmediatePropagation() Function 取消事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用
stopPropagation() Function 取消事件的进一步捕获或冒泡。如果bubbles为true,则可以使用这个方法
target Element 事件的目标
trusted Boolean 表明事件是浏览器生成的
type String 被触发的事件的类型
view AbstractView 与事件关联的抽象视图。等同于发生事件的window对象

3.1 currentTarget, target

currentTarget始终等于对象this的值,target则只包含事件的实际目标。

1
2
3
4
5
document.body.onclick = function(event){
alert(event.currentTarget === document.body); // true
alert(this === document.body); // true
alert(event.target === document.getElementById("myBtn")) // true
}

3.2 event.type

在需要通过一个函数处理多个事件时,可以使用type属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var handler = function(event){
switch(event.type){
case "click":
alert("clicked");
break;

case "mouseover":
event.target.style.backgroundColor = "red";
break;

case "mouseout":
event.target.style.backgroundColor = "";
break;
}
}
btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;

3.3 event.preventDefault()

阻止特定事件的默认行为,例如<a>被单击时会导航到其href特性指定的URL。

1
2
3
4
// 取消了链接导航这一默认行为
link.onclick = function(event){
event.prevenDefault();
}

3.4 stopPropagation()

立即停止事件在DOM层次中的传播,取消进一步的事件捕获或冒泡。

1
2
3
4
5
6
7
8
// 在按钮上调用stopPropagation(),从而避免触发注册在document.body上面的事件
btn.onclick = function(event){
alert("Clicked");
event.stopPropagation();
}
document.body.onclick = function(event){
alert("Body clicked");
}

3.5 IE中的event

属性/方法 说明
cancelBubble Boolean 默认为false,设置为true可以取消事件冒泡
returnValue Boolean 默认为true,设置为false可以取消事件的默认行为
srcElement Element 事件的目标(与DOM的target属性相同)
type String 事件的类型
1
2
3
4
5
6
7
8
// function的作用域是根据指定它的方式来确定的,不能认为this始终等于事件目标。用event.srcElement比较保险
btn.onclick = function(){
alert(window.event.srcElement === this); // true
}
btn.attachEvent("onclick", function(event){
alert(event.srcElement === this); // false
window.event.cancelBubble = true;
})

3.6 event.clientX, pageX,screenX

得知事件发生的坐标。

1
2
3
4
5
6
7
8
9
10
btn.onclick = function(event){
// 浏览器视口中的特定位置上发生
alert('Client coordinates:' + event.clientX + event.clientY);

// 在页面中的位置,页面没有滚动的情况下,pageX和clientX相等
alert('Page coordinates:' + event.pageX + event.pageY);

// 相对于整个电脑屏幕的位置
alert('Screen coordinates:' + event.screenX + event.screenY);
}

3.7 修改键

在按下鼠标键盘上的某些键的状态也可以影响到所要采取的操作。这些修改键是shift, ctrl, alt和meta(windows里是windows键,苹果梨是cmd键)。DOM定义了4个属性,来表示这些修改键的状态: shiftKey, ctrlKey, altKey, metaKey(Boolean值)

1
2
3
btn.onclick = function(event){
if (event.shiftKey){}
}

4. 跨浏览器绑定事件

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
33
34
35
36
37
38
39
40
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;
}
},
getEvent: function(event){
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;
}
}
}

5. 事件类型

5.1 load

window.load当页面完全加载完后(包括所有图像,js文件,css文件等外部资源),就会触发window上的load事件。

1
2
3
4
EventUtil.addHandler(window, "load", function(event){
alert("Loaded");
})
<body onload="alert('Loaded')"></body>

图像上也可以触发load事件,图像加载完毕后触发事件。

1
2
3
4
EventUtil.addHandler(imgElem, "load", function(event){
alert("image loaded");
})
<img src="1.jpg" onload="alert('image loaded')">

<script>元素也会触发load事件,以便确定动态加载的js文件是否加载完毕。

1
2
3
4
var script = document.createElement("script");
script.onload = function(){};
script.src = "1.js";
document.body.appendChild(script);

5.2 焦点事件

焦点事件会在页面元素获得或失去焦点时触发。利用这些事件与document.hasFocus()document.activeElement属性配合,可以知晓用户在页面上的行踪。

  • document.hasFocus()
  • document.activeElement
  • focus: 在元素获得焦点时触发
  • blur: 在元素失去焦点时触发

5.3 鼠标与滚轮事件

鼠标事件 说明
click 单击。只有在同一个元素上相继触发mousedown和mouseup事件,才会触发click事件
dbclick 双击
mousedown 用户按下任意鼠标按钮时触发
mouseup 用户释放鼠标按钮时触发
mouseenter 光标从元素外部首次移动到元素范围内触发
mouseleave 在位于元素上方的光标移动到元素范围外触发
mousemove 当光标在元素内部移动时重复触发
mouseover 鼠标进入,同mouseover
mouseout 鼠标移出, 同mouseout

事件触发顺序: mousedown -> mouseup -> click

5.4 键盘事件

键盘事件 说明
keydown 当用户按下键盘上的任意键时触发,长按不放,会重复触发
keypress 当用户按下键盘上的字符键时触发,长按不放,会重复触发
keyup 当用户释放键盘上的键时触发

用户按下一个字符键时候,会依次触发 keydown -> keypress -> keyup。

  • event.keycCode 非字符键的键码
    在发生keydown和keyup事件时,会有一个event.keyCode,与键盘上一个特定的键对应。
  • event.charCode 字符键的键码
    只有在keypress的时候才触发,event.charCode是键的ascii码。
  • event.key, event.keyIdentifier, event.keyLocation
    DOM3级事件的键盘事件,不再包含charCode属性,而包含了两个新属性: key和char。
    event.key对应的是相应的文本字符(如k,M)。
    event.keyIdentifier返回一个类似”U+0000”的字符串,表示Unicode值。(chrome和safari支持)
    event.location || event.keyLocation表示按下了什么位置上的键: 0-默认键盘,1-左侧位置, 2-右侧位置, 3-数字小键盘, 4-移动设备键盘(虚拟键盘), 5-手柄。(IE9,safari,chrome支持)

  • textInput事件
    DOM3引入了一个新事件,只有可编辑区域才能触发textInput事件。它的event.data属性,就是用户输入的字符。event.inputMethod,表示文本输入到文本框中的方式。

5.5 html5事件

5.5.1 contextmenu

设置鼠标右击菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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';
})
})

5.5.2 beforeunload

IE, firefox, safari, chrome都支持beforeunload事件,在浏览器卸载页面之前询问用户是否真的要关闭页面。

1
2
3
4
5
EventUtil.addHandler(window, 'beforeunload', function(event){
event = EventUtil.getEvent(event);
event.returnValue = "i'm really going to miss you if you go.";
return message;
})

5.5.3 DOMContentLoaded

window的load事件会在页面中的一切都加载完毕时触发,DOMContentLoaded事件则会在形成完整的DOM树之后触发。不理会图像,js,css或其他资源是否已经下载完毕。

5.5.4 pageshow, pagehide

window.pageshow页面显示时触发。 window.pagehide在浏览器卸载页面时触发,在unload事情前触发。

5.5.5 hashchange

在ajax应用中,开发人员经常要利用url参数来保存状态或导航信息。url中#号后面的字符串发生变化时, hashchange事件就会被触发。

1
2
3
EventUtil.addHandler(window, 'hashchange', function(event){
console.log('current hash:' + location.hash);
})

5.6 设备事件

5.6. 1 orientationchange

苹果safari添加了orientationchange时间,确定用户何时将设备由横向查看模式切换为纵向查看模式。

1
2
3
4
EventUtil.addHanlder(window, 'load', function(event){
// 0 肖像模式,90 左旋转的横向模式, -90 右旋转的横向模式
div.innerHTML = 'Current orientation is' + window.orientation;
})

5.6.2 deviceorientation

表示设备在空间中朝向哪里。

1
2
3
4
EventUtil.addHandler(window, 'deviceorientation', function(event){
// alpha, beta, gamma分别指示围绕z,x,y轴旋转时y轴,z轴,z轴的度数差
console.log(event.alpha + event.beta + event.gamma);
})

5.6.3 devicemotion

表示设备什么时候移动。事件对象包含以下属性: acceleration每个方向上的加速度; accelerationIncludingGravity在考虑z轴自然重力加速度的情况下,告诉你在每个方向上的加速度。rotationRate方向值。

5.7 触摸与手势事件

touchstart : 当手指触摸屏幕时触发
touchmove : 当手指在屏幕上滑动时连续第触发
touchend : 当手指在屏幕上移开时触发
touchcancel : 当系统停止跟踪触摸时触发

只有两个手指都触摸,才会触发这些事件
gesturestart : 当一个手指已经按在屏幕上而另一个手指又触摸屏幕时触发
gesturechange : 当触摸屏幕的任何一个手指的位置发生变化时触发
gestureend : 当任何一个手指从屏幕上面移开时触发

其他

  • unload
    window.onunload 在文档被完全卸载后触发。只要用户从一个页面切换到另一个页面,就会发生unload事件。这个事件最多的情况是清除引用,以避免内存泄露。

  • resize
    window.onresize 浏览器窗口被调整到一个新的高度或宽度时触发

  • scroll
    window.scroll 在文档被滚动期间重复触发

  • DOM变动事件
    DOMNodeRemoved会在使用removeChild()或replaceChild()删除结点时触发,event.target是被删除的节点,event.relatedNode包含着目标节点父节点的引用。
    DOMNodeInserted会在appendChild(), replaceChild(), insertBefore()插入节点时触发。

6. 内存与性能

6.1 事件委托

对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,为整个页面指定一个onclick事件处理程序,而不必为每个可单击的元素分别添加事件处理程序。

1
2
3
4
5
6
7
8
9
10
EventUtil.addHandler(list, 'click', function(event){
var event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);

switch(target.id){
case 'doSomething':
doSomething();
break;
}
})

document对象很快就可以访问,可以在页面生命周期的任何时点上为它添加事件处理程序(无需等待DOMContentLoaded或load事件)。只要可单击的元素呈现在页面上,就可以立即具备适当的工鞥。只添加一个事件处理程序所需的DOM引用更少,占用的内存空间更少,所花的时间也更少。

6.2 模拟事件

createEvent可以创建事件,通过dispatchEvent触发事件。
UIEvent 鼠标事件和键盘事件, MouseEvents 鼠标事件, MutationEvents DOM变动事件,HTMLEvents HTML事件。

1
2
3
4
5
6
7
8
9
10
11
// 触发按钮点击事件
var btn = document.getElementById("myBtn");

// 创建事件对象
var event = document.createEvent("MouseEvents");

// 初始化事件对象
event.initMouseEvent("click", true, true, document.defaultView, 0,0,0,0,0,false,false,false,false,0,null);

// 触发事件
btn.dispatchEvent(event);

参考资料

  • 《Javascript高级程序设计》第13章 事件