1 事件(Event)
事件也就是用户或者浏览器执行的某种动作,而JS与Html之间的交互是通过事件而来的。使用仅在事件发生时执行的**监听器(事件处理程序)**来订阅事件。web浏览器可以发生多种事件,在DOM3 Events定义了如下事件类型:
- 用户界面事件(UIEvent):涉及与BOM交互的通用浏览器事件
- 焦点事件(FocusEvent):在元素获得和失去焦点时触发
- 鼠标事件(MouseEvent)
- 滚轮事件(WheelEvent):使用鼠标滚轮时触发
- 输入事件(InputEvent)
- 键盘事件(KeyboardEvent)
- 合事事件(CompositionEvent):在使用某种IME(Input Method Editor,输入法编辑器)输入字符时触发
1.1 事件处理程序
事件处理器(或事件监听器),就是来响应某个事件的函数。主要有HTML
事件处理程序、DOM0事件处理程序和DOM2事件处理程序。
1.1.1 HTML
事件处理程序
也就是直接将事件函数写在HTML
元素的属性上,而所要执行的函数代码,实际上是通过JS引擎由eval()
调用的,它是全局作用域。举个例子:
<button onclick="alert('hello,world')"></button>
同时它也可以写成这样:
<button onclick="printHello()"></button>
<script>
function printHello() {
alert(this)//object window
alert('hello,world');
}
</script>
HTML
事件处理程序有一定的缺点:
- 时机问题:比如当
<script>
中的js代码还没加载,就提前执行事件处理程序,会造成错误 - js与html强耦合:如果修改事件处理器,需要同时改js和html两个地方的代码
- 对事件处理程序作用链的扩展在不同浏览器中可能导致不同的结果
针对这些缺点,在实际的代码编写中,更多人选择JavaScript
指定事件处理程序
1.1.2 DOM0
事件处理程序
在传统的监听器处理方式中,一般把监听器赋值给DOM元素的一个事件处理程序属性(每个元素都有,比如onclick、onload等等),完全通过js代码来进行处理,这个时候btn是局部作用域,而且js和html代码也进行解耦,如下代码:
<button id="button">
<script>
let btn = document.getElementById("button");
btn.onclick=function(){
alert(this);//object HTMLButtonElement
alert('test DOM0 event function');
}
</script>
但是同样也存在着一些问题:
- 无法为一个onclick属性添加多个事件处理程序
- 对于一个元素的事件处理程序属性,也无法轻易赋值事件处理程序(有可能会覆盖之前其他的事件处理程序)
所以针对这一点,DOM2做了一些相关的优化
1.1.3 DOM2
事件处理程序
DOM2 Events
为事件处理程序的增加了两个方法,这些方法暴露在所有的DOM节点上
addEventListener()
:事件处理程序赋值removeEventListener()
:事件处理程序移除
此外它们接收三个参数:
- 事件名:不同的事件的名称
- 事件处理函数:事件处理程序实现
- 一个布尔值:
true
:代表捕获阶段调用事件处理程序false
:表示在冒泡阶段调用事件处理程序
因此对于DOM0
中的代码,可以对一个DOM元素添加多个事件处理程序:
<button id="button">
<script>
let btn = document.getElementById("button");
btn.addEventListener("click", ()=>{
alert('first button');
},false);
btn.addEventListener("click", ()=>{
alert('second button');
},true);
</script>
输出结果:先输出second button
然后再输出first button
。
可以用removeEventListener()
移除事件处理程序,这个时候就无法点击发生对应事件了:
<button id="button">
<script>
let btn = document.getElementById("button");
let test =function(){
alert('test event function remove')
};
btn.addEventListener("click", test, false);
btn.removeEventListener("click", test, false);
</script>
那么为何在addEventListener()
添加多个事件处理程序时,不按照程序顺序输出?可以发现修改了输入参数的布尔值,通过不同阶段调用事件处理程序,能够得到不同的事件执行顺序,这就涉及到了事件流。
1.2 事件流
也就是描述页面接收事件的顺序,在早期不同的浏览器厂商对于事件流有不同的实现方案。IE的事件冒泡,Netscape的事件捕获。之后DOM2将事件流进行了统一(所有现代浏览器都支持DOM事件流,只有IE8及更早版本不支持)
1.2.1 IE事件流-事件冒泡
事件被定义为从文档树最深节点到外部的节点:比如<div>---<body>---<html>---document
1.2.2 Netscape事件流-事件捕获
和事件冒泡相反,事件捕获是外部的节点到内部的节点。对于上面的节点,事件访问的顺序应该是:document---<html>---<body>---<div>
。事件捕获实际上是为了在事件到达最终目标前拦截事件。
1.2.3 DOM事件流
1.2.3.1 0级DOM事件模型(非W3C事件模型)
也就是指W3C的DOM标准形成前的事件模型,也就是原始的添加事件监听函数的模型,W3C的DOM标准并不支持的事件模型,具体例子如下:
<!--html部分-->
<input id="myButton" type="button" value="Press Me" οnclick="alert('thanks')">
<!-- js部分 -->
document.getElementById("myButton").onclick = function() {
alert('thanks');
}
1.2.3.2 2级DOM事件模型
DOM1级于1998年10月1日成为W3C推荐标准,但是1级DOM标准中并没有定义事件相关的内容,所以没有所谓的1级DOM事件模型。2级DOM中除了定义了一些DOM相关的操作之外还定义了一个事件模型 ,这个标准下的事件模型就是我们所说的2级DOM事件模型。
DOM2 事件规范规定了事件流分为三个阶段,会以如下顺序触发事件:
- 事件捕获
- 到达目标
- 事件冒泡
以前面的例子,捕获阶段是从document --> html --> body,div阶段是到达目标阶段,然后事件再进行冒泡,反向传播到document。
1.2.3.3 3级DOM事件模型
DOM3级没有对事件做任何的修订,所以现代浏览器仍然沿用2级DOM事件标准
计算类型的任务,可以由CPU进行执行,但是对于一些IO操作,文件读写,这个时候CPU就闲置,因此可以执行其他的工作。
JavaScript是单线程的,同一时间只能是执行一个任务,也就是它只能执行计算类的操作,无法操作
1.3 事件对象
当DOM中发生事件时,所有相关信息都会被收集并存储在一个名为
event
的对象中。这个对象包含了一些基本信息,比如导致事件的元素、发生的事件类型,以及可能与特定事件相关的任何其他数据。这个对象就是事件对象
事件对象只要事件发生时才会 产生,不管是以哪种方式(DOM0或DOM2)指定事件处理程序,都会传入这个event对象。当所有事件处理程序结束后,事件对象就会被销毁。
<button id="button">
<script>
let btn = document.getElementById("button");
btn.addEventListener("click", (event)=>{
alert(event.type);//"click"
});
</script>
所有事件对象都有这些公共属性:
:::info
- bubbles 布尔值,表示事件是否是冒泡类型
- cancelable 布尔值,表示事件是否可以取消默认动作
- currentTarget 当前目标元素,即添加当前事件处理程序的元素
- target 实际目标元素,即实际触发事件的元素
- type 返回当前事件的名称
- eventPhase 事件传播的当前阶段,1表示捕获阶段
:::
公共方法主要有:
:::info
- preventDefault() 通知浏览器不要执行该事件的默认动作,常用于阻止链接的跳转,表单的提交,等标签的默认行为
- stopPropagation() 冒泡阶段下,阻止事件的继续向上冒泡
:::
1.4 事件委托
在JavaScript中,页面事件处理程序的数量与页面整体性能直接相关。所以在使用事件处理程序时,需要对过多的事件处理程序进行优化调整,比如事件委托。事件委托就是利用事件冒泡,使用一个事件处理程序来管理一种类型的事件。
事件委托是通过在所有元素的共同祖先节点添加一个事件处理程序,来达到管理多个事件类型的效果,比如:
<ul>
<li>列表项1</li>
<li>列表项2</li>
<li>列表项3</li>
</ul>
<script>
var list=document.getElementsByTagName("li");
for(i=0;i<list.length;i++){
list[i].onclick=function(){
alert("我是"+e.target);
}
}
</script>
就可以修改为处理祖先节点<li>
元素,来统一管理该子元素下的多个事件处理程序
var ul=document.getElementById('ul');
ul.onclick=function(e){
var e= e || window.event;
var target = e.target || e.srcElement;
if(target.nodeName.toLowerCase() === "li"){
alert("我是"+e.target);
}
}
2 事件循环(Event Loop)
在具体的代码执行中,JS引擎会常驻内存中,等待着宿主把JS代码或者函数传递给它执行。
在 ES3 和更早的版本中,JavaScript 本身还没有异步执行代码的能力,这也就意味着,宿主环境传递给 JavaScript 引擎一段代码,引擎就把代码直接顺次执行了,这个任务也就是宿主发起的任务。
但是,在 ES5 之后,JavaScript 引入了 Promise,这样,不需要浏览器的安排,JavaScript 引擎本身也可以发起任务了。
在JSC引擎的术语中,把宿主发起的任务称作为宏观任务;把JS引擎发起的任务称为微观任务。在操作系统中,通常等待的行为都是一个事件循环。所以JS没有自己的事件循环系统,它依赖浏览器的事件循环系统。