跳到主要内容

事件

事件流

三个阶段
  • 事件捕获;
  • 到达目标;
  • 事件冒泡;
事件捕获
  • 事件流从 document 触发;
  • 依次向下传播至触发节点;
到达目标
  • 事件流到达触发节点;
  • 从事件捕获转向事件冒泡;
事件冒泡
  • 事件流从最深的节点触发;
  • 依次向上传播;

事件处理程序

HTML 事件处理程序

语法格式
<input type="button" value="Click Me" onclick="console.log('Clicked')" />
调用脚本
  • 全局作用域;
  • this 指向元素本身;
<input type="button" value="Click Me" onclick="showMessage()"/>
<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 事件处理程序

创建事件处理程序
  • element.addEventListener(event,fn,useCapture);
    • useCapture:false 为默认值,表示冒泡阶段处理事件,true 则为捕获阶段;
  • 一个事件可以添加多个事件处理程序;
  • 根据添加顺序触发;
let btn = document.getElementById("myBtn");
btn.addEventListener(
"click",
() => {
console.log(this.id);
},
false
);

let btn = document.getElementById("myBtn");
btn.addEventListener(
"click",
() => {
console.log(this.id);
},
false
);
btn.addEventListener(
"click",
() => {
console.log("Hello world!");
},
false
);
移除事件处理程序
  • element.removeEventListener(event,fn,useCapture);
  • addEventListener 添加的事件只能通过 removeEventListener 移除,且参数相同,意味着匿名函数无法移除;
let btn = document.getElementById("myBtn");
let handler = function () {
console.log(this.id);
};
btn.addEventListener("click", handler, false);
btn.removeEventListener("click", handler, false); // 有效果!
触发时机
  • 默认在冒泡阶段触发;
  • 可通过过 useCapture 参数设置捕获阶段触发;
触发一次
  • 设置 once 为 true;
button.addEventListener(
"click",
() => {
console.log("clicked!");
},
{ once: true }
);

事件对象

event
let btn = document.getElementById("myBtn");
btn.onclick = function (event) {
console.log(event.type); // "click"
};
btn.addEventListener(
"click",
(event) => {
console.log(event.type); // "click"
},
false
);
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
};
type 属性
  • 标记触发事件类型;;
let btn = document.getElementById("myBtn");
let handler = function (event) {
switch (event.type) {
case "click":
console.log("Clicked");
break;
case "mouseover":
event.target.style.backgroundColor = "red";
break;
case "mouseout":
event.target.style.backgroundColor = "";
break;
}
};
preventDefault() 方法
  • 阻止特定事件的默认动作;
let link = document.getElementById("myLink");
link.onclick = function (event) {
event.preventDefault();
};
stopPropagation() 方法
  • 阻止事件流在 DOM 结构中传播,取消后续捕获或冒泡;
let btn = document.getElementById("myBtn");
btn.onclick = function (event) {
console.log("Clicked");
event.stopPropagation();
};
document.body.onclick = function (event) {
console.log("Body clicked");
};
eventPhase 属性
  • 确定事件流当前所处的阶段;
let btn = document.getElementById("myBtn");
// 到达目标
btn.onclick = function (event) {
console.log(event.eventPhase); // 2
};
// 捕获阶段
document.body.addEventListener(
"click",
(event) => {
console.log(event.eventPhase); // 1
},
true
);
// 冒泡阶段
document.body.onclick = (event) => {
console.log(event.eventPhase); // 3
};

事件类型

用户界面事件

  • load 事件:整个页面加载完成后于 windows 对象触发,对应 body 标签 onload 属性;
  • unload 事件:文档卸载后于 windows 对象触发,对应 body 标签 onunload 属性;
  • resize 事件:浏览器缩放于 windows 对象触发,对应 body 标签 onresize 属性;
  • scroll 事件:元素滚动于 windows 对象触发,对应 body 标签 onscroll 属性;

焦点事件

  • focus 事件:元素获得焦点触发,不冒泡;
  • focusin 事件:元素获得焦点触发,冒泡;
  • blur 事件:元素失去焦点触发,不冒泡;
  • focusout 事件:元素失去焦点触发,冒泡;

html5 事件

  • contextmenu 事件:显示菜单栏触发,冒泡;
  • beforeunload 事件:在页面卸载前于 window 对象触发;
  • DOMContentLoaded 事件:在 DOM 树构建完成后于 window/document 对象触发;
  • pageshow 事件:load 事件之后,页面显示时于 window 触发;
  • pagehide 事件:页面卸载后,unload 前于 window 触发;
  • hashchange 事件:URL hash 值改变后于 window 触发;

鼠标事件

常见事件
  • click 事件:点击鼠标左键或回车键触发,冒泡,对应 onclick 属性;
  • dblclick 事件:双击鼠标左键触发,冒泡;
  • mousedown 事件:按下任意鼠标键触发,冒泡;
  • mouseenter 事件:鼠标从元素外到元素内触发,不冒泡;
  • mouseleave 事件:鼠标从元素内到元素外触发,不冒泡;
  • mousemove 事件:鼠标从元素删移动触发,不冒泡;
  • mouseover 事件:鼠标从元素外到元素内触发,冒泡;
  • mouseout 事件:鼠标从元素内到元素外触发,冒泡;
  • mouseup 事件:释放鼠标键触发,冒泡;
  • mousewheel 事件:鼠标滚动触发;
    • wheelDelta 表示鼠标滚动数值;
客户端坐标
  • clientXY:相对于视口的坐标;
let div = document.getElementById("myDiv");
div.addEventListener("click", (event) => {
console.log(`Client coordinates: ${event.clientX}, ${event.clientY}`);
});
页面坐标
  • pageXY:相对于页面的坐标;
let div = document.getElementById("myDiv");
div.addEventListener("click", (event) => {
console.log(`Page coordinates: ${event.pageX}, ${event.pageY}`);
});
客户端坐标和页面坐标的关系
  • 未滚动时,两者相同;
  • 滚动时,客户端坐标 + body.scrollXXX = 页面坐标;
屏幕坐标
  • screenXY:相对于屏幕的坐标;
let div = document.getElementById("myDiv");
div.addEventListener("click", (event) => {
console.log(`Screen coordinates: ${event.screenX}, ${event.screenY}`);
});
修饰键
let div = document.getElementById("myDiv");
div.addEventListener("click", (event) => {
let keys = new Array();
if (event.shiftKey) {
keys.push("shift");
}
if (event.ctrlKey) {
keys.push("ctrl");
}
if (event.altKey) {
keys.push("alt");
}
if (event.metaKey) {
keys.push("meta");
}
console.log("Keys: " + keys.join(","));
});
相关元素
  • 针对 mouseover 和 mouseout 事件;
  • event.relatedTarge 属性指向相关元素;
鼠标按键
  • event.button 属性;
  • 0 鼠标左键,1 鼠标中键,2 鼠标右键;

键盘事件

常见事件
  • keydown 事件:按键触发,持续按键重复触发;
  • keypress 事件:按键并产生字符触发,持续按键重复触发;
  • keyup 事件:释放按键触发;
键码
  • keyCode 属性存储键盘按键键码;
let textbox = document.getElementById("myText");
textbox.addEventListener("keyup", (event) => {
console.log(event.keyCode);
});
字符编码
  • charCode 属性存储对应按键字符编码;
let textbox = document.getElementById("myText");
textbox.addEventListener("keypress", (event) => {
console.log(event.charCode);
});
textInput 事件
  • 在可编辑区域且新字符插入时触发;
let textbox = document.getElementById("myText");
textbox.addEventListener("textInput", (event) => {
console.log(event.data);
});

内存与性能

事件委托

事件委托
  • 利用事件冒泡;
  • 基于 type 属性 和 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);
}

最佳实践

优化 scroll

  • 通过 IntersectionObserve API 替换 scroll 事件;

节流和防抖

节流
  • 固定时间段,最多只能运行一次;
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);
};
};