DOM 事件模型或 DOM 事件机制
引入
1.点击事件
<div class="爷爷"> <div class="爸爸"> <div class="儿子">文字</div> </div> </div>
以上代码即.爷爷>.爸爸>.儿子
给三个div分别添加事件监听 fnYe / fnBa / fnEr。点击谁监听谁 问题一:
- 点击文字,算不算点击儿子?
- 点击文字,算不算点击爸爸?
- 点击文字,算不算点击爷爷?
答案:都算
问题二: 调用监听函数顺序是什么呢
事件捕获:fnYe > fnBa > fnEr , 也就是从外到内去调用
事件冒泡:fnEr > fnBa > fnYe , 也就是从内到外去调用
2.和事佬W3C
2002年,W3C发布标准
- 文档名为DOM Level 2 Events Specification
- 规定浏览器应该同时支持两种调用顺序
- 首先按
爷爷=>爸爸=>儿子
顺序看有没有函数监听 - 然后按
儿子=>爸爸=>爷爷
顺序看有没有函数监听 - 有监听函数就调用,并提供事件信息,没有就跳过
一、DOM 事件机制
事件捕获和事件冒泡
- 从外向内找监听函数,叫
事件捕获
- 从内向外找监听函数,叫
事件冒泡
疑问:从外到内再从内到外,那岂不是fnYe / fnBa / fnEr都调用两次?
- 非也!开发者自己选择把fnYe放在捕获阶段还是放在冒泡阶段
示意图
如何指定走捕获还是冒泡呢?
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,并且提供事件信息。
一个特例
//只有一个 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
滚动条)
不可阻止默认动作
- 有些事件不能
- MDN搜索scroll event,看到Bubbles和 Cancelable
- Bubbles的意思是该事件是否冒泡,所有冒泡都可取消
- Cancelable的意思是开发者是否可以阻止默认事件
- Cancelable与冒泡无关
- 推荐看MDN英文版,中文版内容不全
- 如何阻止滚动
1.scroll 事件不可阻止默认动作
- 阻止scroll默认动作没用,因先有滚动才有滚动事件
- 要阻止滚动,可阻止 wheel和 touchstart的默认动作
- 注意你需要找准滚动条所在的元素,示例
- 但是滚动条还能用,可用CSS让滚动条width: 0 (::-webkit-scrollbar { width: 0 !important })
2.CSS也行
- 使用overflow: hidden可以直接取消滚动条
- 但此时JS依然可以修改scrollTop
小结
- target和currentTarget
- 一个是用户点击的,一个是开发者监听的
- 取消冒泡
- e.stopPropagation()
- 事件的特性
- Bubbles表示是否冒泡
- Cancelable表示是否支持开发者取消冒
- 如scroll不支持取消冒泡
- 如何禁用滚动
- 取消特定元素的wheel和touchstart的默认动作
自定义事件
-自定义事件
- 浏览器自带事件
- 一共100多种事件,列表在MDN 上
- 提问
- 开发者能不能在自带事件之外,自定义一个事件
- 答案:可以,见示例
二、事件委托
1.什么是事件委托
事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。通俗地来讲,就是把一个元素响应事件(click、......)的函数委托到另一个元素; 一般来说,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
2.常见应用场景
场景一
- 你要给100个按钮添加点击事件,咋办?
- 答:监听这100个按钮的祖先,等冒泡的时候判断target是不是这100个按钮中的一个代码示例
场景二
- 你要监听目前不存在的元素的点击事件,咋办?
- 答:监听祖先,等点击的时候看看是不是我想要监听的元素即可代码示例
优点
- 省监听数(内存)
- 可以监听动态元素
3. 代码实现
需求:监听所有的 li 标签,如果用户点击 li 标签,就 console.log('用户点击了 Li 标签')
<ul id="test"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul>
那么实现的 JS 代码就是:
// 监听父元素 ul#test test.addEventListener('click', (e)=> { //通过浏览器传进来的e参数,找到当前点击元素 const t = e.target // 判断当前元素是不是Li标签 if(t.matches('li') { console.log('用户点击了li') } })
实现思路:
- 首先监听父元素,
- 然后根据浏览器传进去的事件信息,拿到当前点击元素,
- 再判断当前点击元素是不是 li 元素, 如果是,就 console.log('用户点击 Li 标签')
基于此,我们可以封装一个事件委托函数
on("click", "#test", "li", () => { console.log("用户点击了li"); }); function on(eventType, parentElement, selector, fn) { // 先判断是不是element, //如果传进来的是选择器,不是element本身,就先变成element, // 因为只有element才能监听事件 if (!(parentElement instanceof Element)) { parentElement = parentElement.querySelectorAll(parentElement); } parentElement.addEventListener(eventType, (e) => { let target = e.target; if (target.matches(selector)) { fn(e); } }); }
但是以上这种实现有一个小问题,那就是如果被点击元素有多个父元素怎么办?
<ul id="test"> <li> <p> <span>1</span> </p> </li> <li> <p> <span>2</span> </p> </li> <li> <p> <span>3</span> </p> </li> <li> <p> <span>4</span> </p> </li> </ul>
我们需要做的就是: 递归地向上多找几层父节点,直到找到 li 标签,
同时还必须限定,寻找的范围不能超过 parentElement,
拿上面的例子来说,不可以越过 ul 标签,去找 body 标签
on("click", "#test", "li", () => { console.log("用户点击了li"); }); function on(eventType, element, selector, fn) { if (!(element instanceof Element)) { element = document.querySelectorAll(element); } element.addEventListener(eventType, (e) => { let target = e.target; // 如果匹配到了selector就跳出循环 while (!target.matches(selector)) { if (target === element) { //已经找到了父元素,说明还没找到,就设置为null target = null; break; } target = target.parentNode; } // 找到了target, 就调用函数 target && fn.call(target, e); }); }
三、总结
1.捕获和冒泡
捕获:当用户点击按钮,浏览器会从 window 从上向下
遍历至用户点击的按钮,逐个触发事件处理函数。
冒泡:浏览器从用户点击的按钮从下往上
遍历至 window,逐个触发事件处理函数。
什么是事件委托
事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。通俗地来讲,就是把一个元素响应事件(click、......)的函数委托到另一个元素; 一般来说,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。