详细梳理 javascript 事件的方方面面……
一 事件流
所谓事件流,即指页面接收事件的顺序。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="myDiv">click me</div>
<script>
var myDiv = document.getElementById('myDiv');
myDiv.onclick = function(event) {
console.log('myDiv');
}
var body = document.getElementsByTagName('body')[0]
body.onclick = function() {
console.log('body');
}
</script>
</body>
</html>
1. 事件冒泡与捕获
浏览器早期发展中,IE 和 Netscape 提出了不同的事件流,分别如下。
如上 html 代码,事件冒泡是指,点击 div 时,click 事件首先发生在 div 上,然后 click 事件沿着 DOM 树向上传播,每一级节点都会发生 click 事件:
div -> body -> html -> document -> window
事件捕获,与“事件冒泡”相反, 点击 div 时,click 事件首先发生在 window 上, 沿着 DOM 树向下传播:
window -> document -> html -> body -> div
2. 事件流
“DOM2 级事件”规定事件流包括三个阶段:事件捕获阶段,处于目标阶段,事件冒泡阶段。处于目标阶段,可看作是冒泡的一部分。整个事件流程如下:
window -> document -> html -> body -> div -> body -> html -> document -> window
该流程主流浏览器都支持,IE8不支持。 “DOM2 级事件”规范要求捕获阶段不触发事件,但实际上主流浏览器(除IE8)都支持在捕获阶段触发事件。一般情况下,建议只在冒泡阶段触发事件,这样可以更大程度的兼容各种浏览器。
二 事件处理程序
1. html 事件处理程序
<input type="button" value="" onclick="doSomething('param')">
方法中不能使用未经转义的HTML语法字符,比如 "param" 应写为 " param "
这种方式的缺点:点击时方法不一定加载完成,从而报错;作用域链(this)在不同浏览器中导致不同结果;html 和 javascript 紧耦合。
根据如今的WEB标准,结构(html)应与行为(javascript)分开,这种写法已不推荐使用。
2. DOM0 级事件处理程序
var btn = document.getElementById("myBtn");
btn.onclick = function(){ console.log(1) } // 添加事件
btn.onclick = function(){ console.log(2) } // 添加事件
btn.onclick = null; // 删除事件
这种方式事件会在冒泡阶段处理。 如果一个元素绑定多个相同的事件,则后面的会覆盖前面的。如上代码,点击后将只会执行 console.log(2)。
3. DOM2 级事件处理程序
var btn = document.getElementById('myBtn');
var handler = function() {
console.log(this.id);
}
btn.addEventListener('click', handler, false); // 添加事件
btn.removeEventListener('click', handler, false);
handler
三个参数:1 事件名;2 事件处理方法;3 boolean 值,true 表示在捕获阶段触发,false 表示在冒泡阶段触发。 通过 addEventListener 添加的事件,只能通过 removeEventListener 删除,并且参数要相同,因此处理事件的方法不能使用匿名函数的方式,必须使用具名函数,如上handler方法。 与 DOM0 不同,该方式可以绑定多个事件,按顺序先后执行。
4. IE 事件处理程序
var btn = document.getElementById('myBtn');
var handler = function() {
console.log(this.id);
}
btn.attachEvent('onclick', handler); // 添加事件
btn.detachEvent('onclick', handler); // 删除事件
只在冒泡阶段触发。 在全局作用域中运行,即 this === window 。 attachEvent 可绑定多个相同事件,但与 addEventListener 不同的是,以相反的顺序执行。 使用 detachEvent 删除,传入相同的参数,不能使用匿名函数,否则删除不掉。
5. 跨浏览器事件处理代码
/** 只关注冒泡阶段 */
var EventUtil = {
addHandler: function(element, type, handler) {
if (element.addEventListener) {
// DOM2 添加事件
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
// IE 添加事件
element.attachEvent('on' + type, handler);
} else {
// DOM0 添加事件
element['on' + type] = handler;
}
},
removeHandler: function(element, type, handler) {
if (element.removeEventListener) {
// DOM2 删除事件
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
// IE 删除事件
element.detachEvent('on' + type, handler);
} else {
// DOM0 删除事件
element['on' + type] = null;
}
}
}
三 事件对象
| 对象属性/方法 | 类型 | 说明 |
|---|---|---|
| bubbles | Boolean | 是否冒泡 |
| cancelable | Boolean | 是否可以取消事件的默认行为 |
| currentTarget | Element | 事件当前正在处理的元素 |
| defaultPrevented | Boolean | 为 true 时表明已调用了 preventDefault() (DOM3) |
| detail | Integer | 与事件相关的细节信息 |
| eventPhase | Integer | 1: 捕获阶段;2:处于目标阶段;3:冒泡阶段 |
| type | String | 被触发的事件类型 |
| target | Element | 事件的目标 |
| srcElement | Element | IE,事件的目标,与 target 同功能 |
| trusted | Boolean | 为 true 表示事件是浏览器生成的,为 false 表示由 javascript 创建的(DOM3) |
| preventDefault() | Function | 取消事件的默认行为 |
| returnValue | Boolean | IE,设为 false,取消事件的默认行为,与 preventDefault() 同功能 |
| stopPropagation() | Function | 取消事件进一步的捕获或冒泡 |
| cancelBubble | Boolean | IE,设为 true,阻止冒泡,与 stopPropagation() 同功能 |
| view | AbstractView | 与事件关联的抽象视图,等同于发生事件的 window 对象 |
IE 事件对象只有 type / srcElement / returnValue / cancelBubble 四个属性
注: srcElement / returnValue / cancelBubble 虽然各种资料说明被 IE 支持,实测至少 Chrome(v89) / Firefox(v88) / Safari(v14) 均支持。
跨浏览器事件对象代码处理
var EventUtil = {
getEvent: function(e) {
return e ? e : window.e;
},
getTarget: function(e) {
return e.target || e.srcElement;
}
preventDefault: function(e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
},
stopPropagation: function(e) {
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelBubble = true;
}
}
}
四 事件类型
1. UI & HTML 事件
| 事件 | 说明 |
|---|---|
| load | 当页面完全加载后(包括所有图像,javascript 文件,css 文件等外部资源),触发 window 上面的 load 事件 |
| DOMContentLoaded | 在形成完整的 DOM 树之后触发,不管 javascript 文件,css 文件等外部资源是否加载完成 |
| unload | 文档被完全卸载后触发。比如用户从一个页面切换到另一个页面。这个事件一般用来清除引用,避免内存泄漏。在<body>或者 window 上使用 |
| beforeunload | 在浏览器卸载页面之前触发,可以用来取消卸载。IE / Firefox 设置 event.returnValue = "message", Chrome / Safari return "message" , 可弹框提示信息 |
| resize | 浏览器窗口变化时在 window 上触发。一般浏览器窗口变化 1px 时就会触发,Firefox 浏览器只会在窗口不变时触发 |
| scroll | 滚动带滚动条的元素,在该元素中触发。可通过元素的 scrollLeft / scrollTop 来监听滚动 |
| contextmenu | 鼠标右键调出上下文菜单。 |
| readystatechange | 有个readyState属性,用来提供元素加载阶段的信息。只有 IE / FireFox / Opera 支持 |
| hashchange | URL参数列表变化时触发,必须添加到 window 对象上 |
2. 焦点事件
| 事件 | 说明 | 是否支持冒泡 |
|---|---|---|
| focus | 元素获得焦点时触发 | 否 |
| blur | 元素失去焦点时触发 | 否 |
3. 鼠标与滚轮事件
| 事件 | 说明 | 是否支持冒泡 |
|---|---|---|
| click | 单击鼠标主按钮 | 是 |
| dbclick | 双击鼠标主按钮 | 是 |
| mousedown | 按下任意鼠标按钮 | 是 |
| mouseup | 松开鼠标按钮 | 是 |
| mouseenter | 鼠标从元素外首次移动到元素内 | 否 |
| mouseleave | 鼠标移动到元素范围外 | 否 |
| mousemove | 鼠标在元素内部移动 | 是 |
| mouseout | 鼠标在元素内部,然后将其移入另一个元素 | 是 |
| mouseover | 鼠标在元素外部,然后将其移入另一个元素边界之内 | 是 |
| mousewheel | 鼠标滚轮事件(向前滚属性 wheelDelta = 120 * n),反之取负 | 是 |
| DOMMouseScroll | 鼠标滚轮事件(FireFox)(向前滚属性 detail = -3 * n),反之取正 | 是 |
| selectstart | 鼠标选中页面内容事件,与此相关的api:window.getSelection() | 是 |
1). 鼠标位置
| 鼠标位置属性 | 说明 |
|---|---|
| clientX / clientY | 相对于浏览器位置 |
| pageX / pageY | 在浏览器页面中的位置(包含有滚动条的情况) |
| screenX / screenY | 相对于显示屏的位置 |
2). 修改键 点击鼠标的同时按下某些键盘键,可能会有别的处理。一般修改键指:Shift / Ctrl / Alt / Meta(Windows 键),对应在事件的属性分别为 shiftKey / ctrlKey / altKey / metaKey。如果相应的键处于按下的状态,则值为 true。如 event.shiftKey === true。
4. 键盘与文本事件
| 事件 | 说明 |
|---|---|
| keydown | 按下键盘上任意键触发,按住不放重复触发 |
| keypress | 按下键盘上字符键(包括 ESC)触发,按住不放重复触发 |
| keyup | 释放键盘上的键 |
| textInput | 文本插入文本框之前触发 |
1). 键盘事件 执行顺序:keydown -> keypress -> keyup。 除了按下字符键,按下能够插入或删除字符的键也会触发 keypress,并且 keypress 事件包含一个 charCode 属性,对应键代表的 ASCII 编码。
2). textInput
textInput, 在可编辑区域输入字符时触发。与keypress的区别在于:
- 只有在可编辑区才能触发 textInput ,可以获得焦点的元素都可以触发 keypress 。
- 只会在输入实际字符时才能触发 textInput ,而能够影响字符显示的键也能触发 keypress, 比如退格键。
textInput 事件的属性:
| 属性 | 说明 |
|---|---|
| data | 用户输入的字符 |
| inputMethod | 0-不确定是怎么输入的;1-键盘输入;2-粘贴输入;3-拖放输入;4-IME输入;5-通过表单选择输入;6-手写输入;7-语音输入;8-几种方法组合输入;9-脚本输入。(只有IE支持) |
5. 变动事件
| 事件 | 说明 |
|---|---|
| DOMSubtreeModified | DOM 结构中发生任何变化时触发 |
| DOMNodeInserted | 在一个节点作为子节点被插入到另一个节点中时触发 |
| DOMNodeRemoved | 在节点从其父节点中被移除时触发 |
6. 触摸事件
| 事件 | 说明 |
|---|---|
| touchstart | 手指触摸屏幕时触发,即使已经有个手指放在了屏幕上 |
| touchmove | 手指在屏幕上滑动时连续触发。发生期间,调用 preventDefault() 可以阻止滚动 |
| touchend | 手指从屏幕上移开时触发 |
| touchcancel | 系统停止跟踪触摸时触发 |
触摸事件包含的一般属性:bubbles / cancelable / view / clientX / clientY / screenX / screenY / detail / altKey / shiftKey / ctrlKey / metaKey 。
除了这些属性外,触摸事件有三个独特的属性:
| 触摸事件属性 | 说明 |
|---|---|
| touches | 当前跟踪的触摸操作的 Touch 对象的数组 |
| targetTouches | 特定于事件目标的 Touch 对象数组 |
| changeTouches | 自上次触摸以来发生了什么改变的 Touch 对象的数组 |
这三个属性同时又包含一下属性:clientX / clientY / identifier / pageX / pageY / screenX / screenY / target
7. 手势事件
| 事件 | 说明 |
|---|---|
| gesturestart | 一个手指已经按在屏幕上,另一个手指又触摸屏幕时触发 |
| gesturechange | 屏幕上的任何一个手指位置发生变化时触发 |
| gestureend | 任何一个手指从屏幕上移开上触发 |
手势事件都有一般鼠标事件的属性:bubbles / cancelable / view / clientX / clientY / screenX / screenY / detail / altKey / shiftKey / ctrlKey / metaKey / rotation / scale 。
五 模拟事件(自定义事件)
图中 createEvent 方法被定义为已过时的方法,被 CustomEvent 函数替代:
var event = new CustomEvent(typeArg ,customEventInit)
typeArg 是 DOMString 代表事件的名称,一个表示 event 名字的字符串。customEventInit(可选):一个字典类型参数,有如下字段:
- detail, 可选的默认值是 null 的任意类型数据,是一个与 event 相关的值,可用来给事件传值。
- bubbles 一个布尔值,表示该事件能否冒泡。 来自 EventInit。注意:测试chrome默认为不冒泡。
- cancelable 一个布尔值,表示该事件是否可以取消。 来自 EventInit
// document 监听dog自定义事件
document.addEventListener('dog', function(e){
console.log('e', e);
})
// 定义dog事件, detail 可以传入参数
var event = new CustomEvent('dog', { detail: {a: true}, bubbles: true });
// 触发事件
document.dispatchEvent(event);
IE 不支持 CustomEvent 函数,以下是兼容性代码,可供参考:
// 兼容ie9-11方案
(function(){
try{
new window.CustomEvent('T');
}catch(e){
var CustomEvent = function(event, params){
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
};
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
}
})();