一、“DOM 分级” 与 “DOM 事件模型” 的关系
W3C DOM 标准(DOM 分级)先后有三个版本:
- 1级 DOM、2级 DOM、3级 DOM
早在 DOM 标准正式形成前,就有一些被广泛应用的规则,这就是我们通常所说的0级 DOM。
DOM 事件模型(指绑定事件的方式,以及事件传播方式)有三个:
- 0级 DOM 中定义的 DOM 0级模型、2级 DOM 定义的 DOM2 级模型、IE 事件模型
0级 DOM 中有 “原始事件模型”;2级DOM中除了定义了一些 “DOM 相关的操作” 之外,还定义了一个 “事件模型” 。1级 DOM 没有定义事件相关的内容,仅仅是定义了 HTML 和 XML 文档的底层结构;3级 DOM 也仅仅定义了一些 DOM 相关的操作。所以这两个版本的 DOM 标准并不存在事件模型。
二、DOM 分级
1. 0级DOM
包含了一些基本事件和 “原始事件模型” ,该模型指明了应该如何为元素添加事件监听,在该模型中,事件不会传播,即没有事件流。同时,用事件的返回值来表示是否阻止浏览器默认行为。
为元素绑定事件监听的两种方法:
1)在标签上,直接为事件绑定对应的回调(内联事件)
<input type="button" onclick="fun()">
这种方法,是无法使用闭包和匿名函数的。
2)也可以通过 js 指定事件的回调函数,这种方法等价于内联事件:
var btn = document.getElementById('.btn');
btn.onclick = fun;
监听回调的返回值为 false 时,可以阻止浏览器执行默认行为。
移除事件监听只有一种方法:
btn.onclick = null;
这种方式所有浏览器都兼容,但是逻辑与显示并没有分离。
在0级 DOM 事件模型中,回调函数被直接赋到了对应的监听事件上。就好像我们用 let a = 1; 定义一个变量,通过 a = 2; 改变了a的值,前一个被赋予的值自然就被顶替掉了。所以这种直接赋值的方法,是很难实现为一个事件添加多个回调函数的。
同时,赋值的方式可以保证回调函数中的 this 指向的是绑定事件监听的元素。
2. 1级DOM
没有定义出新的事件模型,仅仅是定义了 HTML 和 XML 文档的底层结构。
3. 2级DOM
添加了许多新的事件,定义了2级 DOM 事件模型,该模型指明了新的添加、移除事件监听的方式,还增添了事件流。
2级DOM把新增的事件分为了5种类型:
- UI事件:DOMActive、DOMFocusIn、DOMFocousOut(元素激活、获取焦点、失去焦点)
- 鼠标事件:mousedown、mouseup、click、dbclick
- 键盘事件:keydown 、 keypress 、 keyup
- HTML事件:load、unload、abort、error、select、change、submit、reset、resize、scroll、focus、blur。
- 变动事件:当底层 DOM 结构发生变化时触发
- DOMSubtreeModified:DOM 结构中发生任何变化时都会触发。这个事件在其他任何事件触发后都会触发。
- DOMNodeInserted:一个节点作为子节点,被插入到另一个节点上时触发
- DOMNodeRemoved:节点从其父节点中被移除时触发
- DOMNodeInsertIntoDocument:在一个节点被直接插入文档或通过子树间接插入文档之后触发。这个事件在 DOMNodeInserted 之后触发
- DOMNodeRemovedFromDocument:在一个节点被直接从文档中移除或通过子树间接从文档中移除之前触发。这个事件在 DOMNodeRemoved 之后触发
- DOMAttrModified:在特性被修改之后触发
- DOMCharacterDataModified:在文本节点的值发生变化时触发。
在2级 DOM 事件模型中,借助发布订阅模式操作事件回调,可以实现为一个事件添加多个监听回调,同时,还可以准确地移除某一个具体的监听回调
var btn = document.getElementById('.btn');
btn.addEventListener(‘click’, showMessage1, false); // 为btn绑定监听事件,并添加一个回调函数
btn.addEventListener(‘click’, showMessage2, false); // 为btn的点击事件再添加一个监听回调
btn.removeEventListener(‘click’, showMessage1, false); // 清除btn上的第一个监听回调
第三个参数表示:是在捕获阶段处理还是冒泡阶段处理绑定的事件回调。false(默认值),表示和 IE 事件模型行为保持一致,也就是在冒泡阶段处理。
其次,2级 DOM 事件模型将事件传播分为了三个阶段:
- 捕获阶段:事件由最外层document向内层传播,一直传递到对应触发事件的元素
- 命中事件:执行对应监听回调
- 冒泡阶段:从命中元素向外层元素传播,一直传递到 document
触发事件时,会产生一个事件对象 event,在该对象中保存着与事件有关的各种信息,例如:添加事件的元素、事件的类型...
现在,我们依旧可以用0级 DOM 事件模型中的方法添加事件监听,但是在事件触发时,会伴有事件流的产生。只不过,这种方式添加的事件,监听只能在冒泡阶段触发,而不能指定捕获阶段。
在2级 DOM 中,可以用 event.currentTarget 来获取绑定事件的元素。用 event.target 获取触发事件的元素。
在2级 DOM 中事件模型中,通过 preventDefault() 阻止浏览器的默认事件。
4. 3级DOM
再次增加了一些事件,提升交互能力。
三、IE事件模型
在该模型中,事件的传播没有捕获阶段,只有命中与冒泡阶段。
在 IE9 及之前的版本,为元素绑定事件监听通过下面这种方式:
btn.attachEvent('onclick', showMessage);
通过下面这种方式移除事件监听:
btn.detachEvent('onclick', showMessage);
在 IE 中,event 不能直接获取,需要通过 window.event 的方式。
阻止浏览器默认行为需要通过 stopPropagation(retrun false;) 来实现。
之后的版本就和2级 DOM 事件模型中的方法一致了。
注意!在 DOM 事件模型中,事件类型没有 on,而在 IE 事件模型中,事件类型需要加上 on!
四、事件代理(事件委托)
我们借助一个案例来感受一个事件代理的强大。
实现这样一个效果:ul 标签下有很多 li 标签,点击 li 标签,提示标签中文字的颜色
<ul id="ul">
<li>red</li>
<li>yellow</li>
<li>blue</li>
<li>green</li>
<li>black</li>
<li>white</li>
</ul>
常规方法:
var ul = document.getElementById('ul');
var lis = ul.getElementsByTagName('li');
for(let i in lis){
lis[i].addEventListener('click', showColor, false);
};
function showColor(e){
var x = e.target;
alert("The color is " + x.innerHTML);
};
从代码中可以看出来,我们给每个 li 标签都加了功能基本相同的监听函数
借助事件代理来实现:
var ul = document.getElementById('ul');
ul.addEventListener('click',showColor,false);
function showColor(e){
var x = e.target;
if(x.nodeName.toLowerCase() === 'li'){
alert('The color is ' + x.innerHTML);
}
}
将监听加在 li 的父元素 ul 标签中,通过 e.target 获取到真正被点击的子元素标签,也可以实现同样的效果。
最大的优点就在于,我们只添加了一个监听,维护起来会十分的方便。并且,当我们需要通过 js 动态添加 li 时,新添加的 li 自动地实现了上述方法。
五、target & currentTarget
事件代理的核心,一是事件交给父元素处理的思想,还有一点就是对 e.target 的使用。
e.target 与 e.currentTarget 的区别:
- e.target:返回真正的触发了事件的标签对象。这个会随着我们点击位置的不同,返回不同的标签对象,这个就和事件流有关
- e.currentTarget:返回绑定事件的标签对象。显然这个是不会变化的,只要事件绑定好了,自然返回的就总是同一个标签
举个例子来看一下效果:
<div id='outer'>
<div id='inner'></div>
</div>
let outer = document.getElementById('outer');
outer.addEventListener('click', function(e) {
console.log("e.target:",e.target.id);
console.log("e.currentTarget",e.currentTarget.id);
});
先点击outer,然后点击inner,提示:
参考文章: