事件
事件流
三个阶段
- 事件捕获 => 到达目标 => 事件冒泡;
事件捕获
- 事件流从 document 触发;
- 依次向下传播至触发节点;
到达目标
- 事件流到达触发节点;
- 从事件捕获转向事件冒泡;
事件冒泡
- 事件流从最深的节点触发;
- 依次向上传播;
事件处理程序
HTML 事件处理程序
- 全局作用域;
- this 指向元素本身;
- 冒泡阶段处理;
<input type="button" value="Click Me" onclick="console.log(this.value)"> // Click me
DOM0 事件处理程序
element.event = ()=>{...};- 作用域为元素所在作用域;
- 冒泡阶段处理;
let btn = document.getElementById("myBtn");
btn.onclick = function () {
console.log(this.id); // "myBtn"
};
btn.onclick = null;
DOM2 事件处理程序
DOM2 事件处理程序
- 一个事件可以添加多个事件处理程序, 根据添加顺序触发;
- addEventListener 添加的事件只能通过 removeEventListener 移除, 且参数相同, 意味着匿名函数无法移除;
- EventTarget.prototype.addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: AddEventListenerOptions): void 实例方法: 添加事件处理程序;
- EventTarget.prototype.removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: EventListenerOptions): void 实例方法: 移除事件处理程序;
let btn = document.getElementById("myBtn");
let handler = function () {
console.log(this.id);
};
btn.addEventListener("click", handler, false);
btn.removeEventListener("click", handler, false); // 有效果!
AddEventListenerOptions
- AddEventListenerOptions.capture?: boolean: 是否在捕获阶段处理事件, 默认为 false (冒泡阶段);
- AddEventListenerOptions.once?: boolean: 是否只触发一次, 默认为 false;
删除事件处理程序
原因
- 删除节点其事件处理程序不一定被垃圾回收;
- 页面卸载其事件处理程序不一定被垃圾回收;
删除方法
- 手动调用 removeEventListener 方法;
事件对象 event
event api
- Event.prototype.type: string 实例属性: 标记触发事件类型;
- Event.prototype.target: EventTarget 实例属性: 引起事件的元素;
- Event.prototype.currentTarget: EventTarget 实例属性: 处理事件时所在元素;
- Event.prototype.eventPhase: number 实例属性: 确定事件流当前所处的阶段 (1=捕获阶段, 2=到达目标, 3=冒泡阶段);
- Event.prototype.bubbles: boolean 实例属性: 事件是否冒泡;
- Event.prototype.cancelable: boolean 实例属性: 事件是否可取消默认行为;
- Event.prototype.defaultPrevented: boolean 实例属性: 是否已调用 preventDefault();
- Event.prototype.preventDefault(): void 实例方法: 阻止特定事件的默认动作;
- Event.prototype.stopPropagation(): void 实例方法: 阻止事件流在 DOM 结构中传播, 取消后续捕获或冒泡;
- Event.prototype.stopImmediatePropagation(): void 实例方法: 阻止事件流传播并阻止同类型事件处理程序执行;
- Event.prototype.composed: boolean 实例属性: 事件是否可以穿过 Shadow DOM 边界;
- Event.prototype.timeStamp: number 实例属性: 事件创建时的时间戳;
- Event.prototype.isTrusted: boolean 实例属性: 事件是否由用户操作触发;
target 和 currentTarget 属性
- target 为引起事件的元素;
- currentTarget 为处理事件时所在元素;
- this 对象始终等于 currentTarget;
document.body.onclick = function (event) {
// body 为处理事件时所在元素
console.log(event.currentTarget === document.body); // true
console.log(this === document.body); // true
// button 引起的 click 事件, 但 button 无 click 事件处理程序, 冒泡到 body 上处理
console.log(event.target === document.getElementById("myBtn")); // true
};
事件类型
用户界面事件
- load: 整个页面加载完成后于 windows 对象触发;
- unload: 文档卸载后于 windows 对象触发;
- resize: 浏览器缩放于 windows 对象触发;
- scroll: 元素滚动于 windows 对象触发;
焦点事件
- focus: 元素获得焦点触发, 不冒泡;
- focusin: 元素获得焦点触发, 冒泡;
- blur: 元素失去焦点触发, 不冒泡;
- focusout: 元素失去焦点触发, 冒泡;
HTML5 事件
- contextmenu: 显示菜单栏触发, 冒泡;
- beforeunload: 在页面卸载前于 window 对象触发;
- DOMContentLoaded: 在 DOM 树构建完成后于 window/document 对象触发;
- pageshow: load 事件之后, 页面显示时于 window 触发;
- pagehide: 页面卸载后, unload 前于 window 触发;
- hashchange: URL hash 值改变后于 window 触发;
鼠标事件
- click: 点击鼠标左键或回车键触发, 冒泡;
- dblclick: 双击鼠标左键触发, 冒泡;
- mousedown: 按下任意鼠标键触发, 冒泡;
- mouseenter: 鼠标从元素外到元素内触发, 不冒泡;
- mouseleave: 鼠标从元素内到元素外触发, 不冒泡;
- mousemove: 鼠标从元素内移动触发, 不冒泡;
- mouseover: 鼠标从元素外到元素内触发, 冒泡;
- mouseout: 鼠标从元素内到元素外触发, 冒泡;
- mouseup: 释放鼠标键触发, 冒泡;
- mousewheel: 鼠标滚动触发;
- MouseEvent.prototype.clientX: number 实例属性: 相对于视口的 x 坐标;
- MouseEvent.prototype.clientY: number 实例属性: 相对于视口的 y 坐标;
- MouseEvent.prototype.pageX: number 实例属性: 相对于页面的 x 坐标;
- MouseEvent.prototype.pageY: number 实例属性: 相对于页面的 y 坐标;
- MouseEvent.prototype.screenX: number 实例属性: 相对于屏幕的 x 坐标;
- MouseEvent.prototype.screenY: number 实例属性: 相对于屏幕的 y 坐标;
- MouseEvent.prototype.shiftKey: boolean 实例属性: 是否按住 shift 键;
- MouseEvent.prototype.ctrlKey: boolean 实例属性: 是否按住 ctrl 键;
- MouseEvent.prototype.altKey: boolean 实例属性: 是否按住 alt 键;
- MouseEvent.prototype.metaKey: boolean 实例属性: 是否按住 meta 键;
- MouseEvent.prototype.button: number 实例属性: 鼠标按键 (0=左键, 1=中键, 2=右键);
- MouseEvent.prototype.buttons: number 实例属性: 当前按下的鼠标按键位掩码;
- MouseEvent.prototype.relatedTarget: EventTarget | null 实例属性: 关联元素 (mouseover/mouseout);
- MouseEvent.prototype.wheelDelta: number 实例属性: 鼠标滚动数值;
客户端坐标和页面坐标的关系
- 未滚动时, 两者相同;
- 滚动时, 客户端坐标 + body.scrollXXX = 页面坐标;
键盘事件
- keydown: 按键触发, 持续按键重复触发;
- keypress: 按键并产生字符触发, 持续按键重复触发;
- keyup: 释放按键触发;
- textInput: 在可编辑区域且新字符插入时触发;
- KeyboardEvent.prototype.key: string 实例属性: 按键对应的字符串值;
- KeyboardEvent.prototype.code: string 实例属性: 按键的物理代码;
- KeyboardEvent.prototype.keyCode: number 实例属性: 键盘按键键码;
- KeyboardEvent.prototype.charCode: number 实例属性: 对应按键字符编码;
- KeyboardEvent.prototype.shiftKey: boolean 实例属性: 是否按住 shift 键;
- KeyboardEvent.prototype.ctrlKey: boolean 实例属性: 是否按住 ctrl 键;
- KeyboardEvent.prototype.altKey: boolean 实例属性: 是否按住 alt 键;
- KeyboardEvent.prototype.metaKey: boolean 实例属性: 是否按住 meta 键;
内存与性能
事件委托
事件委托
- 利用事件冒泡;
- 基于 target 属性 和 switch/if 语句;
- 使用一个事件处理程序管理一类事件;
let list = document.getElementById("myLinks");
list.addEventListener("click", (event) => {
let target = event.target;
switch (target.id) {
case "doSomething":
document.title = "I changed the document's title";
break;
case "goSomewhere":
location.href = "http:// www.wrox.com";
break;
case "sayHi":
console.log("hi");
break;
}
});
适合事件
- click;
- mousedown;
- mouseup;
- keydown;
- keypress;
- keyup;
优点
- 减少页面所需内存;
- 动态绑定, 减少代码量;
模拟事件
模拟鼠标事件
let btn = document.getElementById("myBtn");
// 创建 event 对象
let event = document.createEvent("MouseEvents");
// 初始化 event 对象
event.initMouseEvent(
"click",
true,
true,
document.defaultView,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null,
);
// 触发事件
btn.dispatchEvent(event);
模拟键盘事件
let textbox = document.getElementById("myTextbox"),
event;
// 按照 DOM3 的方式创建 event 对象
if (document.implementation.hasFeature("KeyboardEvents", "3.0")) {
event = document.createEvent("KeyboardEvent");
// 初始化 event 对象
event.initKeyboardEvent("keydown", true, true, document.defaultView, "a", 0, "Shift", 0);
}
// 触发事件
textbox.dispatchEvent(event);
模拟其他事件
let event = document.createEvent("HTMLEvents");
event.initEvent("focus", true, false);
target.dispatchEvent(event);
自定义事件
// DOM3
let div = document.getElementById("myDiv"),
event;
div.addEventListener("myevent", (event) => {
console.log("DIV: " + event.detail);
});
document.addEventListener("myevent", (event) => {
console.log("DOCUMENT: " + event.detail);
});
if (document.implementation.hasFeature("CustomEvents", "3.0")) {
event = document.createEvent("CustomEvent");
event.initCustomEvent("myevent", true, false, "Hello world!");
div.dispatchEvent(event);
}
最佳实践
节流和防抖
节流
- 固定时间段, 最多只能运行一次;
const throttle = (fun, wait) => {
let timeoutID = null;
return function () {
const context = this;
const args = arguments;
if (timeoutID) return;
timeoutID = setTimeout(() => {
fun.apply(context, args);
timeoutID = null;
}, wait);
};
};
防抖
- 固定时间段内, 只有最后一次操作被执行;
const debounce = (fun, wait) => {
let timeoutID = null;
return function () {
const context = this;
const args = arguments;
if (timeoutID) clearTimeout(timeoutID);
timeoutID = setTimeout(function () {
fun.apply(context, args);
}, wait);
};
};