DOM 事件模型或 DOM 事件机制
事件
通俗理解,事件是用户或者浏览器自己执行的某种动作,是文档或者浏览器发生的一些交互瞬间,比如点击(click)按钮等,这里的click就是事件的名称。JS与html之间的交互是通过事件实现的。
每个事件都有事件监听器(有时也叫事件监听器),也就是触发事件时运行的代码块。严格来说事件监听器监听事件是否发生,然后事件处理器对事件做出反应。
事件模型
一个事件发生后,会在子元素及父元素之间进行传播(propagation),这种传播分为三个阶段。
DOM事件传播的三个阶段:
捕获阶段
目标阶段
冒泡阶段
事件捕获和事件冒泡
事件捕获:由外向内找监听函数。
事件冒泡:由内向外找监听函数。
例子:
<div class="grandfather"> <div class="father"> <div class="son"></div> hi </div> </div> //假如给三个div分别添加事件监听 fnYe / fnBa / fnEr 复制代码
问题一:
点击文字,算不算点击儿子?
点击文字,算不算点击爸爸?
点击文字,算不算点击爷爷?
答案:都算
问题二: 调用监听函数顺序是什么呢?
事件捕获:fnYe > fnBa > fnEr , 也就是从外到内去调用
事件冒泡:fnEr > fnBa > fnYe , 也就是从内到外去调用
W3C在2002年发布了标准, 文件名为DOM Level 2 Events Specification
规定浏览器同时支持两种调用顺序
首先按爷爷->爸爸->儿子顺序看有没有函数监听 , 先事件捕获
然后按儿子->爸爸->爷爷顺序看有没有函数监听 , 再事件冒泡
如图所示:
如何指定走捕获还是冒泡呢?
baba.attachEvent('onclick',fn)//冒泡 baba.addEventListener('click',fn)//捕获 baba.addEventListener('click',fn,bool)//w3c制定 复制代码
如果bool不传或为falsy
就让fn走冒泡,即当浏览器在冒泡阶段发现
baba
有fn
监听函数,就会调用fn,并提供事件信息。如果bool为true
就让fn走捕获,即当浏览器在捕获阶段发现
baba
有fn
监听函数,就会调用fn,并且提供事件信息。
代码示例:
HTML:
<div class="level1 x"> <div class="level2 x"> <div class="level3 x"> <div class="level4 x"> <div class="level5 x"> <div class="level6 x"> <div class="level7 x"> </div> </div> </div> </div> </div> </div> </div> 复制代码
CSS:
* { box-sizing: border-box; } div[class^=level] { border: 1px solid; border-radius: 50%; display: inline-flex; } .level1 { padding: 10px; background: purple; } .level2 { padding: 10px; background: blue; } .level3 { padding: 10px; background: cyan; } .level4 { padding: 10px; background: green; } .level5 { padding: 10px; background: yellow; } .level6 { padding: 10px; background: orange; } .level7 { width: 50px; height: 50px; border: 1px solid; background: red; border-radius: 50%; } .x{ background: transparent; } 复制代码
JS:
const level1 = document.querySelector('.level1') const level2 = document.querySelector('.level2') const level3 = document.querySelector('.level3') const level4 = document.querySelector('.level4') const level5 = document.querySelector('.level5') const level6 = document.querySelector('.level6') const level7 = document.querySelector('.level7') let n = 1 const fm = (e)=>{ const t = e.currentTarget setTimeout(()=>{ t.classList.remove('x') },n*1000) n+=1 } const fa = (e)=>{ const t =e.currentTarget setTimeout(()=>{ t.classList.add('x') },n*1000) n+=1 } level1.addEventListener('click',fm,true) //true 走捕获 level1.addEventListener('click',fa) //默认为 false 走冒泡 // 先捕获后冒泡 level2.addEventListener('click',fm,true) level2.addEventListener('click',fa) level3.addEventListener('click',fm,true) level3.addEventListener('click',fa) level4.addEventListener('click',fm,true) level4.addEventListener('click',fa) level5.addEventListener('click',fm,true) level5.addEventListener('click',fa) level6.addEventListener('click',fm,true) level6.addEventListener('click',fa) level7.addEventListener('click',fm,true) level7.addEventListener('click',fa) 复制代码
一个特例
//只有一个 div 被监听(不需要考虑父子关系) div.addEventListenter('click',f1) //冒泡 div.addEventListenter('click',f2,true) //捕获 复制代码
f1
先执行还是 f2
先执行呢?
先捕获在冒泡, f2
先执行?
正确答案:f1
先执行。
当没有父子关系时,谁先监听谁就先执行。
target 与 currentTarget的区别
e.target
用户操作的元素e.currentTarget
程序员监听的元素
例子:
<div> <span>文字</span> </div> 复制代码
e.target
就是span
,用户操作的元素。e.currentTarget
就是div
, 程序员监听的元素。
取消冒泡
e.stopPropagation()
可打断冒泡,浏览器不再向上走
一般用于封装某些独立组件
注意:捕获不可以取消但是冒泡可以(有些事件也不能够取消冒泡,例如: scroll
滚动条)
事件委托
事件委托的好处:
省内存(省监听数)
可以监听动态元素
例子一:
//有 100 个 button <div id="div1"> <span>span1</span> <button>click 1</button> <button>click 2</button> <button>click 3</button> <button>click 4</button> <button>click 5</button> . . . <button>click 100</button> </div> 复制代码
如果有100个 button
怎么办呢,不可能创建 100 个监听器吧。
将监听委托给 div
我们只需要监听这个100个按钮的祖先,等冒泡的时候判断 e.target
是不是这100个按钮中的一个,节省内存。
div1.addEventListener('click',(e)=>{ const t = e.target if(t.tagName.toLowerCase()==='button'){ console.log('button 被点击了') } }) 复制代码
例子二:
当 button
延迟1秒后才出现,我们如何监听呢?
// 延迟1秒后,创建 button setTimeout(()=>{ const button = document.createElement('button') button.textContent='click' div1.appendChild(button) },1000) div1.addEventListener('click',(e)=>{ const t = e.target if(t.tagName.toLowerCase()==='button'){ console.log('button被点击') } }) 复制代码
监听祖先,等点击的时候看看是不是监听的元素即可,监听动态元素。
封装事件委托
setTimeout(()=>{ const button = document.createElement('button') button.textContent='click' div1.appendChild(button) },1000) on('click','#div1','button',()=>{ //'#div'是选择器不是元素 console.log('button 被点击啦') }) // 声明 on 函数 function on(eventType,element,selector,fn){ //判断 element 类型是不是元素 if(!(element instanceof Element)){ // 不是则找到指定的元素并赋值给 element element = document.querySelector(element) } element.addEventListener(eventType,(e)=>{ const t= e.target if(t.matches(selector)){ fn(e) } }) }
作者:Biao
链接:https://juejin.cn/post/7034081742373781511