DOM事件流及事件委托

什么是DOM

文档对象模型(Document Object Model,简称DOM),是W3C组织推荐的处理可扩展标记语言(HTML 或者 XML )的标准编程接口

简言之,它会将web页面和脚本或程序语言连接起来。

html 和DOM的区别

  • html就是标签,文档对象在页面中的表示,DOM就是对这些标签形成一个对象模型(DOM树),规定了这些标签对象的属性和方法
  • 这些标签/元素在DOM下统称为节点

DOM事件流

事件流描述的是从页面中接收事件的顺序

事件发送时会在元素节点之间按照特定的顺序传播,路径所经过的所有节点都会收到该事件,这个传播过程即DOM事件流

比如我们给一个div注册了点击事件:

  1. 捕获阶段:从根节点到事件触发的目标节点(根 –> 事件的目标元素

    document –> document.Element –> body –>

  2. 当前目标阶段:事件触发的目标元素(确定目标节点)

  3. 冒泡阶段:从触发事件的目标节点到根节点(事件的目标元素 –> 根

DOM事件流

  • 事件捕获:

    IE最早提出的,事件开始由最具体的元素接受,然后逐级向上传播到DOM最顶层节点的过程。

    即:从外向内找监听函数

  • 事件捕获:

    网景公司最早提出的,由DOM最顶层节点开始,然后逐级向下传播到最具体的元素的过程。

    即:从内向外找监听函数

举个栗子:

我们向水里扔个石头,首先它会有个下降的过程、这个过程可以理解为捕获过程;之后会产生泡泡,然后漂流在水面上,这个过程相当于事件冒泡。

代码验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="father">
<div class="son">son</div>
</div>
</body>
<script>
let father = document.querySelector('.father')
let son = document.querySelector('.son')

// 1. 捕获事件,addEventListener第三个参数为true,则处于捕获阶段
son.addEventListener('click', function () {
alert('son')
}, true)
father.addEventListener('click', function () {
alert('father')
}, true) // 点击儿子,先弹出father,再弹出son

// 2.冒泡事件,addEventListener第三个参数为false或不写,则处于冒泡阶段
son.addEventListener('click', function () {
alert('son')
}, false)
father.addEventListener('click', function () {
alert('father')
})
</script>

注意

  • 目标元素(目标节点)鼠标去触发了事件的对象元素,不论该元素是否有该事件的对应处理函数
  • 所有浏览器默认都为冒泡,ie只有冒泡,没有捕获
  • 如果在一系列事件流中 既有捕获 又有冒泡 那么先捕获再冒泡
1
2
3
4
5
6
7
8
  div.addEventListener('click',()=>{
console.log('冒泡');
}) // 冒泡
div.addEventListener('click',()=>{
console.log('捕获');
},true) // 捕获

// 捕获 冒泡(谁先监听谁执行)

事件对象

1
2
3
4
5
6
eventTarget.onclick = funciton(event){
// 这个 event 就是事件对象,可以缩写为e
}
eventTarget.addEventListener('click',function(event){
// 这个 event 就是事件对象,可以缩写为e
})Copy

这个 event 是个形参,系统会自动帮我们设定改为事件对象,不需要传递实参过去。

当我们注册事件时,event对象就会被系统自动创建,并依次传递给事件监听器(事件处理函数)

e.target 和 e.currentTarget 的区别

  • e.target: 返回触发事件的对象。 即用户操作的对象。(假设:你点击了谁就是谁)
  • e.currentTarget : 程序员监听的元素, 即你绑定了谁就是谁
  • this就是e.currentTarget

打印一下就知道:

阻止冒泡

方法是 事件对象的

1
2
3
4
5
6
7
8
function fn1(ev){
var ev = ev || event;
alert('div1');
//ev.stopPropagation(); //标准浏览器
//ev.cancelBubble = true; //ie
ev.stopPropagation? ev.stopPropagation() : ev.cancelBubble = true; //取消冒泡的兼容写法
}

阻止默认事件

  • 事件驱动时阻止默认事件 return false

  • 事件绑定时阻止

    • addEventListener –> ev.preventDefault();

    • attachEvent –> ev.returnValue = false;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //事件驱动时
    oA.onclick = function(){
    return false;
    }

    //事件绑定时
    function fn1(ev){
    ev.preventDefault? ev.preventDefault() : ev.returnValue = false
    }

事件委托

原理
不是每个子节点单独设置事件监听器,而是事件监听器设置在其父节点上,然后利用冒泡原理影响设置每个子节点。

举个场景:
比如给ul列表注册点击事件,然后利用事件对象的target来找到当前点击的li,因为点击了li,事件就会冒泡到ul身上,又因为ul有注册事件,就会触发事件监听器。

作用: 这样我们只操作了一次DOM, 省内存、可以监听动态元素

如何监听一个不存在的元素?

可以利用事件委托,监听父元素即可。

代码展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<body>
<div id="test"></div>
</body>
<script>

const testDiv = document.querySelector('#test')
setTimeout(() => {
const btn = document.createElement('button')
btn.textContent = 'click 1'
testDiv.appendChild(btn)
}, 1000)

testDiv.addEventListener('click', (e) => {
let t = e.target
if (t.tagName.toLowerCase() === 'button') {
console.log('button 被点击了');
}
})
</script>

封装事件委托

要求:
写出一个函数 delegate('ul', 'click','li',fn) 当用户点击ul的li元素时,调用fn函数。要求用事件委托。

思路一

1
2
3
4
5
6
7
function delegate(element, eventType, selector, fn) {
element.addEventListener(eventType, function(e){
if(e.target.tagName.toLowerCase() === selector){
fn()
}
})
}

看起来好像没什么问题,实际上是有bug的:

如果用户点击的是 li 里面的 span,就没法触发 fn,这显然不对

思路二

点击 span 后,递归遍历 span 的祖先元素看其中有没有 ul 里面的 li。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function delegate(element, eventType, selector, fn) {
element.addEventListener(eventType, e => {
let el = e.target
while (!el.matches(selector)) {
if (element === el) {
el = null
break
}
el = el.parentNode
}
el && fn.call(el, e, el)
})
return element
}