【轻聊前端】网页交互之匙——事件

现今各种框架、工具‘横行’,到处在讲原理和源码,更有跨端技术需要我们去探索,但如果基本功不好,学什么都是事倍功半,效果很不好,花费时间的同时打击自信心。此篇文章,为我所计划的【轻聊前端】系列第(十四)篇,旨在系统地、逻辑性地把原生JavaScript知识分享给大家,帮助各位较为轻松地理清知识体系,更好地理解和记忆,我尽力而为,望不负期待。

前面的十多篇文章,介绍了很多模块的知识,数字、字符串、对象、DOM、函数…

但如果告诉你,对于基础的Web页面,JavaScript代码是不必要的,你怎么看,前面的东西白说了?非也。

网页的结构由HTML负责,美观的布局由CSS负责,对于页面展示,这些就够了,但当一个网页呈现在用户面前,用户想要跟它进行交互,让它对用户的行为做出反应,怎么办?这就需要事件。

有了事件,就能够在“用户和网页”、“用户和浏览器”之间建立联系。事件就和JavaScript有关了。

所以,事件的两个特点:

  • 跟操作相关
  • 由客户端提供

事件类型

事件的类型有很多,逐个介绍不现实,也无必要,大致可分这几类:

外设事件:click、mouseenter/up/down、keyup/down/press…

表单事件:input、change…

界面事件:load、select、scroll…

焦点事件:focus、blur…

设备事件:orientationchange、deviceorientation、devicemotion…

触屏事件:touchstart/end/move、gesturestart/change/end…

事件有很多形式,我们只需要理解一件事情——用户所有的操作,或者网页状态的变化,都会对应着某个事件,如果想趁此机会做点什么,将其绑定到目标元素即可,元素绑定了事件,就在人机之间建立了联系。

下面看看有哪些绑定事件的方式。

事件绑定

事件绑定有多种方式,并不都常用,但需要了解它们之间的差异和发展过程。

这里涉及到两个概念:绑定方式、处理程序

HTML

第一种是在HTML中直接绑定,这是在网页开发初期最为常见的方式。

1
<div onclick="console.log('点击事件发生了')"></div>

这段代码给div绑定了点击事件,使用“on” + “事件名称”给元素设置属性的方式。

这里执行的console方法就是“事件处理程序”,它不仅可以用来执行脚本,还可以调用在页面其他地方定义的方法。

这种方式直观明了,但它的弊端是与HTML结构过于耦合,出于职责分离的原则,我们不希望改动脚本代码的时候还要去动结构,所以通常不推荐。

DOM0

另一种较为传统的方式,是把一个函数赋值给DOM元素的事件处理属性。

当然,要先取得目标DOM对象的引用,然后就是一个简单的赋值动作。

1
2
3
4
let btn = document.querySelector("#myBtn")
btn.onclick = function(){
console.log('clicked')
}

DOM2

你可能有个疑问,怎么从DOM0直接跳到DOM2?DOM1呢?

这是因为DOM在不同的时间点,不同版本的标准里,推出了不同的内容,DOM1里并没有跟事件相关的内容,DOM2时才有,所以就不会涉及DOM1了。

addEventListener()/removeEventListener()

接收3个参数:

事件名:就是前面事件类型中提到的,进行了什么操作,就有相对应的事件名;

处理函数:自定义,是跟操作相关需要达到的交互效果、数据传输或者业务逻辑等。

布尔值:false(默认值)表示在冒泡阶段调用事件处理程序、true表示在捕获阶段调用事件处理程序。

DOM2方式的优势:

  • 可以为同一个事件添加多个事件处理程序,且按照添加顺序来触发。
  • 可以精确控制事件触发阶段。

把上面那段代码换成DOM2的形式,就是这样:

1
2
3
4
5
let btn = document.querySelector("#myBtn")
btn.addEventListener('click',handler,false)
function handler(){
console.log('clicked')
}

聊到这儿,都是在聊绑定,但事件不是绑定了就万事大吉,就可以放着不管,还需要在合适的时机进行移除,为什么移除,因为事件处理程序本身也是一段代码,被放到内存中,用完了,或者长期不用,不移除就会白白占用空间,是一种浪费。

DOM0级处理程序的移除可以直接置空。

1
btn.onclick = null

DOM2级中使用removeEventListener(),并且,通过addEventListener()添加的事件处理程序只能使用removeEventListener()并传入同样的参数来移除。这意味着使用addEventListener()添加的匿名函数无法移除。

面试题——addEventListener方法的第三个参数可以是什么

你可能会问,前面不是说了第三个参数是布尔值?平时也是这么用的,有时甚至直接不写,怎么还问“可以是什么”?

没错,在旧版本的DOM中, addEventListener()的第三个参数是一个布尔值,表示是否在捕获阶段调用事件处理程序。但随着时间的推移,这已经不能满足大家对于事件处理程序更多的操作需求,怎么办,继续加参数?与其在方法之中添加更多参数,倒不如把第三个参数改为一个可以包含不同属性的对象来的方便,然后通过不同的设置达到目的就好。

答案:可以是布尔值,也可以是一个对象

问题又来了,对象里面可以设置什么。

capture: 表示listener会在该类型的事件捕获阶段传播到事件对象时触发。

once: 表示listener 在添加之后最多只调用一次。如果是 true,listener 会在其被调用之后自动移除。

passive: 设置为true时,表示listener永远不会调用preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。

signal:该AbortSignal的abort()方法被调用时,监听器会被移除。

了解到这些,下次碰到这个问题,你应该知道怎么应对了~

IE

看到这位,大家心里是咯噔一下,还是微微一笑呢?浏览器的兼容已经不是现阶段最大的问题了,特别是有了一些工具之后,我们不需要每人都手写一份兼容代码放到项目里,但了解还是要的。

IE实现了与DOM类似的方法,即attachEvent()和detachEvent()。

这两个方法接收两个同样的参数:事件处理程序的名字和事件处理函数。因为IE8及更早版本只支持事件冒泡,所以使用attachEvent()添加的事件处理程序会添加到冒泡阶段。

在IE中使用attachEvent()与使用DOM0方式的主要区别是事件处理程序的作用域。使用DOM0方式时,事件处理程序中的this值等于目标元素。而使用attachEvent()时,事件处理程序是在全局作用域中运行的,因此this等于window。

事件流

客户端为我们提供了事件,也提供了绑定方法,这不就齐了,事件流又是什么?

可以回想一下,前面聊DOM的时候,页面的元素节点组成了DOM树,而这棵树反应到页面上,就是一个个顺序摆放或者嵌套起来矩形盒子

假想一下:现在有一个列表,有列表项,列表项有个footer元素,footer元素里有个button按钮,那么,我点击button的时候,是在点击谁呢?

你说我在点击button,没错,但同样我是在点击footer,我也是在点击整个列表,

理论上讲,页面上的元素均可点击,但作为开发者,我们想要往哪个元素上绑定事件,又由哪个元素来响应行为呢?这就涉及到事件流。

事件流,即事件传播的流向

事件流有两种:事件冒泡事件捕获,它们分别是由IE和Netscape团队提出的。

事件冒泡

事件被定义为从最具体的元素开始触发,然后一直向上传播,直至文档。即从内向外。

事件捕获

和事件冒泡相反,最外层的节点最先收到事件,最具体的节点最后收到事件。即由外向内。

事件捕获实际上是为了在事件到达最终目标前拦截事件。

到这里,你可能要问了,到底现在浏览器是支持哪个?冒泡还是捕获呢?答案是:两个都支持,只是有个对应的流程。

事件流

DOM2 Events规范规定事件流分为3个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生,为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。请看图示:

555ce5668d7c445e7d6516ee9443813f.jpeg

有关“事件流”先介绍到这,后面有相关的运用。

事件对象(event)

“JavaScript中万物皆对象”,事件也不例外,但事件对象是什么,从哪来,有什么用途呢。

是什么

前面我们讲了,当需要跟网页产生交互的时候,事件提供了这个接口,这个接口,连接了“事件类型”和“事件处理程序”,前面也说,处理程序可以写逻辑,可以调方法,但除此之外,有相当一部分的操作,是需要我们拿到操作对象的某些数据的,比如:表单、表格、列表。这个数据,可以是业务数据,可以是设备相关的数据,也可以是跟事件相关的数据。

事件对象就担当了这样一个角色,他包含了触发事件的元素、事件类型,以及可能与特定事件相关的其他数据。例如,鼠标操作的位置信息,键盘操作中被按下的键的信息等。

而这个事件对象,从哪来呢,还记得前面的绑定事件吗?都会给事件绑定一个处理程序,事件对象就是作为处理程序的入参被传进去

1
2
3
btn.onclick = function(event){
console.log(‘事件对象:’,event)
}

有什么

每种事件都有这个对象,但不同的事件对象包含不同的属性和方法。这就在减少信息冗余的同时提供了针对性。

所有事件对象都会包含下表列出的这些公共属性和方法:

target:事件目标,常在获取节点内容时使用

currentTarget:处理程序所在的元素

type:事件类型,在一个处理程序处理多个事件时很有用。

preventDefault():用于阻止特定事件的默认动作。

stopPropagation():用于立即阻止事件流在DOM结构中传播,取消后续的事件捕获或冒泡。

eventPhase:可用于确定事件流当前所处的阶段。如果事件处理程序在捕获阶段被调用,则eventPhase等于1;如果事件处理程序在目标上被调用,则eventPhase等于2;如果事件处理程序在冒泡阶段被调用,则eventPhase等于3。不过要注意的是,虽然“到达目标”是在冒泡阶段发生的,但其eventPhase仍然等于2。

由于数量繁多,这里没有列举完全,关于特定事件的特定属性也略过,有需要大家可以去查阅文档,或者直接写一段代码打印出来便知。

面试题——target 和 currentTarget 的区别

在事件处理程序内部,this对象始终等于currentTarget的值,而target只包含事件的实际目标。如果事件处理程序直接添加在了意图的目标,则this、currentTarget和target的值是一样的,否则它们就会是不同的值。

内存与性能

看起来,事件仅仅跟交互相关,交互动作跟性能有什么关系?

这就不得不重提上面聊过的两个东西:事件绑定处理程序

事件绑定,即为元素绑定事件处理程序所需访问DOM的次数,这会在前期造成整个页面可交互时间的延迟。

处理程序:即所定义的可调用的函数,函数也是对象,会占用内存空间,对象越多,对性能的影响就越明显。

“过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。例如,click事件冒泡到document。这意味着可以为整个页面指定一个onclick事件处理程序,而不用为每个可点击元素分别指定事件处理程序。

那么处理程序怎么优化呢,想要不占用内存,只能释放出来,即及时删除不用的事件处理程序。前面讲绑定的时候提过,这里再深入一些。

删除处理程序就代表不再使用,什么时候不再使用呢,比如:

  • 页面元素整个被删除或者替换,比如使用removeChild()或replaceChild()删除的节点,和使用innerHTML整体替换页面的某一部分,元素没了,但事件处理程序并不一定会被垃圾收集程序清理掉。
  • 页面卸载,当切换页面或者关闭页面的时候,绑定在页面元素上的处理程序仍在内存中,这也是个容易忽略的点,在页面卸载事件发生时(比如:unload)将其手动清理是推荐的做法。

这些优化是和业务实现无关的,我们常说,实现一个功能有不同的方法,差异就在这些细节上,而细节决定了最终的体验。

事件模拟

在文章开头,我们提到了事件的一个特点,即由客户端提供,非自定义,这是因为,大部分情况下,我们只需要使用它即可,并没有特别的需求,如果有自定义的需求,也是可以实现的。

可以使用document.createEvent()方法创建一个event对象。这个方法接收一个参数,此参数是一个表示要创建事件类型的字符串。

创建event对象之后,需要使用事件相关的信息来初始化。每种类型的event对象都有特定的方法,可以使用相应数据来完成初始化。方法的名字并不相同,这取决于调用createEvent()时传入的参数。

事件模拟的最后一步是触发事件。为此要使用dispatchEvent()方法,这个方法存在于所有支持事件的DOM节点之上。dispatchEvent()方法接收一个参数,即表示要触发事件的event对象。

比如模拟一个鼠标事件:

1
2
3
4
let btn = document.querySelector("#myBtn")
let event = document.createEvent('MouseEvents')
event.initMouseEvent('click',true,true...)
btn.dispatchEvent(event)

整个过程跟EventEmitter的实现有点类似,只是使用了原生提供的方法,而且创建了事件对象相关的信息。

总结

这是本系列的第十四篇,在讲了不同数据类型,函数,DOM 和 BOM之后,加上这篇的事件,不仅网页的各元素之间可以串联起来,人和网页也能产生交互了,似乎已经接近了具有完备功能网页的模样,但是,仍有一些东西,特别对于现代网页来说很重要的东西尚未提及。

革命尚未成功,不过它越来越近了,让我们一起期待最后的几块拼图吧。