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
一共有 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 | <button id=X onclick="print">A</button> |
1 | X.onclick = print |
正确的调用方式是?
我们不能默认 HTML 中怎么写,JS 就怎么写。
1 | onclick="要执行的代码" |
所以我们可以确定 BC 是对的,因为 print 不是一个方法。
我们再来看 JS 中的代码:
1 | // 一旦用户点击,浏览器就执行 |
预示着 onclick 是一个函数属性,onclick 一定对应一个函数对象,也就是 {}
。所以选 X。
我们再来看 Y 和 Z 的类型,不是函数对象那么是什么?
1 | X.onclick = print // 函数对象 |
DOM Level 2 是一样的吗?不是一样的:
1 | <button id=xxx>xxx</button> |
你看这样的写法是不是很麻烦?下面我们说一说它们之间的区别:
1 | // 属性,唯一 |
你看上面的代码,只调用后面一个,打印 2,不好用。这就导致我们不敢轻易调用 onclick
接口。
所以这个唯一的接口是一个非常不好的设计模型,所以我们使用队列来绑定多个事件:
1 | // 事件队列,先进先出 |
既然他是一个队列,那么他就可以实现出列和入列:
1 | function f1(){ console.log(1) } |
此时点击 xxx,会打印出什么?答案是 2
我们根据这个写一个单次触发的按钮:
1 | function f1(){ |
事件模型
简单讲
来看一个小例子:
1 | <body> |
1 | div { |
1 | // 1. 当我点击了儿子的时候,是否点击了父亲和爷爷 |
和代码的书写顺序没有关系,除非你监听的是同一个元素,那就是普通队列的逻辑。
以上讲的就是事件模型!
复杂讲
我们可以看到 fn1 参数的执行顺序和参数有关,如果都是默认值,也就是说都是 false,就从左边走。
从上到下阶段:捕获
从下到上阶段:冒泡
事件模型就是:先捕获再冒泡
DOM 事件流,指的是这三个阶段:事件捕获 –> 目标事件 –> 事件冒泡。
智障题
如果儿子身上既有冒泡阶段,又有捕获阶段:
1 | child1.addEventListener('click', ()=>{ console.log('儿子捕获') }, true) |
谁先执行?
这是个特例:被点击的元素(最后一个节点)如果同时有捕获和冒泡,按照写的顺序执行。
浏览器行为
定义:就是标签特有的响应某一事件的行为。
例如:
<a href="www.baidu.com">
,当点击 a 标签,自然会跳转到百度搜索页。<input type="checkbox">
,点击这个 checkbox,就会被勾选中。<button type="submit">
,点击按钮之后,页面就会提交表单信息。
阻止浏览器默认行为
- W3C:
e.preventDefault()
- IE:
window.event.returnValue = false
因此综合一下,可以很快给出兼容版本的阻止默认代码,但是在取消默认行为之前,一定要判断一下是否可以取消默认行为,否则会报错。
1 | function handlePropagation(e) { |
事件对象(event)
事件对象有以下几个属性值:
target
timeStamp
type
(事件类型)currentType
(最开始触发事件的节点)pageX
(事件触发的 x 轴坐标)pageY
(事件触发的 y 轴坐标)which
(鼠标的左、中、右键值(1、4、2))
1 | div.addEventListener('click', function(e) { |
e.target.id
表示的是触发事件的标签名称,我们可以根据这一点进行控制节点事件的响应。比如,点击 div
标签,只有它响应,其它 DOM 节点不准响应。也就是说点谁谁响应,这就涉及事件委托。
事件委托
事件委托就是利用事件模型,将事件的响应交由其它节点对象去处理。
委托的优点:
1. 减少内存的消耗
因为绑定事件越多,浏览器内存占用越大,严重影响性能 还是举个🌰吧:
- 有100条数据,100个li,给每个li都加事件,占用内存很大,所以,利用冒泡机制,在父元素上添加点击事件:如下
1 | <ul id="ul"> |
2. ajax的出现,局部刷新的盛行,导致每次加载完,都要重新绑定事件(这里就使用setTimeout异步代替了)
1 | <ul id="ul"> |
委托的局限性
- 比如 focus、blur 之类的事件本身没有事件冒泡机制,所以无法委托;
- mousemove、mouseout这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的;
使用委托的注意项(可以叫应用项)
- 只在必须的地方,使用事件委托,比如:ajax的局部刷新区域
- 尽量的减少绑定的层级,并且不在body元素上,进行绑定;(
事件委托的原理离不开DOM的查找;而浏览器太多层级的查找非常耗性能
) - 减少绑定的次数,如果可以,那么把多个事件的绑定,合并到一次事件委托中去,由这个事件委托的回调,来进行分发。
经典题
mouseeneter 和 mouseover 的区别?
1 | inner.onmouseenter = function () { |
- over属于滑过(覆盖)事件,从父元素进入到子元素,属于离开了父元素,会触发父元素的out,触发子元素的over;enter属于进入,从父元素进入子元素,并不算离开父元素,不会触发父元素的leave,触发子元素的enter
- enter和leave阻止了事件的冒泡传播,而over和out还存在冒泡传播的
所以对于父元素嵌套子元素这种情况,使用OVER会发生很多不愿意操作的事情,此时我们使用ENTER会更加简单,操作方便,所以真实项目中ENTER的使用会比OVER多
哪些事件可以冒泡
冒泡事件:
click
dblclick
copy
cut
drag
mouseover
指针移动到有监听事件的元素,或者其子元素(有渗透,作用范围渗透到其子元素)mouseout
指针移除元素,或者其子元素
不冒泡事件
- blur
- focus
- load
- unload