浏览器的事件机制 Recommended
4月 18, 2022
事件是在编程时系统内发生的动作或者发生的事情,系统响应事件后,如果需要,我们可以用事件处理器(event handler)对事件做出回应。浏览器中常见的可以触发事件的时机比如有:
- 用户在某个元素上点击鼠标或悬停光标时。
- 用户在键盘中按下某个按键时。
- 用户调整浏览器的大小或者关闭浏览器窗口时。
- 一个网页停止加载时。
- 提交表单时。
- 播放、暂停、关闭视频时。
- 发生错误时。
事件处理程序 #
事件处理程序(也叫监听器)是一个函数,有多种方式可以触发(fire、trigger、dispatch)事件。
HTML 内原生方式 #
也就是在 HTML 文本中原生的方式。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Example</title>
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js"
integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI="
crossorigin="anonymous"></script>
</head>
<body>
<div id="root">
<button type="button" onclick='change();'>触发</button>
<br />
<div id="output"></div>
</div>
</body>
<script>
const btn = document.querySelector('button');
function random(number) {
return Math.floor(Math.random()*(number+1));
}
function change(e) {
const rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
document.body.style.backgroundColor = rndCol;
output(JSON.stringify(e));
}
function output(msg) {
$("#output").append("<p>" + msg + "</p>");
}
</script>
</html>
运行这段代码你会发现打印出来的是 undefined
,而其他的是 {"isTrusted":true}
,除非像 onclick='change(e)'
参数中传入参数 e
。并且这种方式被认为是不好的做法,因为一个按钮还好,但是按钮多了的话就会导致维护非常艰难。
另外 HTML 事件处理方式还存在时差问题,当按钮显示出来,script 却没有下载完毕,用户已经按下按钮时,就会报错。需要使用 try catch 额外处理。
<button type="button" onclick='try{change();}catch(ex){}'>触发</button>
DOM 0 级事件处理 #
即在 JavaScript 代码中通过对元素的 onclick
属性进行注册。现在习惯上将早期未形成标准的试验性质的初级阶段的 DOM 称为 DOM 0 级。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Example</title>
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js"
integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI="
crossorigin="anonymous"></script>
</head>
<body>
<div id="root">
<button type="button">触发</button>
<br />
<div id="output"></div>
</div>
</body>
<script>
const btn = document.querySelector('button');
function random(number) {
return Math.floor(Math.random()*(number+1));
}
btn.onclick = function(e) {
const rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
document.body.style.backgroundColor = rndCol;
output(JSON.stringify(e));
}
function output(msg) {
$("#output").append("<p>" + msg + "</p>");
}
</script>
</html>
通过 DOM 0 级的实现不难发现,只能给 onclick
属性赋值一次,第二次的话就覆盖了,即不能给一个元素添加两个事件。还有一个问题是不能控制元素的事件流,这点下面的事件流章节会详细说。
DOM 2 级事件处理 #
即中 JavaScript 代码中通过 addEventListener
函数添加事件监听器。另外这种方式可以给一个事件注册多个事件处理器,他们在被触发后会按照注册的顺序执行。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Example</title>
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js"
integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI="
crossorigin="anonymous"></script>
</head>
<body>
<div id="root">
<button type="button">触发</button>
<br />
<div id="output"></div>
</div>
</body>
<script>
const btn = document.querySelector('button');
function random(number) {
return Math.floor(Math.random()*(number+1));
}
function change(e) {
const rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
document.body.style.backgroundColor = rndCol;
output(JSON.stringify(e));
}
function record(e) {
console.log("record click");
}
function output(msg) {
$("#output").append("<p>" + msg + "</p>");
}
btn.addEventListener('click', change);
btn.addEventListener('click', record);
</script>
</html>
通过 addEventListener
添加的事件只能用 removeEventListener
来移除,移除时传入的参数与添加事件使用的参数相同,通过 addEventListener
添加的匿名函数无法删除。
btn.removeEventListener('click', change);
IE 事件处理 #
IE 事件处理程序中与 DOM 2 级事件处理程序相似,即有 attachEvent()
和 detachEvent()
,使用方法也基本一样。
跨浏览器的事件处理 #
为了跨浏览器同时支持 IE 和 DOM 2 级事件处理器,有时需要封装两个方法:addHandler()
和 removeHandler()
,使用逻辑也和上面两个一样。
var EventUtil = {
addHandler: 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;
}
},
removeHandler: 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;
}
},
};
事件流 #
对上面的几种事件触发方式有了一定认识后,现在进一步看看事件流。参考下面这段代码,我把 onclick
属性放到了 div
里面,程序还是可以正常执行,即 div
标签也响应了事件。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Example</title>
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js"
integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI="
crossorigin="anonymous"></script>
</head>
<body>
<div id="root" onclick='change();'>
<button type="button">触发</button>
<br />
<div id="output"></div>
</div>
</body>
<script>
const btn = document.querySelector('button');
function random(number) {
return Math.floor(Math.random()*(number+1));
}
function change(e) {
const rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
document.body.style.backgroundColor = rndCol;
output(JSON.stringify(e));
}
function output(msg) {
$("#output").append("<p>" + msg + "</p>");
}
</script>
</html>
这个原因就是事件流的存在,事件流描述了页面接收事件的顺序。目前有两种方向的实现,一种是事件冒泡,一种是事件捕获。
事件冒泡(bubble) #
当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。参考下面这个例子:
代码为:
<style>
#post-event * {
margin: 10px;
border: 1px solid #f66;
}
</style>
<div id="post-event">
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
</div>
现代所有浏览器都默认使用事件冒泡的顺序,冒泡事件从目标元素开始向上冒泡。通常,它会一直上升到 <html>
,然后再到 document
对象,有些事件甚至会到达 window
,它们会调用路径上所有的处理程序。
事件捕获(capturing) #
与事件冒泡相反,事件捕获的意思是最顶端的节点应该最先收到事件,而最具体的节点应该最后收到事件,事件捕获实际上是为了在事件到达最终目标前拦截事件。
现代所有浏览器也都支持事件捕获,但通常没有开启,只有特殊情况下才需要使用事件捕获。将 addEventListener
函数的第三个参数设置为 true
可开启事件捕获机制。
三个阶段 #
DOM 2 事件标准描述了事件流的三个阶段:
- 事件捕获阶段
- 事件目标阶段
- 事件冒泡阶段
事件委托 #
事件委托(event delegation)利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件,它是一种思想或设计模式,不是机制。例如 click 事件冒泡到 document,这意味着可以为整个页面指定一个 onclick 事件处理程序,而不用为每个可点击元素分别指定事件处理程序。
事件委托特别适合为父标签添加一个事件处理器,而不需要为每一个子标签单独添加事件处理器。并且可以和 dataset 结合句使用为每一个子标签标记不同的行为(behavior)。
另外实现事件委托的过程中如果子元素的数量过多、逻辑过于复杂可能需要考虑性能、用户体验问题。
事件对象 #
Event #
使用 Event
类可以创造一个事件,通常用于自定义事件。而事件处理器的参数就是一个 event 对象,它有一些非常有用的属性和方法,举几个例子。
Target #
只读属性 event.target
是触发事件的 DOM 元素的引用,可以用来实现事件委托。currentTarget
与 target
不同,它标识当事件沿着 DOM 触发时事件的当前目标,它总是指向事件绑定的元素,而 target
则是事件触发的元素。
<style>
#post-event * {
margin: 10px;
border: 1px solid #f66;
}
</style>
<div id="post-event">
<form onclick="output(event)">FORM
<div onclick="output(event)">DIV
<p onclick="output(event)">P</p>
</div>
</form>
<script>
function output(event){
alert("target: " + event.target.tagName + " currentTarget: " + event.currentTarget.tagName);
event.target.style.visibility = "hidden"
}
</script>
</div>
尝试执行上面的代码体会一下。
preventDefault #
方法 preventDefault()
可以跳过一个事件默认的处理程序,并且继续事件流的传播。
function change(e) {
e.preventDefault()
}
stopPropagation #
任意事件处理程序都可以决定事件已经被完全处理,并停止事件传播。用于停止传播的方法是 event.stopPropagation()
,因为会中断默认的层次传播,可能会对其他组件的事件造成影响,一般情况不会使用。stopPropagation()
既可以中断事件委托,也可以中断事件冒泡。
stopImmediatePropagation #
event.stopImmediatePropagation()
除了具有 stopPropagation()
的功能外,还会中断同一事件的其他处理程序,而后者不会。
EventTarget #
EventTarget 是一切的根节点,以便让所有的 DOM 节点都支持事件,Element
,document
和 window
是最常见的 EventTarget 对象,所以其他的子元素都是一层层继承下来的。作为一个抽象类,它实现了事件管理最基本的几个方法:
addEventListener #
将指定的监听器注册到 EventTarget
对象上。
addEventListener(type, listener);
addEventListener(type, listener, options);
addEventListener(type, listener, useCapture);
removeEventListener #
删除曾经使用 addEventListener()
添加的事件。
removeEventListener(type, listener);
removeEventListener(type, listener, options);
removeEventListener(type, listener, useCapture);
dispatchEvent #
方法 dispatchEvent()
向一个指定的事件目标派发一个事件, 并以合适的顺序同步调用目标元素相关的事件处理函数,通常用于模拟事件。
let event = new Event("click")
dispatchEvent(event)
需要注意参数 event
不是字符串,而是一个 Event
对象。
事件类型 #
通过上面的 HTML 原生方式和 DOM 0 级可以看到,这两种使用 on
前缀来绑定事件,而 DOM 2 级事件处理使用 addEventListener
来绑定事件。在现代 JavaScript 编程中来说,通常推荐后面这种方式,可以为一个事件添加多个处理函数,并且与 HTML 代码可以解耦,更加容易维护。
下面按类型列举一些具体常用的事件,这会帮助我们对浏览器里能做的事情有一个初步认识。详细的列表可以参考 MDN Events 文档,W3C 文档 也有。
用户界面事件(UIEvent) #
resize #
文档视图调整大小时会触发 resize 事件。
dragstart/dragend #
关于拖拽事件的详细内容可以访问 https://zh.javascript.info/mouse-drag-and-drop
scroll #
文档视图或者一个元素在滚动时,会触发元素的 scroll
事件。
select #
选中文本时。
load #
error #
unload #
鼠标事件(MouseEvent) #
mousedown/mouseup #
在元素上点击/释放鼠标按钮。
mouseover/mouseout #
鼠标指针从一个元素上移入/移出。
mouseenter/mouseleave #
与上面 mouseover
不同的是, mouseenter
不会冒泡,也就是说当指针从它的子层物理空间移到它的物理空间上时不会触发。
mousemove #
当指针设备 ( 通常指鼠标 ) 在元素上移动时,mousemove 事件被触发。
click #
如果使用的是鼠标左键,则在同一个元素上的 mousedown
及 mouseup
相继触发后,触发该事件。
contextmenu #
在鼠标右键被按下时触发。
document.addEventListener("contextmenu", (event: MouseEvent) => {
event.preventDefault()
})
通过监听 contextmenu
事件,然后使用 preventDefault
来阻止浏览器右键的原生菜单,很多 Electron App、画图 App 都会通过这种方式自定义右键菜单。
dblclick #
在短时间内双击同一元素后触发。如今已经很少使用了。
滚轮事件(WheelEvent) #
wheel #
鼠标滚轮触发的事件。
指针事件 #
和鼠标事件类似的还有指针事件(pointer events),是一种用于处理各种输入设备(触控笔、触摸屏、鼠标)的输入信息的现代化解决方案。例如,指针事件可以支持轻触屏幕可以进行多点触控。
键盘事件(KeyboardEvent) #
keydown/keypress/keyup #
当按下和松开一个按键时,会按照上面这个顺序触发事件。
输入事件 #
input #
当一个 <input>
、<select>
、<textarea>
元素的 value 被修改时,触发 input 事件。
change #
与 input 事件不同,change 事件仅在提交时触发。change 事件属于 HTMLElement 类。
焦点事件(FocusEvent) #
focus #
当元素获得焦点时触发。
blur #
当元素失去焦点时触发。
手势事件 #
参考 #
https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Building_blocks/Events