Web 事件机制研究
2021-04-13罗才华刘小园
罗才华,刘小园
(罗定职业技术学院信息工程系,广东罗定,527200)
1 事件与事件对象
Web 事件是html 与JavaScript 之间进行交互的载体与桥梁,是文档或浏览器窗口中发生的一些特定的交互瞬间,如用户鼠标经过或点击某个特定元素、按下键盘上某个按键、浏览器窗口大小发生改变、页面加载与页面滚动等动作都属于Web 事件[1]。事件最早是作为分担服务器运算负载的一种手段出现在IE3 和Netscape Navigator2 中,后来DOM2 级规范开始尝试以一种符合逻辑的方式来标准化DOM事件,目前主要浏览器都已经实现了“DOM2 级事件”[2]。事件通过监听页面的变化或者用户对页面或浏览器执行的动作,从而执行某些指定的JavaScript 代码段,达到实现某些功能的目的。
事件对象是一个包含事件具体信息的对象。该对象在事件发生时产生,并在该事件处理程序函数中以参数的形式出现,通常命名为event。比如,用户在页面中点击了某个元素,那么在产生一个事件的同时也生成了一个事件对象,该对象中包含着事件的具体信息,并可以从该对象获取到这个事件的类型和触发这个事件的具体元素。如果事件是由鼠标触发,对象中会包含鼠标触发时的位置信息;如果事件是由键盘的触发,对象中则会包含具体的按键信息等。
2 事件流
在网页中,元素是具有层级关系的。当用户从浏览器打开一个网页时,这个网页的层级关系依次为:window →document →html →body →div(或某个具体的元素)。当用户点击了一个div 元素(或其他元素)时,不仅点击了元素本身,也点击了元素的容器,甚至点击了整个页面,那么除了被点击的元素会触发事件之外,body、html 等元素也会触发事件。因为元素间存在包含关系,那么事件的触发就涉及到触发顺序的问题。
事件流用来说明页面中事件的触发过程[3],但IE 和Netscape 开发团队提出了两种完全不同类型的事件流。IE 提出的事件流是事件冒泡,形容事件像水中的鱼类吐气泡一样往上冒,直到顶端,即当页面中某个特定目标元素事件触发,事件会一直沿着包含关系往上传递(DOM树上触发事件的当前结点逐级向上传至根节点),事件冒泡的特点是从某特定事件目标开始到不确定的事件目标结束,如图1 所示。而Netscape 提出的是另一种事件流——事件捕获,它与事件冒泡的传播顺序基本上完全相反,强调事件到达特定目标节点之前就应该对该事件进行捕获,即document 最先获取事件,然后事件沿DOM 树依次向下传,直到特定目标节点,如图2 所示[2]。需要注意的是,现代浏览器尽管都支持事件冒泡,但实现细节略有差别,有些浏览器在事件冒泡过程中会从body 跳过html,直接到document,而有些浏览器则一直冒泡到window 对象,如在IE5.5、IE6 和主流浏览器中,事件冒泡过程分别为div →body →document、div →body →html →document 和div →body →html →document →window。现代浏览器基本都支持事件捕获,但由于历史原因,基本都是从window 对象开始捕获事件,而非按DOM2 级事件规范要求的“事件应从document 对象开始传播”。由于旧版浏览器(如IE9 以前版本)不支持事件捕获,因此在开发中建议优先使用事件冒泡,如有特殊需要再选择使用事件捕获。
图1 事件冒泡模型
图2 事件捕获模型
下面以一个简单的例子来展示事件冒泡和事件捕获的具体过程,html 和JavaScript 主要代码如下:
wrapper
container
let wrapper = document.querySelector('#wrapper');
let container = document.querySelector('#container');
let btn = document.querySelector('#btn');
wrapper.addEventListener('click', ()=> {
console.log('点击了wrapper 元素')
})
container.addEventListener('click', ()=> {
console.log('点击了container 元素')
})
btn.addEventListener('click', (event)=>{
onsole.log('点击了btn 元素')
})
分别为wrapper、container 和btn 三个div 元素注册了click 单击事件,当点击了btn 元素后,从图3 可以看到这三个元素是按事件冒泡接收事件,即btn →container →wrapper,这是因为函数addEventListener(eventType,function,useCapture)三个参数中,第一个参数表示触发的事件类型,此处用了click;第二个参数是该事件触发后的回调函数,可处理事件触发后的具体操作;第三个参数是布尔值,用来开启或关闭捕获模式,true 为捕获模式,false 为冒泡模式,如果不传该参数的话,就采用默认的冒泡模式,上例中就是采用默认的冒泡模式。当将上例中addEventListener()函数都添加第三个参数值true 时,则三个元素就会按事件捕获来接收事件,即wrapper →container →btn。
图3 事件冒泡示例
3 DOM 事件流
不同级别的DOM采用DOM事件处理方式也不同。DOM的级别一共分为4 级,即DOM0 级、DOM1 级、DOM2 级和DOM3 级。由于DOM1 级标准中没有定义事件相关的内容,所以不存在1 级DOM事件模型,因此,DOM事件只有3 个级别,即DOM0 级事件、DOM2 级事件和DOM3 级事件[4-5]。
由于DOM0 级事件中存在冒泡和捕获两种截然不同的事件流模型,引发了大众对web 事件流的猜测。因此,ECMAScript 对事件流在DOM2 中进行了新的规范,规定事件流顺序包含了“事件捕获、处于目标和事件冒泡”三个阶段,融入了冒泡和捕获两种事件模型,如图4 所示。其中事件捕获阶段提供了拦截事件的机会,处于目标阶段确保特定目标接收到事件,事件冒泡阶段负责对事件做出响应。虽然在DOM2 中明确指出“在事件捕获阶段,事件不会接触到事件目标元素”,然而各浏览器厂商好像并没有完全遵守,因此在事件捕获阶段和事件冒泡阶段均有在事件目标上进行事件处理的机会。
图4 DOM事件流模型
随着网页的运行环境越来越复杂、用户对网页的功能需求越来越大,DOM3 级事件应运而生,DOM3 级事件在DOM2 级事件的基础上重新对事件进行了规整分类,大致可分为UI 事件、焦点事件、鼠标事件、滚轮事件、文本事件、键盘事件、合成事件和变动事件等几种。新增的DOM3 级事件主要是面对其他各种运行环境的事件,比如用于检测移动端设备屏幕是否发生旋转,用户是否触摸了屏幕、是否在屏幕上滑动、是否在屏幕上画手势(如屏幕锁)等触摸或手势事件,还有智能电视遥控器的按键事件等等。DOM3 事件主要是对以往的事件进行了规划和增加了一些面对多场景的事件类型,依然会按照事件的基本运行机制来运行,仍然需要通过事件处理程序来处理事件。
4 默认事件与阻止事件传播
在项目开发过程中,有时候会遇到对元素触发事件后运行结果并没有预期效果的情况,这个时候需要考虑到该元素的默认事件的问题,在页面中,所有元素都可以触发事件,但是有些特殊元素会先触发自身的默认事件。比如a 标签,在页面中a 标签的默认事件动作是跳转链接,当点击该元素时,页面将会跳转到a 标签href 属性设置的那个网址上,如果想要阻止这种情况发生,就需要使用preventDefault()函数,这个函数可以通过事件对象调用,在事件触发时,可以利用该事件的事件对象执行preventDefault()函数来阻止元素的默认事件[6],然后去执行其他代码,示例代码如下:
var target = document.querySelector('#target');
target.addEventListener('click', (event)=> {
event.preventDefault()
console.log('点击了a 标签元素')
})
除了默认事件会影响代码的运行效果外,事件的传播也会对实现某些功能产生很大的干扰。比如现有一个列表,列表中每个列表项都绑定了一个事件,当用户鼠标点击该列表项时,会展开一个详情框,显示该列表项的数据详情;同时,该列表项里面还包含了一个编辑按钮,当用户点击了这个按钮的时候,当前的列表项的文字内容变得可修改。以冒泡事件传播方式为例,如果在没有阻止事件冒泡传播的情况下,当点击了列表项中的那个编辑按钮,不但列表项的内容可以被修改,列表项的详情框也会被展开,因为当点击按钮后,按钮触发该事件后,事件会继续沿着父容器向外传播,当传播到包含该按钮的列表项时,列表项也触发了该事件。这就是很糟糕的用户体验了,但是通过使用阻止事件传播就可以解决这个问题。无论是事件冒泡还是事件捕获,都可以使用stopPropaga tion()来阻止事件的传播,该函数与默认事件的函数获取方式相同,示例代码如下:
item1
选取我院2017年12月—2018年5月期间收治的甲状腺CNB患者84例,且84例患者均在我院进行了手术。84例CNB患者共87个结节,其中男性16例,女性68例,年龄24~73岁,平均(42.65±5.27)岁。其中3个患者进行2个结节穿刺,81个患者进行1个结节穿刺。术前均进行血常规、血凝四项、传染病三项、乙肝五项检查。
item2
item3
let listItem = document.querySelectorAll ('.list-item');
let editBtn = document.querySelectorAll('.edit');
// 绑定列表项的事件, 点击列表项后会展开列表项里面的详情框
listItem.forEach((item, index)=> {
item.addEventListener('click', (event)=> {
if (item.querySelector('.detail').style.height=== '200px'){
item.querySelector ('.detail').style.display= "none"
item.querySelector('.detail').style.height= "0"
}else {
item.querySelector ('.detail').style.display= "block"
item.querySelector('.detail').style.height= "200px"
}
})
})
// 绑定列表项里面的按钮,点击列表项里面的按钮后,按钮文本会由‘edit’变成‘编辑模式’
editBtn.forEach((btn, index)=> {
btn.addEventListener('click', (event)=> {
event.stopPropagation()
if(btn.innerText === '编辑模式'){
btn.innerText = 'edit'
}else {
btn.innerText = '编辑模式'
}
})
})
代码执行后的效果如图5 所示。
图5 示例效果
阻止默认事件的函数preventDefault()和阻止事件传播的函数stopPropagation()是符合w3c 标准的,适用于大部分浏览器。但是面向旧版本的IE浏览器时,阻止默认事件和阻止事件传播则需分别使用window.event.returnValue = false 和window.event.cancelBubble = true。
5 事件处理程序
事件处理程序是指用来响应某个事件的JavaScript 代码片段,一般封装成一个函数,事件被触发时,就会调用执行这一代码段,添加事件处理程序的方式有4 种,分别是HTML 事件处理程序、DOM0 级事件处理程序、DOM2 级事件处理程序、IE 事件处理程序。
5.1 HTML 事件处理程序
最简单最直接的方式就是HTML 事件处理程序,可以直接在html 元素上通过添加事件属性名,然后将要执行的具体动作设置为该属性的值即可,也可以通过在页面其他地方定义脚本函数的方式来实现,例如:。但是不能在属性值中使用未转义的HTML 实体字符,如不能直接使用<或>,要用相应的实体符号<和>来替代。除此之外,HTML 事件处理程序也可以移除事件处理程序,只需将事件处理程序的属性值设置为null 即可,但是这种方式和不绑定事件处理程序没有什么差异,所以实用性不大。
5.2 DOM0 级事件处理程序
由于HTML 方式存在HTML 代码与JS 代码没完全分离、扩展事件处理程序的作用链域在不同浏览器可能会有不同结果、时差问题和代码紧耦合等诸多缺点,导致使用起来非常糟糕,而DOM0 级事件处理程序则可以避免这些问题。DOM0 级事件处理程序是使用JavaScript 指定事件处理程序的方式。在给元素绑定事件处理处理程序之前,需先获取一个要操作的对象的引用,再将一个函数直接赋值给一个事件处理程序属性[7],且所有的事件名都是采用小写的,并以on 开头,后接具体动作的事件程序名称,示例代码如下:
var btn = document.querySelector ('#btn'); // 先取得该对象的引用
btn.onclick = function (){ // 给引用的对象的事件属性赋值一个匿名函数,也可以是非匿名函数
alert('我是按钮')
}
由于DOM0 级方式绑定事件处理程序使用方式简单,并且JavaScript 和HTML 有较好的耦合度,即使html 的元素修改了,只要指定的id 不变,就能获取到该元素,就可以使用该事件程序。如果需要移除该事件程序,则只需将事件属性值设为null 即可,例如:btn.onclick=null。
5.3 DOM2 级事件处理程序
由于DOM1 级标准中没有定义事件相关的内容,所以也不存在DOM1 级事件程序,而是直接到DOM2 级事件处理程序,而这种事件处理程序在上文中已有所介绍,可分别通过addEventListener()和removeEventListener()函数实现DOM2 级事件程序的绑定和移除,这两个函数都带有三个参数,且含义相同。但要注意的是,可以同时添加多个DOM2级事件处理程序,并会按添加的顺序触发,这与DOM0 级事件会发生覆盖具有完全不同的表现,另外,DOM0 和DOM2 也可以共存,并不会互相覆盖;第一个表示事件名的参数不再像DOM0 级那样以on 开头了,而是直接使用on 后面的具体动作的事件程序名称;第二个表示处理事件的回调函数,该可以是个匿名函数,但是如果绑定事件时使用了匿名函数则无法将其移除[8]。
5.4 IE 事件处理程序
IE 浏览器可以正常使用DOM0 级事件处理程序,但是大部分IE 版本却不支持DOM2 级事件处理程序,不过IE 提供了两个与DOM2 级事件处理程序类似的函数,即attachEvent()和detachEvent()[9-10]。attachEvent()函数为目标元素提供事件程序绑定,该函数接收两个参数,第一个参数是事件名,但这里的事件名与DOM0 级的事件名是一致的,即以on 开头的,例如:‘onclick’;而第二个参数是处理事件的回调函数,需要注意的是,从IE11 起,attachEvent()函数就无法使用了。而detachEvent()函数是用于移除事件程序的,该函数需要的参数与attachEvent()函数的参数一致。在网页运行环境不确定情况下,可通过if 语句判断当前浏览器是否支持该事件程序来选择函数,下面是事件绑定(事件移除过程类似)示例代码:
var target = document.querySelector('#target');
var handler = function(){
console.log('DOM2 级事件处理程序')
}
//检测是否支持DOM2 级事件处理程序
if(target.addEventListener){
target.addEventListener ('click',handler,false)
}else{
target.attachEvent("onclick", handler);
}
6 性能优化
在网页中,每添加一个事件处理程序,浏览器就会开辟一块内存空间。因为每绑定一个事件处理程序都需要绑定一个事件处理的函数,而在js的定义中,函数是属于对象的,每新建一个对象,浏览器都要开辟一块内存给这个对象存储数据。那么随着网页的内容越来越多,越来越复杂,页面的绑定的事件程序就会越来越多,使用的内存也会越来越多,就会导致网页的运行速度变慢,因此就需要对网页的性能进行进一步的优化。
事件移除和事件委托就是常用的性能优化手段。通过调用移除事件处理函数可以释放给元素绑定一个事件处理程序在浏览器中开辟的内存空间。事件委托就是一个针对页面由于绑定事件处理程序过多而导致网页内存占用过大问题提出的一个性能优化解决方案,主要是利用事件流中的冒泡传播方式,在顶层的父元素上绑定一个事件处理程序,专门处理该元素下产生的事件。如上文阻止事件传播的例子中给列表中的每个列表项都添加了一个事件处理程序,如果列表项很多,就会导致页面的内存消耗过大,性能比较糟糕,可以通过事件委托来优化这个问题。