第十四篇 事件 - 事件流、事件处理程序、事件委托(代理)
by caix in 深圳
事件
JavaScript 与 HTML 之间的交互通过事件实现
事件 就是用户或浏览器自身执行的某种动作,比如点击、加载,鼠标移入移出等等
可以使用侦听器来预订事件
事件流
DOM (文档对象模型) 结构 是一个树形结构,当一个 HTML元素 产生一个事件时,该事件会在 元素结点 与 根节点 之间按特定的顺序传播,路径所经过的节点都会收到该事件,这个传播过程可称为 DOM 事件流
DOM 规范规定了事件流分为三个阶段:事件捕获 => 达到目标元素 => 事件冒泡
其中事件捕获是最先发生的,最晚发生的是事件冒泡
总之 事件流 描述的是 网页中 中接收事件的顺序
过程
元素事件响应在 DOM树 中是从顶层的 Window开始 “流向” 目标元素,然后又从目标元素 “流向” 顶层的 Window
因为 Window对象 是直接面向用户的,那么用户触发一个事件,如点击事件,肯定是从 window对象 开始的,所以自然就是先捕获后冒泡
阶段
事件流 包括三个阶段: 1、事件捕获阶段 2、处于目标阶段 3、事件冒泡阶段
1、事件捕获阶段 捕获阶段是指事件响应从最外层的 Window 开始,逐级向内层前进,直到具体事件目标元素。在捕获阶段,不会处理响应元素注册的冒泡事件 该阶段的主要作用是捕获截取事件 2、处于目标阶段 当事件不断的传递直到目标节点的时候,最终在目标节点上触发这个事件,就是目标阶段。具体的元素本身 该阶段具有双重范围,即 捕获阶段的结束,冒泡阶段的开始 3、事件冒泡阶段 冒泡阶段与捕获阶段相反,事件的响应是从最底层开始一层一层往外传递到最外层的Window 主要作用是将目标元素绑定事件执行的结果返回给浏览器。 处理不同浏览器之间的差异,主要在该阶段完成 我们平时用的事件绑定就是利用的事件冒泡的原理
模型
事件传播的顺序对应浏览器的两种事件流模型:捕获型事件流 和 冒泡型事件流
1、冒泡型事件流 事件的传播是从最特定的事件目标到最不特定的事件目标。即从 DOM树 的叶子到根。【推荐】 2、捕获型事件流 事件的传播是从最不特定的事件目标到最特定的事件目标。即从 DOM树 的根到叶子 事件捕获的思想就是不太具体的节点应该更早接收到事件,而最具体的节点最后接收到事件
事件冒泡
微软提出了名为 事件冒泡(event bubbling) 的事件流,由内向外响应 1、阻止事件冒泡 W3C标准 event.stopPropagation(); 不支持 IE9 以下版本 IE独有 event.cancelBubble = true; 方法一 给子元素设置 .stopPropagation() function click(e){ alert("我是最内层的div") var event = e || window.event event.stopPropagation() event.cancelBubble = true // IE } event.stopPropagation() 则只阻止事件往上冒泡,不阻止事件本身 方法二 event.target == event.currentTarget 让触发事件的元素等于绑定事件的元素,也可以阻止事件冒泡 function click1(e) { var event = e || window.event; if (event.target == event.currentTarget) { alert("我是最外层的div"); return false } } 注意: 如果采用这种方法阻止事件冒泡,必须所有属于冒泡影响范围内的元素都采用这个方法 2、阻止事件默认行为 方法一 适用于普通浏览器 e.preventDefault() 最常用 <body> <a href="">test</a> <script> var a = document.querySelector('a') a.addEventListener('click', function(e) { e.preventDefault() => 适用于普通浏览器 }) </script> </body> 方法二 适用于低版本浏览器(IE6/7/8) e.returnValue <body> <a href="">test</a> <script> var a = document.querySelector('a') a.onclick = function(e) { e.returnValue => 适用于低版本浏览器 } </script> </body> 方法三 使用 return false 阻止默认行为 没有兼容性问题 但 return后面 代码不再执行 <body> <a href="">test</a> <script> var a = document.querySelector('a') a.onclick = function(e) { return false } </script> </body>
事件捕获(event capturing)
网景提出了名为 事件捕获(event capturing) 的事件流,由外向内响应
事件代理(事件委托)
事件代理就是利用 事件冒泡 或 事件捕获 的机制把一系列的内层元素事件绑定到外层元素
当一个循环达到几十次或者上百次,并且在其本身绑定事件,渲染的时候,给每个 dom 绑定事件,这样操作是很耗费性能的,这时我们就应该利用冒泡的机制和事件流的特性去把他的事件绑定在父亲本身,这样我们只需要绑定一个事件就能操作所有元素
事件代理 可以减少事件处理器的数量,提高 JS 脚本的性能, 这也是前端性能优化的一个重要方面
事件代理的作用
减少监听器数量,去除重复笨重的代码 <ul id="item-list"> <li>item1</li> <li>item2</li> <li>item3</li> <li>item4</li> </ul> let items = document.getElementById("item-list") 事件捕获实现事件代理 items.addEventListener("click",(e) => { consle.log(e.target.innerHtml) },true) 事件冒泡实现事件代理 items.addEventListener("click",(e) => { consle.log(e.target.innerHtml) },false)
<div id="box"> <input type="button" id="add" value="添加" /> <input type="button" id="remove" value="删除" /> <input type="button" id="move" value="移动" /> <input type="button" id="select" value="选择" /> </div> window.onload = function(){ var Add = document.getElementById("add"); var Remove = document.getElementById("remove"); var Move = document.getElementById("move"); var Select = document.getElementById("select"); Add.onclick = function(){ alert('添加'); }; Remove.onclick = function(){ alert('删除'); }; Move.onclick = function(){ alert('移动'); }; Select.onclick = function(){ alert('选择'); } } 上面实现的效果我就不多说了,很简单,4个按钮,点击每一个做不同的操作,那么至少需要4次dom操作,如果用事件委托,能进行优化吗 ? window.onload = function(){ var oBox = document.getElementById("box"); oBox.onclick = function (ev) { var ev = ev || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLocaleLowerCase() == 'input'){ switch(target.id){ case 'add' : alert('添加'); break; case 'remove' : alert('删除'); break; case 'move' : alert('移动'); break; case 'select' : alert('选择'); break; } } } } 适合用事件委托的事件 click,mousedown,mouseup,keydown,keyup,keypress 值得注意的是,mouseover 和 mouseout 虽然也有事件冒泡 但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易
事件处理程序
事件意味着用户或浏览器执行的某种动作。比如,单击(click)、加载(load)、鼠标悬停 (mouseover)。为响应事件而调用的函数被称为事件处理程序(或事件监听器)
事件处理程序的名字以“on”开头,且有很多方式来指定事件处理程序
HTML 事件处理程序
特定元素 支持的每个事件都可以使用事件处理程序的名字以 HTML 属性的形式来指定
将 JS 代码字符串赋值给 HTML 事件属性
<input type="button" onclick="console.log('clicked!')"/> 点击这个按钮后控制台会输出“clicked!”。这种交互能力是通过onclick属性指定 javascript代码 值来实现的 因为属性的值是 javascript 代码,所以不能以未转义的情况下使用HTML语法字符,如:和号(&)、双引号(")、小于号(<)、大于号(>) 为了避免使用HTML实体,可以使用单引号来代替双引号,或使用转义符号代替
在 HTML 中定义的时间处理程序可以包含精准的动作指令,也可以调用在页面其他地方定义的javascript脚本
<script> function showMessage() { console.log("Hello world!"); } </script> <button type="button" onclick="showMessage()">showMessage</button> 点击按钮时会调用 showMessage()函数 showMessage()函数 是单独在 <script> 元素中定义的,也可以在外部 js 文件中定义
以这种方式指定的事件处理程序有一些特殊的地方
1、首先,会创建一个函数来封装属性的值。这个 函数有一个特殊的局部变量 event,其中保存的就是 event 对象 <button type="button" onclick="console.log(event.type)">click me!</button> => 输出 “click” 2、有了这个对象,就不用开发者另外定义其他变量,也不用从包装函数的参数列表中去取了。在这个函数中,this 值相当于事件的目标元素 <button type="button" onclick="console.log(this.type)">click me!</button> => 输出 “click” 3、这个动态创建的包装函数还有一个特别有意思的地方,就是其作用域链被扩展了。在这个函数中, document 和元素自身的成员都可以被当成局部变量来访问。这是通过使用 with 实现的 : function() { with(document) { with(this) { // 属性值 } } } 这意味着事件处理程序可以更方便地访问自己的属性。下面的代码与前面的示例功能一样 <button type="button" onclick="console.log(type)"> click me!</button> => 输出 “click”
DOM0 级事件处理程序
将一个函数赋值给一个事件处理程序属性
指以 Javascript 形式在DOM对象上以事件动作(如:onclick、onmouseover 等)为属性名指定事件处理程序
用法: 1、获取元素引用 let btn = document.getElementById("myBtn"); 2、 添加事件 btn.onclick = function() { console.log("Hello world!"); }; 3、 移除事件 btn.onclick = null; 作用域: 在元素的作用域中运行。换句话说,事件处理程序中的 this 引用当前元素 使用 DOM0 方式为事件处理程序赋值时,所赋函数被视为元素的方法。因此,事件处理程 序会在元素的作用域中运行,即 this 等于元素。 this 可以访问元素的任何属性和方法 以这种方式添加事件处理程序是注册在事件流的 冒泡阶段 的 let btn = document.getElementById("myBtn"); btn.onclick = function() { console.log(this.id); // "myBtn" }; 移除事件处理程序: 通过将事件处理程序属性的值设置为 null,可以移除通过 DOM0 方式添加的事件处理程序 btn.onclick = null; // 移除事件处理程序 缺点: 只能添加一个事件处理程序
DOM2 级事件处理程序
DOM2 级事件处理程序为事件处理程序的赋值和移除定义了两个方法:addEventListener() 和 removeEventListener()。
这两个方法暴露在所有 DOM 节点上,它们接收 3 个参数
使用 DOM2 方式的主要优势是可以为同一个事件添加多个事件处理程序
let btn = document.getElementById("myBtn"); btn.addEventListener("click", () => { console.log(this.id); }, false); 与 DOM0 方式类似,这个事件处理程序同样在被附加到的元素的作用域中运行
<button id="myBtn">click me!</button> <script> let btn = document.getElementById("myBtn"); btn.addEventListener("click", () => { console.log(this.id); }, false); btn.addEventListener("click", () => { console.log("Hello world!"); }, false); </script> 使用 DOM2 方式的主要优势是可以为同一个事件添加多个事件处理程序
<div id="parent"> <div id="child"> I'm child </div> </div> <script> const parent = document.querySelector('#parent'); const child = document.querySelector('#child'); parent.addEventListener('click', function() { console.log('Im parent1'); }); parent.addEventListener('click', function() { console.log('Im parent2'); }, true); child.addEventListener('click', function() { console.log('Im child1'); }); child.addEventListener('click', function() { console.log('Im child2'); }, true); </script> 这里分别给id为parent和child的div元素添加了一个冒泡阶段事件和一个捕获阶段事件 结合上述DOM事件流关系图可以推断出:当点击childdiv元素时,会先在捕获阶段触发parent的捕获阶段点击事件,然后触发child的捕获阶段点击事件,然后在冒泡阶段先触发child的冒泡阶段点击事件,最后触发parent的冒泡阶段点击事件。 打印结果为: 1、Im parent2 2、Im child2 3、Im child1 4、Im parent1
通过 addEventListener()添加的事件处理程序只能使用 removeEventListener()并传入与添加时同样的参数来移除 这意味着使用addEventListener()添加的匿名函数无法移除 let btn = document.getElementById("myBtn"); btn.addEventListener("click", () => { console.log(this.id); }, false); btn.removeEventListener("click", () => { // 没有效果 console.log(this.id); }, false); 由于传给 removeEventListener()函数 和传给 addEventListener()函数 的第二个参数为同一引用函数,所以会起到效果移除事件处理程序。
addEventListener()
新的语法 target.addEventListener(type, listener [,{capture: Boolean, bubbling: Boolean, once: Boolean}]); type 表示监听事件类型的字符串 listener 当所监听的事件类型触发时,会接收到一个事件通知 options (可选) capture 表示listener会在该类型的事件捕获阶段传播到该EventTarget 时触发 once 表示listener在添加之后最多只调用一次。如果是 true, listener会在其被调用之后自动移除 passive 表示listener永远不会调用preventDefault()。如果listener仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告 <nav id="menu"><a href="https://juejin.cn">首页</a></nav> var nav = document.getElementById('menu') var link = nav.firstElementChild capture: 表示 listener 会在该类型的事件捕获阶段传播到该EventTarget 时触发 menu.addEventListener('click', function (e) { console.log('menu clicked!') }, { capture: true }) => menu clicked link.addEventListener('click', function (e) { e.preventDefault(); console.log('link clicked!') }, { capture: false }) => link clicked once: 表示listener在添加之后最多只调用一次 如果是 true,listener会在其被调用之后自动移除 link.addEventListener('click', function (e) { e.preventDefault(); console.log('link clicked!') }, { capture: false,once:true }) => 输出一次link clicked 后自动移除 listener 函数,再次点击无效。 passive 表示 listener 永远不会调用 preventDefault() 如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告 link.addEventListener('click', function (e) { e.preventDefault(); console.log('link clicked!') }, {capture: false,passive:true }) => 控制台输出:Unable to preventDefault inside passive event listener invocation. 链接跳转 使用 passive 改善的滚屏性能,添加 passive 参数后,touchmove事件不会阻塞页面的滚动(同样适用于鼠标的滚轮事件) addEventListener('touchmove', function listener() { /* do something */ }, { passive: true });
removeEventListener()
removeEventListener 删除对应 addEventListener 添加的事件,必须保证事件函数的引用地址为同一地址 因此匿名函数的注册事件无法被删除 el.addEventListener('click',()=>{},false); el.removeEventListener('click',()=>{},false); => 无法删除添加的点击事件 let fun = () => { ***** } el.addEventListener('click',fun,false); el.removeEventListener('click',fun,false);
IE 事件处理程序
IE 实现了与 DOM 类似的方法,即 attachEvent()和 detachEvent()
这两个方法接收两个同样的参数:事件处理程序的名字 和 事件处理函数
因为 IE8 及更早版本只支持事件冒泡,所以使用 attachEvent() 添加的事件处理程序会添加到冒泡阶段
var btn = document.getElementById("myBtn"); btn.attachEvent("onclick", function() { console.log("Clicked"); }); 注意: attachEvent()的第一个参数是"onclick",而不是 DOM 的 addEventListener()方法 的"click"
在 IE 中使用 attachEvent()与使用 DOM0 方式的主要区别是事件处理程序的作用域。 使用 DOM0 方式时,事件处理程序中的 this 值等于目标元素。 而使用 attachEvent()时,事件处理程序是在全局作用域中运行的,因此 this 等于 window var btn = document.getElementById("myBtn"); btn.attachEvent("onclick", function() { console.log(this === window); // true });
与使用 addEventListener()一样,使用 attachEvent()方法也可以给一个元素添加多个事件处理程序 var btn = document.getElementById("myBtn"); btn.attachEvent("onclick", function() { console.log("Clicked"); }); btn.attachEvent("onclick", function() { console.log("Hello world!"); }); 与DOM2方法不同的是,这里的事件处理程序会以他们定义的顺序反向触发 所以会先打印 Hello world!,然后打印 Clicked
使用 attachEvent()添加的事件处理程序将使用 detachEvent()来移除,只要提供相同的参数 与使用 DOM 方法类似,作为事件处理程序添加的匿名函数也无法移除 但只要传给 detachEvent() 方法相同的函数引用,就可以移除 var btn = document.getElementById("myBtn"); var handler = function() { console.log("Clicked"); }; btn.attachEvent("onclick", handler); btn.detachEvent("onclick", handler);
跨浏览器事件处理程序
通过以上定义事件处理程序的概述,为了能够使浏览器兼容处理事件,可以封装如下代码
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; } } } 使用: var btn = document.getElementById('myBtn'); var handler = function(){ console.log('hi') } EventUtil.addHandler(btn,'click',handler); EventUtil.removeHandler(btn,'click',handler);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人