DOM 事件是处理 Web 页面交互的基础,是掌握前端开发技术的基础。
W3C协会早在1988年就开始了DOM标准的制定,W3C DOM标准可以分为DOM1,DOM2,DOM3三个版本。
1.Html事件处理
原始事件模型,事件处理程序被设置为html控件的性质值,一般是html控件的 onclick、onerror、onload ....
如:
<button type="button" onclcik="console.log('Hello Console!')">ShowConsole</button>
<button type="button" onclick="showFn()">ShowFn</button>
<script>
function showFn() {
alert('Hello World');
}
</script>
优点:
简单快速,此时代码的作用域是文档全局,可以直接使用文档所有公共变量
缺点:
1、是HTML于JS 强耦合,绑定在一起
2、一个处理程序无法同时绑定多个处理函数
3、执行时机问题:可能会出现 html 上已经显示控件,但是关联的函数还未加载进来,此时用户点击,就会执行失败(你没办法告知用户要等一会儿...)
4、不同浏览器对此方式绑定的函数的作用域解释可能有差异,在部分情况下会导致不同的行为
2.DOM0级事件
就是先取得控件变量,然后给变量的事件处理程序赋予一个函数,解绑事件处理是将事件处理程序赋为 null
<input id="x1" type="text" value="World!">
<script>
var x1 = document.getElementById('x1');
x1.onclick = function() {
alert('Hello World'+this.value);
}
// x1.onclick = null; 解绑事件
</script>
特点:
- 此方式添加的代码是注册在冒泡阶段的
- 此方式添加的函数是元素的属性,作用域是元素范围,可以引用元素的属性(使用 this 代表元素本身)
3.DOM2级事件
DOM级别1于1998年10月1日成为W3C推荐标准。1级DOM标准中并没有定义事件相关的内容,所以没有所谓的1级DOM事件模型。
在2级DOM中除了定义了一些DOM相关的操作之外还定义了一个事件模型。当事件发生在节点时,目标元素的事件处理函数就被触发,而且目标的每个祖先节点也有机会处理那个事件。
DOM事件流包括两个阶段:
1.capturing 消息捕获阶段,事件从Document对象沿着文档树向下传播给节点。如果目标的任何一个祖先专门注册了事件监听函数,那么在事件传播的过程中就会运行这些函数。
2.下一个阶段发生在目标节点自身,直接注册在目标上的适合的事件监听函数将运行。
3.bubbling 消息冒泡阶段,这个阶段事件将从目标元素向上传播回Document对象(与capturing相反的阶段)。虽然所有事件都受capturing阶段的支配,但并不是所有类型的事件都bubbling。(0级DOM事件模型处理没有capturing阶段)
老版本的浏览器不支持消息捕获阶段的处理。一般都只需要处理消息冒泡阶段的事件。
如果想在事件发生前捕获,然后阻止默认动作发生,可以指定事件处理程序在捕获阶段运行。
所谓事件冒泡就是事件像泡泡一样从最开始生成的地方一层一层往上冒,一层一层向上直至最外层的html或document。
<div id="box">
<a id="child">事件冒泡</a>
</div>
<script>
var box = document.getElementById('box'),
child = document.getElementById('child');
child.addEventListener('click', function() {
alert('我是目标事件');
}, false);
box.addEventListener('click', function() {
alert('事件冒泡至DIV');
}, false);
</script>
上面的代码运行后我们点击a标签,首先会弹出’我是目标事件’提示,然后又会弹出’事件冒泡至DIV’的提示,这便说明了事件自内而外向上冒泡了。
DOM2级标准中,事件定义了addEventListener和removeEventListener两个方法,分别用来绑定和解绑事件。
方法中包含3个参数,分别是:
(1)绑定的事件处理属性名称(不包含on)
(2)处理函数
(3)是否在捕获时执行,省略此参数时,默认为false,表示仅在冒泡阶段处理
DOM2级事件允许给一个处理程序添加多个处理函数。代码如下:
<button id="btn" type="button"></button>
<script>
var btn = document.getElementById('btn');
function showFn() {
alert('Hello World');
}
btn.addEventListener('click', showFn, false);
btn.addEventListener('mouseover', showFn, false); // 添加一个鼠标移入的方法
// btn.removeEventListener('click', showFn, false); 解绑事件
</script>
如果是 IE8 以下需要用attachEvent和detachEvent来实现
btn.attachEvent('onclick', showFn); // 绑定事件
btn.detachEvent('onclick', showFn); // 解绑事件
\\ 这里我们不需要传入第三个参数,因为IE8级以下版本只支持冒泡型事件。
4.事件对象
DOM当中发生事件时,所有信息都会被收集并存储到一个event对象当中
可以在调试台查看 event 对象的结构
let btn = document.getElementById('mybtn');
btn.onclick = function(event){
console.log(event);
}
btn.addEventListener("click",(event)=>{
console.log(event);
// event.stopPropagation();
// 加上上面这一句,则按钮以上层次的就监听不到 click 事件了,事件冒泡被终止
});
document.body.onclick = (event)=>{
console.log(event);
}
event对象有一些重要属性,以下是必须牢记的:
- type 代表事件类型,例如:click、mouseover...
- target 代表事件的目标控件
- currentTarget 代表事件监听者控件,等于 this
- eventPhase 事件处理程序的阶段:1.捕获 2.到达目标 3.冒泡阶段
- shiftKey bool类型,发生事件时 shift 键是否按下
- ctrlKey bool类型,发生事件时 ctrl 键是否按下
- altKey bool类型,发生事件时 Alt 键是否按下
还有一些方法: - stopPropagation 方法:取消事件冒泡
- preventDefault 方法:阻止默认事件的发生
在上面的代码当中,点击按钮 btn 监听click 事件,输出的结果中 event 的 target、currentTarget 都等于 this,就是按钮对象自身;
但在页面的 body 的监听当中,接收到的 event 当中,currentTarget 和 this 代表 body,target 当表点击发生的真正对象 btn
// 采用 preventDefault 方法,阻止超链接的默认动作
let link = document.getElementById("mylink");
link.onclick = (event){
// 此处可以加上 用户状态判断...
event.preventDefault();
}
采用 event 的 type 属性,可以简化事件处理函数
let btn = document.getElementById("mybtn");
let handler = (event)=>{
switch(event.type){
case "click":
console.log("clicked");
break;
case "mouseover":
console.log("mouseover");
break;
default:
console.log(event.type);
break;
}
}
btn.onclick = handler;
btn.onmouseover = handler;
5.DOM3级事件
DOM3级事件在DOM2级事件的基础上添加了更多的事件类型,全部类型如下:
UI事件,当用户与页面上的元素交互时触发,如:load、unload、scroll
焦点事件,当元素获得或失去焦点时触发,如:blur、focus
鼠标事件,当用户通过鼠标在页面执行操作时触发如:dbclick、mouseup
滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
文本事件,当在文档中输入文本时触发,如:textInput
键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart,compositionupdate,compositionend
变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified
同时DOM3级事件也允许使用者自定义一些事件。
一些常用的事件:
// load:页面加载事件
window.addEventListener("load",(event)=>{
console.log("html文档已经加载完毕");
});
// DOMContentLoaded: 网页DOM树构建完成后触发,并不等待图像等附加资源完成
// beforeunload:可以发生在网页内容卸载之前,可以阻止(例如要求用户进行保存)
window.addEventListener("beforeunload",(event)=>{
console.log("文档保存...");
});
// unload:可以发生在网页导航,加载新网页时, 旧内容已经卸载
window.addEventListener("unload",(event)=>{
console.log("文档已经卸载...");
});
// error:添加全局错误的监听,如果页面内图像未加载成功,就将图像替换为另一张图像
window.addEventListener("error",(event)=>{
console.log('body knows:',event);
console.log(event.target.tagName);
if (event.target.tagName=="IMG"){
console.log(event.target);
event.target.src = 'img/404.jpg';
}
},true);
// resize:窗口缩放事件
window.addEventListener("resize",(event)=>{
console.log("height=",window.innerHeight);
console.log("width=",window.innerWidth);
});
// textInput:监听输入内容
// textInput 不同于 keyPress,只监听输入区内容,且可以通过 event.data 获得输入的内容
let myTxt = document.getElementById("myInput");
myTxt.addEventListener("textInput",(event)=>{
console.log(event.data);
if (event.data=="yyds"){
event.target.value = event.target.value.replace(event.data,"永远的神");
}
});
6.时钟事件
setTimeout函数,可以利用系统时钟,创建一个被时钟唤醒的函数,来执行功能;
setInterval函数,可以利用系统时钟,创建一个以固定间隔执行任务的程序。
// 创建一个5秒钟以后执行的程序
let timer1 = setTimeout(()=>{ console.log('一次性任务');},5000);
// 如果在timer1执行前调用此函数,可以取消时钟任务
function killTimer(){
clearTimeout(timer1);
}
// 创建每秒报时的报时器
let timer2 = setInterval(()=>{
console.log(new Date());
},1000);
// 清除报时器
function killInteTimer(){
clearTimeout(timer2);
}
7.SPA应用中的事件处理框架
为了避免在html控件上直接添加事件处理程序与监听程序,避免复杂凌乱的事件函数,避免html与JS代码的混合,以及避免执行时机、内存泄露等问题,
可以利用 DOM 事件机制,利用事件捕获以及事件冒泡,在 DOM 的顶层节点上添加事件监听处理函数,将用户交互事件处理入口归并到一处。
主要原理是,在需要交互的控件上添加自定义属性,并赋值,在DOM顶层监听中,利用截获的 event.target 获得事件发生的控件,
利用控件的 attributes 数组获得自定义属性的值,以这些值作为参数,用于事件处理函数。
// 以下程序中,控件上添加了 ele_type 自定义属性
// Html控件
<span id="lab_username" ele_type="username" ele_data="Bruce">精思入神</button>
<p ele_type="userinfo">这个用户刚刚注册,还没有填写个人信息</p>
// 统一的处理入口
document.addEventListener('click',function(event){
let t = event.target;
if (!t.attributes.ele_type){
return;
}
let type = t.attributes.ele_type.value;
if (type=="username"){
console.log("username clicked!")
let userid = t.attributes.ele_data.value;
console.log(userid); // 输出 Bruce
}
if (type=="userinfo"){
console.log("userinfo clicked!")
}
})
8.事件模拟
可以利用代码模拟事件。可以用在程序逻辑需要的地方,不需要用户动作,自动提供事件
let btn = document.getElementById("mybtn");
// 创建事件
let m_event = document.createEvent("MouseEvents");
// 初始化为鼠标click事件
m_event.initMouseEvent("click",true,true,document.defaultView,0,0,0,0,0,false,false,false,false,0,null);
// 通过 btn 按钮发送出去(此 event 的target自动赋值为当前按钮)
btn.dispatchEvent(m_event);
9.用户自定义事件
利用用户自定义事件,可以在控件、模块之间传递消息,不用在模块之间直接函数调用,可以让相互调用更加灵活。
// 自定义事件
let div = document.getElementById('myInput');
// 定义用户自定义事件监听程序,app_event 是自己定义的全新消息类型
div.addEventListener("app_event",(event)=>{
console.log(event.detail);
});
// 激发用户自定义事件
function sendAppEvent(){
if (document.implementation.hasFeature("CustomEvents","3.0")){
event = document.createEvent("CustomEvent");
event.initCustomEvent('app_event',true,false,'Hello App!');
div.dispatchEvent(event);
}
}
参考文献:
- DOM事件机制: https://blog.csdn.net/caseywei/article/details/81435645
- JS中DOM0,DOM2,DOM3级事件模型解析: https://www.jianshu.com/p/bbd6e600c0d3
- 20个优秀的 JavaScript 键盘事件处理库: https://www.cnblogs.com/lhb25/p/20-javascript-libraries-to-handle.html