DOM 历史、流模型及事件委托

Javascript DOM(文档对象模型)是一个允许开发人员操纵页面内容、结构和风格的接口。基本上网页由 HTML 和 CSS 文档组成,浏览器用于创建文档的描述被称为文档对象模型(DOM)。它使 Javascript 能够访问和操作页面的元素和样式。该模型构建在基于对象的树结构中,并定义:HTML 元素作为对象,HTML 元素的属性和事件,访问HTML元素的方法。

事件的历史

W3C 统一各个浏览器标准:

1
btn.onclick = function(){...}

只要用户点击按钮,就会调用函数:

1
btn.onclick.call(btn, {arg})

最早的事件,这是 DOM 0 的事件监听,在 DOM level 1 之前出现的事实标准

现在有多少个 DOM 标准呢?我们可以打开网址 https://www.w3.org/DOM/DOMTR

DOM标准

一共有 3 个标准和一个标准草案。

我们先来看 DOM Level 1 :

一共就两个章节,很简单对不对?

点开Chapter 1: Document Object Model Core查看它的内容:

我们可以了解到 DOM 的结构是怎样的,内存管理是怎样的,名称转移,String 是怎样的,String 是怎么比较的,完全没看到 Event 是不是?我们来看第二章:

这里讲了名称管理,HTML Collection,HTML Document,HTML Elements 这些定义。也没有我们关心的 Event 是吧?我只在文档中找到了简短的介绍:

当时连 change 事件都没有,就这四个。要清楚,DOM Level 1 只是对之前浏览器事件的一次汇总而已。

DOM Level 2 内容就丰富了很多:

这次可以清楚看到 Events 被单独拿出来做了一下整理,而且添加了很多新功能:

就这一章:1. Document Object Model Events DOM Events 是一个单独的标准,

我们可以看到几个熟悉的名词:event flow(事件流),event capture(事件捕获),event bubbling(事件冒泡),event cancelation(事件取消),Mouse event(鼠标事件),Key event(键盘事件),当然目录中一些内容已经被废弃。这个标准把事件写得特别的详细,我们也主要关注 DOM Level 2 的内容,DOM Events 可以说是 Level 2 最大的更新。

DOM Level 3 中并没有 Events 相关的内容,所以说到 DOM 事件指的是 DOM Level 2 已经规定好的 Event 标准。

DOM 中的事件队列

来看 DOM Level 1 :

1
2
3
<button id=X onclick="print">A</button>
<button id=Y onclick="print()">B</button>
<button id=Z onclick="print.call()">C</button>
1
2
3
X.onclick = print
Y.onclick = print()
Z.onclick = print.call()

正确的调用方式是?

我们不能默认 HTML 中怎么写,JS 就怎么写。

1
2
onclick="要执行的代码"
一旦用户点击,浏览器就 eval('要执行的代码') eval('print')

所以我们可以确定 BC 是对的,因为 print 不是一个方法。

我们再来看 JS 中的代码:

1
2
// 一旦用户点击,浏览器就执行
X.onclick.call(X, {})

预示着 onclick 是一个函数属性,onclick 一定对应一个函数对象,也就是 {}。所以选 X。

我们再来看 Y 和 Z 的类型,不是函数对象那么是什么?

1
2
3
X.onclick = print // 函数对象
Y.onclick = print() // undefined
Z.onclick = print.call() // undefined

DOM Level 2 是一样的吗?不是一样的:

1
2
3
4
5
<button id=xxx>xxx</button>

xxx.addEventListener('click', function(){
alert(1)
})

你看这样的写法是不是很麻烦?下面我们说一说它们之间的区别:

1
2
3
4
5
6
7
// 属性,唯一
xxx.onclick = function(){
console.log(1)
}
xxx.onclick = function(){
console.log(2)
}

你看上面的代码,只调用后面一个,打印 2,不好用。这就导致我们不敢轻易调用 onclick 接口。

所以这个唯一的接口是一个非常不好的设计模型,所以我们使用队列来绑定多个事件:

1
2
3
4
5
// 事件队列,先进先出
xxx.addEventListener('click', function(){ alert(1) })
xxx.addEventListener('click', function(){ alert(2) })

// xxx 拥有一个队列 eventListeners

既然他是一个队列,那么他就可以实现出列和入列:

1
2
3
4
5
6
7
8
9
10
11
function f1(){ console.log(1) }
function f2(){ console.log(2) }
function f3(){ console.log(3) }

xxx.addEventListener('click', f1 })
xxx.addEventListener('click', f2 })

xxx.removeEventListener('click', f1 })

xxx.addEventListener('click', f3 })
xxx.removeEventListener('click', f3 })

此时点击 xxx,会打印出什么?答案是 2

我们根据这个写一个单次触发的按钮:

1
2
3
4
5
6
function f1(){ 
conselo.log(1)
xxx.removeEventListener('click', f1)
}

xxx.addEventListener('click', f1)

事件模型

简单讲

来看一个小例子:

1
2
3
4
5
6
7
8
9
10
11
<body>
<div id="grand1">
爷爷
<div id="parent1">
父亲
<div id="child1">
儿子
</div>
</div>
</div>
</body>
1
2
3
4
div {
border: 1px solid black;
margin: 10px;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 当我点击了儿子的时候,是否点击了父亲和爷爷
// yes

// 2. 当我点击儿子的时候,三个函数是否调用
grand1.addEventListener('click', ()=>{ console.log('爷爷') }) //fn1
parent1.addEventListener('click', ()=>{ console.log('爸爸') }) //fn2
child1.addEventListener('click', ()=>{ console.log('儿子') }) //fn3
// yes

// 3. 请问三个函数的执行顺序 123 or 321?
// w3c 表示都可以

// false 假值:undefined 0 null ‘’ NaN
// 其实 addEventListener 是有参数的
grand1.addEventListener('click', ()=>{ console.log('爷爷') }, true)
parent1.addEventListener('click', ()=>{ console.log('爸爸') }, true)
child1.addEventListener('click', ()=>{ console.log('儿子') }, true)
// 不传第三个参数 / 传 false:儿子爸爸爷爷
// 传递第三个参数 true:爷爷爸爸儿子

和代码的书写顺序没有关系,除非你监听的是同一个元素,那就是普通队列的逻辑。

以上讲的就是事件模型!

复杂讲

我们可以看到 fn1 参数的执行顺序和参数有关,如果都是默认值,也就是说都是 false,就从左边走。

从上到下阶段:捕获

从下到上阶段:冒泡

事件模型就是:先捕获再冒泡

DOM 事件流,指的是这三个阶段:事件捕获 –> 目标事件 –> 事件冒泡。

智障题

如果儿子身上既有冒泡阶段,又有捕获阶段:

1
2
child1.addEventListener('click', ()=>{ console.log('儿子捕获') }, true) 
child1.addEventListener('click', ()=>{ console.log('儿子冒泡') }, false)

谁先执行?

这是个特例:被点击的元素(最后一个节点)如果同时有捕获和冒泡,按照写的顺序执行。

浏览器行为

定义:就是标签特有的响应某一事件的行为。

例如:

  1. <a href="www.baidu.com">,当点击 a 标签,自然会跳转到百度搜索页。
  2. <input type="checkbox">,点击这个 checkbox,就会被勾选中。
  3. <button type="submit">,点击按钮之后,页面就会提交表单信息。

阻止浏览器默认行为

  • W3C:e.preventDefault()
  • IE:window.event.returnValue = false

因此综合一下,可以很快给出兼容版本的阻止默认代码,但是在取消默认行为之前,一定要判断一下是否可以取消默认行为,否则会报错。

1
2
3
4
5
6
7
8
function handlePropagation(e) {
let e = e || window.event
if (e.preventDefault) {
e.preventDefault(); // 除IE外
} else {
e.returnValue = false; // IE专属
}
}

事件对象(event)

事件对象有以下几个属性值:

  1. target
  2. timeStamp
  3. type(事件类型)
  4. currentType(最开始触发事件的节点)
  5. pageX(事件触发的 x 轴坐标)
  6. pageY(事件触发的 y 轴坐标)
  7. which(鼠标的左、中、右键值(1、4、2))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
div.addEventListener('click', function(e) {
console.log('=======点击了div标签=======')
console.log('当前目标对象:', e.target)
console.log('当前时间戳:', e.timeStamp)
console.log('当前目标对象节点', e.target.id)
console.log('当前节点(最先开始触发事件的节点):', e.currentTarget.id)
console.log('当前事件类型:', e.type)
console.log('当前事件发生的横坐标点:', e.pageX)
console.log('当前事件发生的纵坐标点:', e.pageY)
console.log('当前点击鼠标的哪个键:', e.which)
console.log('==========================')
}, false) // 注意:这里是false,如果改成捕获模式,当点击span标签时,e.currentTarget.id会输出div
span.addEventListener('click', function(e) {
console.log('点击了span标签')
e.stopPropagation();
})

e.target.id表示的是触发事件的标签名称,我们可以根据这一点进行控制节点事件的响应。比如,点击 div 标签,只有它响应,其它 DOM 节点不准响应。也就是说点谁谁响应,这就涉及事件委托。

事件委托

事件委托就是利用事件模型,将事件的响应交由其它节点对象去处理。

委托的优点:

1. 减少内存的消耗

因为绑定事件越多,浏览器内存占用越大,严重影响性能 还是举个🌰吧:

  • 有100条数据,100个li,给每个li都加事件,占用内存很大,所以,利用冒泡机制,在父元素上添加点击事件:如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ul id="ul">
</ul>
<script>
const ul=document.getElementById('ul');
for(let i=0;i<100;i++){
let li=document.createElement('li');
li.innerHTML=i;
ul.appendChild(li);
}
ul.addEventListener('click',function(e){
if(e.target.tagName==='UL') return;
e.target.className=e.target.className.indexOf('color')===-1?'color':'';
})//可自行执行;color是类名可以在style中添加自己喜欢的颜色,666
</script>

2. ajax的出现,局部刷新的盛行,导致每次加载完,都要重新绑定事件(这里就使用setTimeout异步代替了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<ul id="ul">
<li>666</li>
</ul>
<script>
const ul=document.getElementById('ul');
const lists=ul.getElementsByTagName('li');
setTimeout(()=>{
for(let i=0;i<100;i++){
let li=document.createElement('li');
li.innerHTML=i;
ul.appendChild(li);
}
},400)
for(let i=0;i<lists.length;i++){
lists[i].onclick=function(){
alert(i);
}
}
//结果只点击666弹窗;怎么解决;大家动动脑子吧;相信你们是最棒的
</script>

委托的局限性

  1. 比如 focus、blur 之类的事件本身没有事件冒泡机制,所以无法委托;
  2. mousemove、mouseout这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的;

使用委托的注意项(可以叫应用项)

  1. 只在必须的地方,使用事件委托,比如:ajax的局部刷新区域
  2. 尽量的减少绑定的层级,并且不在body元素上,进行绑定;(事件委托的原理离不开DOM的查找;而浏览器太多层级的查找非常耗性能
  3. 减少绑定的次数,如果可以,那么把多个事件的绑定,合并到一次事件委托中去,由这个事件委托的回调,来进行分发。

经典题

mouseeneter 和 mouseover 的区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
inner.onmouseenter = function () {
console.log('inner enter');
};
outer.onmouseenter = function () {
console.log('outer enter');
};
inner.onmouseleave = function () {
console.log('inner leave');
};
outer.onmouseleave = function () {
console.log('outer leave');
};
复制代码
  1. over属于滑过(覆盖)事件,从父元素进入到子元素,属于离开了父元素,会触发父元素的out,触发子元素的over;enter属于进入,从父元素进入子元素,并不算离开父元素,不会触发父元素的leave,触发子元素的enter
  2. enter和leave阻止了事件的冒泡传播,而over和out还存在冒泡传播的

所以对于父元素嵌套子元素这种情况,使用OVER会发生很多不愿意操作的事情,此时我们使用ENTER会更加简单,操作方便,所以真实项目中ENTER的使用会比OVER多

哪些事件可以冒泡

冒泡事件:

  1. click
  2. dblclick
  3. copy
  4. cut
  5. drag
  6. mouseover 指针移动到有监听事件的元素,或者其子元素(有渗透,作用范围渗透到其子元素)
  7. mouseout指针移除元素,或者其子元素

不冒泡事件

  1. blur
  2. focus
  3. load
  4. unload
编辑