导语:在js中,具有交互性程序的是事件驱动。它可以很好的和用户进行互动,增强网页的趣味性和互动效果,提高更好的用户体验。今天我分享一篇有关事件驱动的文章,讲诉事件的来龙去脉,文章有不妥之处,还请电邮反馈。

事件的起源

这节开始说事件的起源,也就是原始事件模型。这是最初始的事件处理模式,通常把它当作0级DOM的一部分,所有游览器都支持。

事件以及事件类型

事件就是当用户在用鼠标或者键盘对网页进行操作时所,触发了的网页调用某个方法,反馈给用户的结果。不同的事件类型,生成的事件也不一样,反馈效果也不一样。

在原始事件模型中,事件不能被js直接操作,是游览器从内部提取的。这些事件类型都是响应事件调用时的事件句柄名称。HTML属性就可以用来处理这些事件代码。

下面是收集总结的事件句柄列表:

序号 事件句柄 触发条件 支持元素
1 onload 文档加载完毕 <body>
2 unonload 文档卸载完毕 <body>
3 onresize 调整窗口大小 <body>
4 onabort 图像加载被中断 <img>
5 onerror 图像加载发生错误 <img>
6 onclick 鼠标按下被释放 大多数元素
7 ondbclick 双击鼠标 大多数元素
8 onmousedown 鼠标键被按下 大多数元素
9 onmousemove 鼠标移动 大多数元素
10 onmouseup 释放鼠标键 大多数元素
11 onmouseover 鼠标移到元素上 大多数元素
12 onmouseout 鼠标离开元素 大多数元素
13 onkeydown 键盘被按下 表单元素、<body>
14 onkeyup 键盘被按下后释放 表单元素、<body>
15 onkeypress 键盘按下被释放,返回false取消默认 表单元素、<body>
16 onblur 元素失去焦点 <body><input><textarea><button><select>
17 onfocus 元素得到焦点 <body><input><textarea><button><select>
18 onchange 得到焦点使值发生了改变 <input><textarea><select>
19 onselect 选中表单文本 <input><select><textarea>
20 onreset 表单重置,返回false取消 <form>
21 onsubmit 表单提交,返回false取消 <form>

以上是一些定义的事件列表,仔细点,你会发现列表中可以大致的分为鼠标事件、键盘事件、表单事件以及其他事件。后面会详细介绍。

html属性的事件

之前说过,事件是用户在对html标签进行触发时调用的js方法,用来执行一些相关的任务。

所以在html中,以上列表的事件可以被当作html的属性来使用。这里部分大小写,但是我还是习惯小写。

【例如】:给一个按钮加上事件,弹出内容。

1
<button onclick="alert('你好啊,事件!');">点我</button>

js属性的事件

如果不想再html属性值里面写js的方法,字符串,可以在js中写这样一个方法,然后在html属性中进行调用,同样也可以的。

【例如】:给一个输入框加上提示。

1
<input type="text" name="username" onchange="tips(event);">
1
2
3
4
5
6
7
function tips(event) {
var val = event.target.value;
var nameReg = /^[\u4e00-\u9fa5]{0,}$/;
if (!nameReg.test(val)) {
alert('请输入中文!');
}
}

还可以这样写。

1
<input type="text" name="username">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var username = document.getElementsByName('username')[0];
username.onchange = tips;
function tips(event) {
var val = event.target.value;
var nameReg = /^[\u4e00-\u9fa5]{0,}$/;
if (!nameReg.test(val)) {
alert('请输入中文!');
}
}
// 或者这样
username.onchange = function (event) {
var val = event.target.value;
var nameReg = /^[\u4e00-\u9fa5]{0,}$/;
if (!nameReg.test(val)) {
alert('请输入中文!');
}
}

js事件的返回值

大多数情况下,事件都有返回值,如果是true就执行,不是就取消。

比如说:onclick事件,如果返回true就执行,否则不执行。

【例如】:提交表单不执行默认提交方法。

1
2
3
4
<form name="login" onsubmit="return checkForm();">
<input type="text" name="username">
<input type="submit" value="提交">
</form>
1
2
3
function checkForm() {  
return false;
}

其实还有表单重置事件也是这样,你可以私下里去练习一下。

this关键词

js中的this是一个很神奇的存在,一般情况下,它都指向的是Window顶层全局对象,也包括函数调用。

【例如】:你打印一下这个this,你会发现结果是Window。

1
2
3
4
5
console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
function hello() {
console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
}
hello();

但是也有其他情况,比如说对象方法调用,事件调用。这种情况下,就是谁调用,指向谁。对象里面的方法调用会指向对象本身。

【例如】:

1.对象方法调用

1
2
3
4
5
6
7
8
9
10
//对象方法调用
var obj = {
name: 'mark',
sayName: function () {
console.log(this); // {name: "mark", sayName: ƒ}
console.log(this.name); // mark
return this.name;
}
}
console.log(obj.sayName()); // mark

注意:如果把对象中的方法赋予另一个变量,由于这个变量是Window下的变量、属性,所以再次打印this会指向Window对象。

1
2
3
4
5
6
7
8
9
//对象方法调用
var obj = {
name: 'mark',
sayName: function () {
console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
}
}
var fn = obj.sayName;
console.log(fn());

2.事件调用

1
<button id="tip">点我</button>
1
2
3
4
5
6
//事件方法调用
var tip = document.getElementById('tip');
tip.onclick = function(event) {
console.log(this); // <button id="tip">点我</button>
console.log(this.id); // tip
}

作用域

在js中,有变量作用域,有函数作用域,但是所有作用域的顶层都是全局对象。

在事件句柄中,也存在作用域,但是这个作用域只是适用于html属性的事件句柄。

例如:在一个button按钮中调用了名为form的变量,那么就会解析成form属性。

1
2
3
4
<form>
<input type="text" name="username" value="hello">
<button onclick="alert(this.form.username.value);">点我</button>
</form>

在此案例中,当点我按钮点击后会把form解析成这个表单属性,从而获取到旁边输入框的值hello。

事件的2级DOM标准

在经历了原始事件模型后,DOM又进行了扩展,定义了一些高级的事件处理方法。这些方法除了ie游览器外其他的游览器都支持。ie有自己的处理方法,这些方法会在第三节进行补充。

事件传播

在0级DOM事件模型中,是游览器负责把事件分配到发生事件的元素,如果那个元素有合适的事件类型就会触发,不会再执行其他的操作。

但是2级DOM标准中规定的是只要事件发生在文档元素(目标元素)上,就会触发。它的上级元素也有机会触发该事件。

此时就有了事件传播,它主要是分为3个阶段进行。

  • 事件传播的捕捉阶段(capturing)

这个阶段事件从Document对象沿着DOM树向下传播给那个目标节点。

这个阶段是从上到下进行传播,如果在途中有目标节点的祖先注册了捕捉事件句柄,就会先执行这个事件。

  • 事件传播的第二个阶段(self)

这个阶段已经到达了目标元素本身,直接注册在元素本身的事件就会执行。

  • 事件传播的起泡阶段(bubbling)

这个阶段,目标元素已经执行完自身的事件句柄了,所以这个阶段的事件就会沿着DOM树向上回到Document对象。如果在途中有目标节点的祖先有合适的事件句柄,就会先执行这个事件。

但是并非所有的事件类型都起泡,像表单这种form元素执行完自身事件后再往Document对象走没有意义。

一般来说,原始事件起泡,而高级语义的事件不起泡。

下面一张表就列出了哪些适用于哪个阶段的事件传播。

序号|事件类型|接口|B(起泡)|C(捕捉)|支持元素/属性
1|abort|Event|yes|no|<img>
2|blur|Event|no|no|<a><area><button><input><select><textarea>
3|change|Event|yes|no|<input><select><textarea>
4|click|MouseEvent|yes|yes|screenX,screenY,clientX,clientY,altKey,ctrlKey
5|error|Event|yes|no|<body>,<frameset><img>
6|focus|Event|no|no|<a><area><button><input><select><textarea>
7|load|Event|no|no|<body><frameset><iframe><img>
8|mousedown|MouseEvent|yes|yes|screenX,screenY,clientX,clientY,altKey,ctrlKey
9|mousemove|MouseEvent|yes|no|screenX,screenY,clientX,clientY,altKey,ctrlKey
10|mouseout|MouseEvent|yes|yes|screenX,screenY,clientX,clientY,altKey,ctrlKey
11|mouseover|MouseEvent|yes|yes|screenX,screenY,clientX,clientY,altKey,ctrlKey
12|mouseup|MouseEvent|yes|yes|screenX,screenY,clientX,clientY,altKey,ctrlKey
13|reset|Event|yes|no|<form>
14|resize|Event|yes|no|<body><iframe><frameset>
15|scroll|Event|yes|no|<body>
16|select|Event|yes|no|<input><textarea>
17|submit|Event|yes|yes|<form>
18|unload|Event|no|no|<body><frameset>
19|DOMActivate|UIEvent|yes|yes|detail
20|DOMFocusIn|UIEvent|yes|no|none
21|DOMFocusOut|UIEvent|yes|no|none

温馨提示:

在事件传播的过程中,任何事件句柄都可以使用表示事件的Event对象提供的stopPropagation()方法来停止事件的传播。

也有一些事件会引起游览器对元素的默认动作,比如a标签,默认动作是超链接跳转。在事件传播阶段结束后,便会进行默认动作。可以使用Event对象提供的preventDefault()方法来阻止默认动作的发生。

在0级DOM模型中,只能为特定对象的特定类型的事件注册一个事件句柄,但是在2级模型中,就可以为特定对象的特定类型的事件注册多个事件句柄。

事件句柄注册

在0级DOM事件中,通过html属性或者js来为元素注册事件;但在2级DOM事件中,可以调用addEventListener()方法来为某个元素注册事件,该方法有3个参数。

  • 第一个参数是事件类型,不加前缀on,比如:onmouseover要写成mouseover;
  • 第二个参数是句柄函数,也就是事件发生时调用的函数;这个函数只接受唯一的Event对象;
  • 第三个参数是布尔值,值为true,那就用于捕捉事件,否则就是直接发生在元素本身的事件;

这个注册方法只对注册的元素有用,是独立的,可以为一个元素注册多个事件而互相不受影响的。

【例如】:

  • 为按钮注册一个点击事件。
1
<button id="clickme">点我</button>
1
2
3
4
5
var clickme = document.getElementById('clickme');
clickme.addEventListener('click',clickMe,false);
function clickMe(e) {
console.log(this.id); // clickme
}
  • 监听一个元素中发生的所有鼠标移入事件
1
<button id="overme">点我</button>
1
2
3
4
5
var overme = document.getElementById('overme');
overme.addEventListener('mouseover',handlerMouse,true);
function handlerMouse(e) {
console.log(this.id); // clickme
}

既然有注册,也就有注销事件,与之对应的是方法是removeEventListener(),它和注册方法接受的参数一样,不过是从元素上面移除了这个事件。

1
2
3
4
5
var overme = document.getElementById('overme');
overme.removeEventListener('mouseover',handlerMouse,true);
function handlerMouse(e) {
console.log(this.id); // clickme
}

小提示:这个注册方法中第二个参数的函数内部的this是指向注册事件的元素。

事件模块

在2级DOM中,事件是模块化的,所以你可以使用以下方法来测试游览器是否支持2级DOM事件模块。

下面是我封装的一个方法,只需要传入两个参数就可以查询是否支持。

1
2
3
4
5
6
7
8
9
10
11
12
function searchDomSupport(name,version) {
if (document.implementation &&
document.implementation.hasFeature &&
document.implementation.hasFeature(name,version)
) {
return true;
} else {
return false;
}
}
var res = searchDomSupport('Event','2.0');
console.log(res); // true;

Event接口和Event

2级DOM API提供了事件发生时事件的一些额外信息,包括事件发生的时间,类型,元素的属性等等。事件是模块化的,所以一个模块就有一个相关的事件接口,声明了该事件类型的详细信息。

Event接口就是这个事件接口,它包括以下几部分:

序号|模块名|事件接口|事件类型
1|HTMLEvents|Eevent|abort,blur,change,load,resize,scroll,select
2|MouseEvents|MouseEvent|click,mousedown,mouseup,mouseover,mouseout
3|UIEvents|UIEvent|DOMActivate,DOMFocusIn,DOMFocusOut

Event的属性

  • type,发生事件的类型,和注册事件名称一样,比如:click,mouseover;
  • target,发生事件的节点信息;
  • currentTarget,发生当前事件的节点,在事件传播过程中和target的值不一样;
  • eventPhase,一个数字表明当前所处的传播阶段,是一个常量;
  • timeStamp,一个Date()对象,表面事件发生的时间;
  • bubbles,一个布尔值,声明该事件是否在文档中起泡;
  • cancelable,一个布尔值,声明该事件是否能用preventDefault()方法;

【例如】:获取按钮的点击事件的属性。

1
<button id="tips">点我</button>
1
2
3
4
5
6
7
8
9
10
11
var tips = document.getElementById('tips');
tips.onclick = function (e) {
var e = e || window.event;
console.log(e.type); // click
console.log(e.target); // target: button#tips
console.log(e.currentTarget); // null
console.log(e.eventPhase); // 2
console.log(e.timeStamp); // 3006.099999998696
console.log(e.bubbles); // true
console.log(e.cancelable); // true
}

Event的方法

  • stopPropagation(),阻止事件从当前正在处理它的节点传播;
  • preventDefault(),阻止游览器执行节点的默认动作;

【例如】:

1
2
3
4
5
6
7
8
// 调用此函数
function clickMe(e) {
e.stopPropagation();
}
//超链接跳转禁用
function clickMe(e) {
e.preventDefault();
}

UIEvent的属性

  • view,发生事件的Window对象;
  • detail,对于鼠标点击事件来说,值为1就是点击1次,为2就是点击2次。如果值为2,说明之前还有一个点击为1的事件;

MouseEvent的属性

  • button,一个数字,声明在mousedown,mouseup,click事件中,哪个鼠标键改变了状态。0为左键,1为中间键,2为右键;
  • altKey、ctrlKey、metaKey、shiftKey,这四个值都是布尔值,声明在鼠标事件发生时,是否同时按住了这四个键盘键;alt,ctrl,shift,meta;
  • clientX、clientY,声明鼠标指针位于游览器窗口的X坐标和Y坐标;不考虑文档滚动,如果在顶部则clientY始终是0;在ie以外,要转换成文档坐标,而不是游览器窗口坐标;可以使用window.pageXOffset和window.pageYOffset来获取。
  • screenX、screenY,声明鼠标指针位于用户显示器的左上角的X坐标和Y坐标;
  • relatedTarget,引用事件的节点相关的节点。比如:对于mouseover,是鼠标移入目标节点离开时的那个节点;

【例如】:按钮的点击事件属性

1
<button id="tips">点我</button>
1
2
3
4
5
6
7
8
var tips = document.getElementById('tips');
tips.onclick = function (e) {
console.log(e.button); // 0
console.log(e.altKey,e.ctrlKey,e.metaKey,e.shiftKey); // false false false true
console.log(e.clientX,e.clientY); // 22 17
console.log(e.screenX,e.screenY); // 22 119
console.log(e.relatedTarget); // null
}

温馨提示:2级DOM事件模型是可以兼容0级DOM事件的一些属性和方法。这叫混合事件模型。

IE事件模型

本节介绍的是IE4、5、6支持的中间模型,它介于0级模型和2级DOM事件模型之间。包括以下几部分:

Event对象,和2级DOM事件模型的Event属性有些相似;

IE事件的传播、注册和内存泄漏

Event对象的属性

  • type,发生事件的类型,和2级DOM中的type属性兼容。比如:click,mouseover;
  • srcElement,发生事件的文档元素,和2级DOM中的target属性兼容;
  • button,声明鼠标被按下的鼠标键;值为1是左键,2为右键,4为中间键;
  • clientX、clientY,声明鼠标指针位于游览器窗口的X坐标和Y坐标;要转换成文档坐标,需要加文档滚动的量;
  • offsetX、offsetY,声明鼠标指针位于元素的位置;
  • altKey、ctrlKey、shiftKey,这四个值都是布尔值,声明在鼠标事件发生时,是否同时按住了这三个键盘键;
  • keyCode,声明在键盘事件(keydown,keyup,keypress)中的键盘码;可以用String.fromCharCode()方法把字符代码转换成字符串;
  • fromElement、toElement,fromElement声明mouseover事件移动过的文档元素,toElement声明mouseout事件移到的文档元素;
  • cancelBubble,布尔值为true时阻止当前事件进一步起泡到上一层次的元素;相当于2级DOM中的stopPropagation()方法;
  • returnValue,布尔值为false时可以阻止游览器执行默认动作,相当于2级DOM中的preventDefault()方法;

【例如】:在ie游览器中进行点击事件。

1
<button id="tips">点我</button>
1
2
3
4
5
6
7
8
9
10
11
var tips = document.getElementById('tips');
tips.onclick = function (e) {
console.log(e.type); // click
console.log(e.srcElement); // <button id="tips">点我</button>
console.log(e.button); // 0
console.log(e.clientX,e.clientY); // 30.399999618530273 14.399999618530273
console.log(e.offsetX,e.offsetY); // 29.399999618530273 11.800000190734863
console.log(e.altKey,e.ctrlKey,e.shiftKey); // false false false
console.log(e.cancelBubble); // false
console.log(e.returnValue); // undefined
}

温馨提示:IE中事件模型Event对象是作为全局的变量,在Window对象下的,所以要使用必须这样写window.event。

IE事件注册

ie4中是和0级模型一样的方法;ie5以及以后的版本是使用attachEvent()方法和detachEvent()方法,类似于2级DOM事件模型中的事件监听注册方法;但是不同的是:

IE事件模型不支持事件捕捉,所以这两个方法的参数只要两个;

  • 第一个参数的事件类型名称应该包括on这个前缀;例如:click要写成onclick;

attachEvent()方法注册的函数将作为全局函数调用,而不是事件的目标元素,this关键词指向Window;

attachEvent()方法允许注册多次,当事件发生时,注册函数的被调用次数和注册次数一样多;

事件起泡:IE事件模型中的事件起泡,想要阻止只能把cancelBubble这个属性设置为true,当新事件生成时,又会自动还原成false。

事件捕捉:IE事件模型中的事件捕捉,可以使用setCapture()releaseCapture()方法来实现,只是对鼠标事件适用。

内存泄漏:IE6之前的嵌套函数容易引起内存泄漏。

IE兼容性方法

下面是我根据原始事件模型、2级DOM事件模型和IE事件模型,封装的一个方法。

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
var uniHandler= {};
if (document.addEventListener) {
uniHandler.add = function (elem,eventType,handleEvent,isCapture) {
elem.addEventListener(eventType,handleEvent,isCapture);
}
uniHandler.remove = function (elem,eventType,handleEvent,isCapture) {
elem.removeEventListener(eventType,handleEvent,isCapture);
}
uniHandler.stoppro = function () {
e.stopPropagation();
};
uniHandler.prevent = function () {
e.preventDefault();
}
} else if (document.attachEvent) {
uniHandler.add = function (elem,eventType,handleEvent) {
elem.attachEvent('on'+eventType,handleEvent);
}
uniHandler.remove = function (elem,eventType,handleEvent) {
elem.detachEvent('on'+eventType,handleEvent);
}
uniHandler.stoppro = function () {
e.cancelBubble = true;
};
uniHandler.prevent = function () {
e.returnValue = false;
}
} else {
uniHandler.eventOrigin = function (elem,eventType,handleEvent) {
elem.eventType = handleEvent;
}
}

【例如】:在ie和非ie游览器中测试,均没有问题;ie包括ie5-11。

一个按钮注册点击事件

1
<button id="tips">点我</button>
1
2
3
4
var tips = document.getElementById('tips');
uniHandler.add(tips,'click',function () {
alert('你好!');
},false);

一个链接注册点击事件,并且阻止游览器默认动作

1
<a id="goto" href="#111" target="_blank">链接</a>
1
2
3
4
5
6
var goto = document.getElementById('goto');
uniHandler.add(goto,'click',function (e) {
var e = e || window.event;
uniHandler.prevent(e);
alert('我不走!');
},false);

写在最后

有关DOM中的事件今天就说到这里,还有不明白的可以到前面去仔细看看,多练练,你就会了。