【重温基础】20.事件

32次阅读

共计 11797 个字符,预计需要花费 30 分钟才能阅读完成。

本文是 重温基础 系列文章的第二十篇。
这是第三个基础系列的第一篇,欢迎持续关注呀!重温基础 系列的【初级】和【中级】的文章,已经统一整理到我的【Cute-JavaScript】的 JavaScript 基础系列中。
今日感受:电影有时候看的是缘分。
系列目录:

【复习资料】ES6/ES7/ES8/ES9 资料整理 (个人整理)
【重温基础】1-14 篇
【重温基础】15.JS 对象介绍
【重温基础】16.JSON 对象介绍
【重温基础】18. 相等性判断
【重温基础】19. 闭包

本章节复习的是 JS 中的事件,事件冒泡捕获代理模拟等等。
前置知识:JavaScript 与 HTML 的交互式通过事件来实现的,是文档或浏览器窗口中发生的一些特定的交互瞬间。
1. 事件流
事件流描述的是从页面中接收事件的顺序,通常有这样两种完全相反的事件流概念:事件冒泡流(IE 团队提出)和事件捕获流(网景团队提出)。
1.1 事件冒泡
冒泡事件(Event Bubbling):事件开始时由最具体的元素接收(文档中嵌套层次最深的那个节点),然后逐层向上传播到较为不具体的节点(文档),看下示例代码:
<!DOCTYPE html>
<html>
<head>
<title>leo 事件冒泡 </title>
</head>
<body>
<div id=”leo”> 点击 </div>
</body>
</html>
点击页面中 <div> 元素,这个 click 事件就会按照下面顺序传播:

<div>
<body>
<html>
document

由此可见,元素绑定的事件会通过 DOM 树向上传播,每层节点都会发生,直到 document 对象,如图展示了冒泡过程:
1.2 事件捕获
事件捕获(Event Capturing):让不太具体的节点更早接收事件,而最具体的节点最后接收事件,即在事件到达预定目标之前捕获到,看下示例代码(HTML 代码和前面一样),事件捕获的过程是这样的:

document
<html>
<body>
<div>

看得出,document 对象最新接收事件,然后沿 DOM 树依次向下,直到最后的实际目标 <div> 元素,如图展示了捕获过程:

注意:由于老版本的浏览器不支持,因此很少人使用事件捕获,不过如果特殊需求还是可以使用事件捕获,建议还是使用事件冒泡。
1.3 DOM 事件流
“DOM2 级事件”规定的事件流包含三个阶段:事件捕获阶段,处于目标阶段和事件冒泡阶段。事件捕获为截获事件提供机会,然后实际的目标接收到事件,最后事件冒泡,对事件作出响应。按照前面的 HTML 代码,整个流程是这样的:

在 DOM 事件流中,实际目标(<div> 元素)在捕获阶段不接收事件,即在捕获阶段,事件从 document 对象到 <html> 再到 <body> 后就停止,进入“处于目标”阶段,事件在 <div> 元素上发生,然后才进入冒泡阶段,将事件传回给文档。
注意:目前主流浏览器都支持 DOM 事件流,只有 IE8 和之前版本不支持。
2. 事件处理
事件处理,即响应某个事件。我们把事件处理的函数,称为“事件处理程序”。事件处理程序的名称一般都以 on 开头,如 click 事件的事件处理程序就是 onclick,load 事件的事件处理程序就是 onload。我们将事件处理程序,分为这么几类:

HTML 事件处理程序
DOM0 级事件处理程序
DOM2 级事件处理程序
IE 事件处理程序
跨浏览器事件处理程序

2.1 HTML 事件处理程序
某个元素支持的事件,都可以用一个与相应事件处理程序同名的 HTML 特性来指定,这个特性的值应该是能够执行的 JavaScript 代码。比如:
<input type=”button” value=” 点击 ” onclick=”alert(‘hello leo’);”>
也可以把需要执行的具体事件单独定义出来,可以放置与单独.js 文件,也可以在文档内用 <script> 标签引入:
function fun(){
alert(‘hello leo’);
}
<input type=”button” value=” 点击 ” onclick=”fun()”>
我们通过这样指定事件处理程序,可以有一个局部变量 event 来获取事件对象本身,在这个函数内部,this 值等于这个变量 event。
<input type=”button” value=” 点击 ” onclick=”fun(event)”>
另外,HTML 中指定事件处理程序,会有 2 个缺点:
存在时间差
可能出现这样的情况:HTML 元素触发事件,但是事件处理程序还未定义(函数的定义在 HTML 最底下定义),就会出现报错,这与 HTML 代码加载顺序有关。
作用域链的异常
由于不同浏览器 JavaScript 引擎遵循的标识符解析规则存在差异,导致访问非限定对象成员时出错,表现为事件处理程序的作用域链在不同浏览器结果不同。
HTML 和 JavaScript 代码紧密耦合
这常常就是很多开发人员放弃 HTML 事件处理程序的原因。
2.2 DOM0 级事件处理程序
通过赋值形式,将一个函数赋值给一个事件处理程序属性。每个元素(包含 window 和 document)都有自己的事件处理属性,这些属性通常全部小写,如 onclick,将这种属性的值设置成一个函数,就可以指定事件处理程序:
var leo = document.getElementById(‘leo’);
leo.onclick = function(){
alert(‘hello leo!’);
}
使用 DOM0 级方法指定事件处理程序,被认为是元素的方法。此时的事件处理程序是在元素的作用域执行,那么,this 就引用当前元素,可以访问元素的任何属性和方法:
var leo = document.getElementById(‘leo’);
leo.onclick = function(){
alert(this.id); // “leo”
}
我们也可以通过设置事件处理程序属性来删除 DOM0 级的事件处理程序。
leo.onclick = null;
2.3 DOM2 级事件处理程序
有 2 个方法:

添加事件处理程序:addEventListener()

删除事件处理程序:removeEventListener()

所有的 DOM 节点都包含这两个方法,并且它们都接收三个参数:

处理的事件名称
事件处理程序的函数
布尔值(true:事件捕获阶段调用,false:事件冒泡阶段调用)

var leo = document.getElementById(‘leo’);
leo.addEventListener(‘click’,function(){
alert(this.id); // “leo”
},false);
与 DOM0 级方法一样,这里的事件处理程序也会是在元素的作用域中执行,因此 this 也是指向元素,可以访问元素的任何属性和方法。
使用 DOM2 级方法,可以添加多个事件处理程序,并按照添加顺序触发:
var leo = document.getElementById(‘leo’);
leo.addEventListener(‘click’,function(){
alert(this.id); // “leo”
},false);
leo.addEventListener(‘click’,function(){
alert(‘hello leo!’); // “hello leo!”
},false);
注意:通过 addEventListener() 添加的事件只能通过 removeEventListener() 移除,并且两者传入的参数一致,这就意味着通过 addEventListener() 添加的匿名函数不能被移除,看下面代码:
var leo = document.getElementById(‘leo’);
leo.addEventListener(‘click’,function(){
alert(this.id); // “leo”
},false);

// 没有效果
leo.removeEventListener(‘click’,function(){
// do some thing
},false);
没有效果是因为这两个方法传入的函数,是完全不同的,为了达到删除事件处理程序的效果,我们可以将处理函数单独定义出来:
var leo = document.getElementById(‘leo’);
var fun = function(){
alert(this.id);
}
leo.addEventListener(‘click’,fun,false);
// 有效果
leo.removeEventListener(‘click’,fun,false);
2.4 IE 事件处理程序
IE 实现两种方法:attachEvent() 和 detachEvent()。这两个方法接收相同的两个参数:事件处理程序名称和事件处理程序函数。由于 IE8 和更早版本只支持事件冒泡,所以通过 attachEvent() 添加的事件处理程序会被添加到冒泡阶段。
var leo = document.getElementById(‘leo’);
leo.attachEvent(‘onclick’,function(){
alert(‘hello leo’); // “leo”
},false);
// attachEvent 也支持添加多个事件处理程序

leo.attachEvent(‘onclick’,function(){
alert(‘hello world’); // “leo”
},false);
注意:这里的第一个参数是 onclick 而不是 DOM 的 addEventListener() 的 click。
IE 的 attachEvent() 和 DOM0 级方法区别:两者事件处理程序的作用域不同。

DOM0 级方法,作用域在所属元素的作用域。
attachEvent(),作用域在全局作用域,即 this 指向 window。

和 DOM0 级方法一样,detachEvent() 只能移除使用 attachEvent() 添加的方法,为了避免无法移除,也是需要将处理的函数单独定义出来:
var leo = document.getElementById(‘leo’);
var fun = function(){
alert(this.id);
}
leo.attachEvent(‘onclick’,fun,false);
// 有效果
leo.detachEvent(‘onclick’,fun,false);
2.6 跨浏览器事件处理程序
在做跨浏览器事件处理程序,我们可以有两种方式:

使用能够隔离浏览器差异的 JavaScript 库
单独手写一个处理方法

我们单独受写一个处理方法也不难,只需关注好事件冒泡阶段,我们可以创建一个方法,区分使用 DOM0 级方法、DOM2 级方法或 IE 方法即可,默认采用 DOM0 级方法。
var my_event = {
addMyEvent: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;
}
};
removeMyEvent: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;
}
}
}

3. 事件对象
当触发一个 DOM 上的事件时,都会产生一个事件对象 event,并作为参数传入事件处理程序,这个对象包含所有与事件相关的信息。包括导致事件的元素、事件类型等其他信息。
3.1 DOM 中的事件对象
无论指定事件处理程序时使用什么方法(DOM0 级方法或 DOM2 级方法),都会传入 event 对象:
var leo = document.getElementById(‘leo’);
leo.onclick = function(event){
alert(event.type); // ‘click’
}
leo.addEventListener(‘click’,function(event){
alert(event.type); // ‘click’
},false);
event 对象包含与创建它的特定事件相关的属性和方法,常常有如下成员:

我们可以使用 event 中的对象和方法:
阻止事件的默认行为
var leo = document.getElementById(‘leo’);
leo.onclick = function(event){
// 只有当 event 中的 cancelable 属性为 true 的事件
event.preventDefault();
}
立即停止事件在 DOM 的传播
通过调用 event.stopPropagation() 方法避免弹框出现两次。
var leo = document.getElementById(‘leo’);
leo.onclick = function(event){
alert(‘leo’);
event.stopPropagation();
}
document.body.onclick = function(event){
alert(‘hello leo’);
}
3.2 IE 中的事件对象
访问 IE 中的事件对象 event,方法有多种,取决于事件处理程序的方法:
DOM0 级方法,使用 window.event

var leo = document.getElementById(‘leo’);
leo.onclick = function(){
var event = window.event;
alert(event.type); // ‘click’
}
IE 的 attachEvent 方法,event 作为参数传入(也可以使用 window.event)
var leo = document.getElementById(‘leo’);
leo.attachEvent(‘onclick’,function(event){
alert(event.type); // ‘click’
},false);
3.3 跨浏览器的事件对象
虽然 DOM 和 IE 中 event 对象不同,但我们也可以和前面的 跨浏览器事件处理程序 处理一样,通过他们之间的区别,实现映射:
var my_event = {
myAddFun : function(element, type, handler){
// do some thing
},
// 返回对 event 的引用
getMyEvent : function(event){
return event?event:window.event;
},
// 返回事件的目标
getMyTarget : function(event){
return event.target || event.srcElement;
},
// 取消事件的默认行为
preventDefault : function(event){
if(event.preventDefault){
event.preventDefault();
}else {
event.returnValue = false;
}
},

myRemoveFun : function(element, type, handler){
// do some thing
},

// 阻止事件流
stopPropagetion : function(event){
if(event.stopPropagetion){
event.stopPropagetion();
}else {
event.cancelBubble = true;
}
}
}

var leo = document.getElementById(‘leo’);
leo.onclick = function(event){
alert(‘leo’);
event = my_event(event);
my_event.stopPropagation(event);
}

leo.onclick = function(event){
alert(‘hello world’);
}
4. 事件类型
Web 浏览器中的事件类型有很多,DOM3 级事件规定有以下几类事件类型:

UI 事件:当用户与页面上元素交互时触发;

焦点事件:当元素失去或获取焦点时触发;

鼠标事件:当用户通过鼠标在页面操作时触发;

滚轮事件:当使用鼠标滚轮(或类似设备)时触发;

文本事件:当在文档中输入文本时触发;

键盘事件:当用户通过键盘操作时触发;

合成事件:当为 IME 输入字符时触发;

变动事件:当底层 DOM 结构变动时触发;

具体每个方法的详细介绍,可以查看 W3school HTML DOM Event 对象 或者查看《JavaScript 高级程序设计(第三版)》的第 362 页开始。我后面会单独整理一篇,介绍这些事件的文章。
5. 事件委托
简单理解就是讲一个响应事件委托到另一个元素。事件委托利用事件冒泡,指定一个事件处理程序来管理某一类型的所有事件,比如我们通过一个函数来代替其他三个函数:
<ul id=”btn”>
<li id=”btn1″> 按钮 1 </li>
<li id=”btn2″> 按钮 2 </li>
<li id=”btn3″> 按钮 3 </li>
</ul>
var btn1 = document.getElementById(‘btn1’);
var btn2 = document.getElementById(‘btn2’);
var btn3 = document.getElementById(‘btn3’);
// my_event 在前面定义了
my_event.myAddFun(btn1, ‘click’, function(event){
alert(‘btn1 点击 ’);
});
my_event.myAddFun(btn2, ‘click’, function(event){
alert(‘btn2 点击 ’);
});
my_event.myAddFun(btn3, ‘click’, function(event){
alert(‘btn3 点击 ’);
});
下面我们在 DOM 树层级更高的元素上添加一个事件处理函数,来做事件委托,我们这么重写这段代码:
var btn = document.getElementById(‘btn’);
my_event.myAddFun(btn, ‘click’, function(event){
event = my_event.getMyEvent(event);
var target = my_event.getMyTarget(event);
switch(target.id){
case “btn1”:
alert(‘btn1 点击 ’);
break;
case “btn2”:
alert(‘btn2 点击 ’);
break;
case “btn3″:
alert(‘btn3 点击 ’);
break;
}
});
最适合采用事件委托技术的事件包括:click/mousedown/mouseup/keyup/keydown/keypress,虽然 mouseover 和 mouseout 事件也有冒泡,但因为不好处理它们并且经常需要重新计算元素位置。
可以看出,事件委托有以下优点:

减少内存消耗
动态绑定事件

6. 事件模拟
JavaScript 的事件模拟主要用来在任意时刻触发特定事件。
6.1 DOM 中的事件模拟
在 document 对象上使用 createEvent() 方法创建一个 event 对象。createEvent() 接收一个参数,即要创建的事件类型的字符串。DOM2 级中,所有这些字符串都使用英文复数形式,DOM3 级中都变成单数,也可以是下面中的字符串:

UIEvents : 一般化的 UI 事件(鼠标和键盘事件都继承自 UI 事件)(DOM3 级中 UIEvent)
MouseEvents : 一般化的鼠标事件(DOM3 级中 MouseEvent)
MutationEvents : 一般化的 DOM 滚动事件(DOM3 级中 MutationEvent)
HTMLEvents : 一般化的 HTML 事件(DOM3 级中 HTMLEvent)

创建 event 之后,我们需要使用 dispatchEvent() 方法去触发这个事件,需要传入一个参数,参数是需要触发事件的 event 对象。
所有支持事件的 DOM 节点都支持这个方法。事件触发之后,事件就能照样冒泡并引发响应事件处理程序的执行。
6.1.1 模拟鼠标事件
使用 createEvent() 方法传入 MouseEvents 创建一个鼠标事件,返回的对象有一个 initMouseEvent() 方法,用于指定与该鼠标事件相关的信息,有 15 个参数:

type:字符串,表示触发的事件类型,如 click

bubble : 布尔值,表示是否冒泡,为了精确模拟鼠标事件,通常设置为 true
cancelable:布尔值,表示是否可以取消,为了精确模拟鼠标事件,通常设置为 true
view : 与事件关联的视图,基本都设置为 document.defaultView

detail : 整数,与事件有关的详细信息,基本设置为 0
screenX : 整数,事件相对屏幕的 X 坐标
screenY : 整数,事件相对屏幕的 Y 坐标
clientX : 整数,事件相对视口的 X 坐标
clientY : 整数,事件相对视口的 Y 坐标
ctrlKey : 布尔值,表示是否按下 Ctrl 键,默认 false
altKey : 布尔值,表示是否按下 Alt 键,默认 false
shiftKey : 布尔值,表示是否按下 Shift 键,默认 false
metaKey : 布尔值,表示是否按下 Meta 键,默认 false
button : 整数,表示按下哪个鼠标键,默认 0
relatedTarget : 对象,表示与事件相关的对象,只在 mouseover 和 mouseout 时使用

案例:
var btn = document.getElementById(‘btn’);
var myEvent = document.createEvent(‘MouseEvents’);
myEvent.initMouseEvent(
‘click’, true, true, document.defaultView,
0, 0, 0, 0, 0,
false, false, false, false, 0, null
)
btn.dispatchEvent(myEvent);
6.1.2 模拟键盘事件
DOM3 级规定,使用 createEvent() 方法传入 KeyboardEvent 创建一个键盘事件,返回的对象有一个 initKeyEvent() 方法,有 8 个参数:

type:字符串,表示触发的事件类型,如 keydown

bubble : 布尔值,表示是否冒泡,为了精确模拟键盘事件,通常设置为 true
cancelable:布尔值,表示是否可以取消,为了精确模拟键盘事件,通常设置为 true
view : 与事件关联的视图,基本都设置为 document.defaultView

key : 整数,表示按下的键的键码
localtion : 整数,表示按下哪里的键,默认 0 表示主键盘,1 表示左,2 表示右,3 表示数字键盘,4 表示移动设备(即虚拟键盘),5 表示手柄
modifiers : 字符串,空格分隔的修改件列表,如 ”shift”
repeat : 整数,在一行中按了多少次这个键

由于 DOM3 级不提倡使用 keypress 事件,因此只能用这个方式来模拟 keyup/keydown 事件。
模拟按住 Shift 和 A 键的案例:
var btn = document.getElementById(‘btn’);
var myEvent;

// 以 DOM3 级方式创建
if(document.implementation.hasFeature(‘KeyboardEvents’, ‘3.0’)){
myEvent = document.createEvent(‘KeyboardEvent’);
myEvent.initKeyboardEvent(
‘keydown’, true, true, document.defaultView,
‘a’, 0, ‘Shift’, 0
);
}
btn.dispatchEvent(myEvent);
6.1.3 模拟其他事件
如模拟变动事件和 HTML 事件。
模拟变动事件

通过 createEvent() 传入 MutationEvents 参数创建,返回一个 initMutationEvent() 方法,这个方法接收参数包括:type/bubbles/cancelable/relatedNode/preValue/newValue/attrName/attrChange,下面模拟一个案例:
var btn = document.getElementById(‘btn’);
var myEvent = document.createEvent(‘MutationEvents’);
myEvent.initMutationEvent(
‘DOMNodeInserted’, true, false, someNode, ”, ”, ”, 0
);
btn.dispatchEvent(myEvent);
模拟 HTML 事件

通过 createEvent() 传入 HTMLEvents 参数创建,返回一个 initEvent() 方法,下面模拟一个案例:
var btn = document.getElementById(‘btn’);
var myEvent = document.createEvent(‘HTMLEvents’);
myEvent.initEvent(‘focus’, true, false);
btn.dispatchEvent(myEvent);
6.1.4 自定义 DOM 事件
通过 createEvent() 传入 CustomEvent 参数创建,返回一个 initCustomEvent() 方法,有 4 个参数:

type:字符串,表示触发的事件类型,如 keydown

bubble : 布尔值,表示是否冒泡,为了精确模拟键盘事件,通常设置为 true
cancelable:布尔值,表示是否可以取消,为了精确模拟键盘事件,通常设置为 true
detail : 对象,任意值,保存在 event 对象的 detail 属性中

案例:
var btn = document.getElementById(‘btn’);
var myEvent;

// my_event 在前面定义 2.6 跨浏览器事件处理程序
my_event.addMyEvent(btn, ‘myevent’, function(event){
alert(‘btn detail ‘, event.detail);
});

my_event.addMyEvent(document, ‘myevent’, function(event){
alert(‘document detail ‘, event.detail);
});

// 以 DOM3 级方式穿件
if(document.implementation.hasFeature(‘CustomEvents’, ‘3.0’)){
myEvent = document.createEvent(‘CustomEvent’);
myEvent.initCustomEvent(
‘myevent’, true, false, ‘hello leo’,
);
btn.dispatchEvent(myEvent);
}
6.2 IE 中的事件模拟
IE8 及之前的版本模拟事件和 DOM 中模拟思路相似:想创建 event 对象再指定信息,最后触发。区别在于,IE 中使用 document.createEventObject() 方法创建 event 对象,并且不接收参数,返回一个通用 event 对象,我们要做的就是给这个 event 对象添加信息,最后在目标上调用 fireEvent() 方法,并接受两个参数(事件处理程序的名称和 event 对象)。在调用 fireEvent() 方法会自动添加 srcElement 和 type 属性,我们需要手动添加其他属性,下面模拟一个 click 事件:
var btn = document.getElementById(‘btn’);
var myEvent = document.createEventObject();

myEvent.screenX = 100;
myEvent.screenY = 0;
myEvent.clientX = 100;
myEvent.clientY = 0;
myEvent.ctrlKey = false;
myEvent.altKey = false;
myEvent.shiftKey = false;
myEvent.button = 0;

btn.fireEvent(‘onclick’, event);
参考文章
《JavaScript 高级程序设计》第 13 章 事件
本部分内容到这结束

Author
王平安

E-mail
pingan8787@qq.com

博 客
www.pingan8787.com

微 信
pingan8787

每日文章推荐
https://github.com/pingan8787…

JS 小册
js.pingan8787.com

正文完
 0