一、DOM
文档对象模型DOM(Document Object Model)文档对象模型,是HTML和XML文档的编程接口它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将web页面和脚本或程序语言连接起来。
简单来说就是把document作为根节点(DOM树),展开各种处理网页内容的方法和接口,对网页进行访问与操作(window.document)。能够使用如 JavaScript等脚本语言进行修改。
二、事件
事件是您在编程时系统内发生的动作或者发生的事情,系统响应事件后,如果需要,您可以某种方式对事件做出回应。例如:如果用户在网页上单击一个按钮,您可能想通过显示一个信息框来响应这个动作。
例如,click就是一个事件,可以在后面绑定函数执行功能。事件可以在文档(Document)结构的任何部分被触发,触发者可以是用户操作,也可以是浏览器本身。事件并不是只是在一处被触发和终止;他们在整个document中流动,拥有它们自己的生命周期。
三、 DOM 事件模型或 DOM 事件机制
DOM 事件模型或 DOM 事件机制也可以叫做DOM事件流,也可以说是事件传播顺序。
从一个简单的代码来举例:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<div id="box">Click</div>
</body>
</html>
思考:点击div时,事件的传播顺序是:
(1)div→body→HTML→document
还是
(2)document→HTML→body→div
呢?
在早期,IE走的是(1),网景走的是(2)
(1)从小到大,由内到外叫做事件冒泡,也就是IE的事件流
(2)从大到小,由外到内叫做事件捕获,也就是网景的事件流
在捕获阶段:
- 浏览器检查元素的最外层祖先
<html>,是否在捕获阶段中注册了一个onclick事件处理程序,如果是,则运行它。 - 然后,它移动到
<html>中单击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。
在冒泡阶段,恰恰相反:
- 浏览器检查实际点击的元素是否在冒泡阶段中注册了一个
onclick事件处理程序,如果是,则运行它 - 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达
<html>元素。

两家大佬互相倾轧,苦的是一大批搬砖的小工人,就请出了W3C来指定一个统一的标准——DOM Level 2 Events Specification(DOM2级事件模型)规定浏览器应该同时支持两种调用顺序,先走捕获,看有没有函数监听,有就执行,没有就跳过;然后再走冒泡,看有没有函数监听
DOM2级事件模型包括三个阶段:
**事件捕获阶段:**该阶段的主要作用是捕获截取事件
**处于目标阶段:**事件的目标接收到事件,但不会 作出相应。一般地,该阶段具有双重范围,即捕获阶段的结束,冒泡阶段的开始;
**事件冒泡阶段:**主要作用是将目标元素绑定事件执行的结果返回给浏览器,处理不同浏览器之间的差异,主要在该阶段完成(响应)
- 疑问:那函数是不是要调用两次? 不是,可以自己选择把函数放在捕获阶段还是冒泡阶段
e.addEventLisenter('click',f2,true) // true按捕获方向执行函数
e.addEventLisenter('click',f2,false) // false按冒泡方向执行函数(这个也是默认值,就是最后不填布尔值,会默认是冒泡)
代码示例
一个彩虹圈圈,一层套一层,.x就是让所有的颜色变透明,接下来就是添加JS代码,让冒泡或捕获实现。根据调用的顺序把背景色依次亮起,通过事件监听依次取消.x
const level1 = document.querySelector('.level1');
const level2 = document.querySelector('.level2');
const level3 = document.querySelector('.level3');
const level4 = document.querySelector('.level4');
const level5 = document.querySelector('.level5');
const level6 = document.querySelector('.level6');
const level7 = document.querySelector('.level7');
let n = 1;
const removeX = (e) => {
// e只存在事件点击的一瞬间,会在事件结束后自动消亡
const t = e.currentTarget;
//点击时获取到e,让它继续存在
setTimeout(() => {
t.classList.remove('x');
}, n * 1000);
n += 1;
};
level1.addEventListener('click', removeX);
level2.addEventListener('click', removeX);
level3.addEventListener('click', removeX);
level4.addEventListener('click', removeX);
level5.addEventListener('click', removeX);
level6.addEventListener('click', removeX);
level7.addEventListener('click', removeX);
**冒泡:**默认如果不传参或者是false的话就是冒泡,彩虹会从内到外依次亮起(从div7到div1)
**捕获:**加上true,颜色会从外到内依次亮起,(从div1到div7)
level1.addEventListener('click', removeX,true);
level2.addEventListener('click', removeX,true);
level3.addEventListener('click', removeX,true);
level4.addEventListener('click', removeX,true);
level5.addEventListener('click', removeX,true);
level6.addEventListener('click', removeX,true);
level7.addEventListener('click', removeX,true);
捕获和冒泡同时存在:
let n = 1;
const removeX = (e) => {
const t = e.currentTarget;
setTimeout(() => {
t.classList.remove('x');
}, n * 1000);
n += 1;
};
const addX = (e) => {
const t = e.currentTarget;
setTimeout(() => {
t.classList.add('x');
}, n * 1000);
n += 1;
};
level1.addEventListener('click', addX);
level2.addEventListener('click', removeX,true);
level2.addEventListener('click', addX);
level3.addEventListener('click', removeX,true);
level3.addEventListener('click', addX);
level4.addEventListener('click', removeX,true);
level4.addEventListener('click', addX);
level5.addEventListener('click', removeX,true);
level5.addEventListener('click', addX);
level6.addEventListener('click', removeX,true);
level6.addEventListener('click', addX);
level7.addEventListener('click', removeX,true);
level7.addEventListener('click', addX);
在捕获过程中删除x,就是添加颜色。然后在冒泡过程中添加x,就是删除颜色
先把颜色一层一层加进来,在把颜色一层一层删除,也就是先走捕获再走冒泡
四、DOM事件委托
当监听子元素时,事件冒泡会通过目标元素向上传递到父级,直到document,如果子元素不确定或者动态生成,可以通过监听父元素来取代监听子元素。
一个很好的例子是一系列列表项,如果你想让每个列表项被点击时弹出一条信息,您可以将click单击事件监听器设置在父元素<ul>上,这样事件就会从列表项冒泡到其父元素<ul>上。并且父元素能够通过target判断是哪个子元素,从而做相应的处理
- 优点
- 大量减少内存占用,减少事件注册 // 假如100个子元素触发同一个监听函数,可以将此放在父元素上面。
- 新增元素实现动态绑定事件 // 用createElement创建的元素,可以将监听函数放到父元素上面。
-
举例
-
div1有1个span,100个button(click1~100),监听所有button,只需要在div1绑定就可以了
div1.addEventListener('click',(e)=>{ const t = e.target if(t.tagName.toLowerCase() === 'button'){ console.log('button 被点击了') console.log('button内容是'+t.textContent) console.log('button data-id是'+t.dataset.id) //获取data-id的值 } })这时点击span或者其他地方是不会触发事件监听的,但是无论点击哪个button,都会触发
-
监听目前不存在的元素的点击事件,可以先监听祖先
setTimeout(()=>{ const button = document.createElement('button') button.textContent = 'click 1' div1.appendChild(button) },1000) //1秒后才有button div1.addEventListener('click',(e)=>{ const t = e.target if(t.tagName.toLowerCase()==='button'){ console.log('button 被点击了') } }) //在父元素绑定判断监听
-
五、阻止冒泡
阻止冒泡,也可以称作DOM事件流阻断,也就是在冒泡过程中,DOM事件流可以在经过的任意事件处理函数中终止----event.stopPropagation(),一般用于封装一些独立的组件
补充
不可取消冒泡
有些事件不可取消冒泡,MDN搜索scroll event,看到Bubbles(该事件是否冒泡)和Cancelable(开发者是否可以取消冒泡)
scroll事件不可取消冒泡,但可以阻止滚动!
阻止scroll默认动作没有用,依旧可以滚动,因为先有滚动才有滚动事件
要阻止滚动,可以阻止wheel(滚轮)和touchstart(手机触摸滚动)的默认动作
注意需要找准滚动条所在元素
但是滚动条还能用,可用CSS让滚动条width:0(直接搜索hidden scrol bar )
CSS使用overflow:hidden可以直接取消滚动条,但此时JS依然可以修改scrollTop(滚动条存在与否是不能判断一个元素是否能滚动的)
滚动条出现在document
举例:
-
-
如果在
#x阻止冒泡是不能禁用滚动的,因为scroll不能阻止冒泡 -
阻止滚动的默认事件也是没有用的,默认事件是一个事件触发后才有的动作,所以以下代码没有任何作用
-
x.addEventListener('scroll',(e)=>{ e.stopPropagation() e.preventDefault() })
-
-
-
可以禁用滚轮(JS),隐藏滚动条(CSS),禁止触控(JS)
-
x.addEventListener('wheel',(e)=>{ e.preventDefault() }) //禁用滚轮 x.addEventListener('touchstart',(e)=>{ e.preventDefault() }) //禁用触摸滚动 -
::-webkit-scrollbar { width: 0 !important }
-
自定义事件
button1.addEventListener('click',()=>{ //当button1被点击时,触发一个事件
const event = new CustomEvent('huhu',{ //声明一个新的事件(事件名,{事件信息}
detail:{name:'huhu',age:18}, //事件内容
bubbles: true, //可以冒泡
cancelable:false //不可取消冒泡
})
button1.dispatchEvent(event) //触发event,只要点击button就会触发huhu事件
})
button1.addEventListener('huhu',(e)=>{
console.log('huhu 事件触发了')
console.log(e.detail)
}) //监听button1
div1.addEventListener('huhu',(e)=>{
console.log('huhu 事件触发了 div1监听成功')
console.log(e.detail)
}) //监听div1
<div id="div1">
<button id="button1">点击触发 huhu 事件</button>
</div>
封装一个事件委托
写一个函数,可以直接调用on事件
on('click','#div1','button',()=>{
console.log('button 被点击了')
}) //直接调用on事件可以在div1上看button有没有被click
补充on事件
setTimeout(() => {
const button = document.createElement('button');
const span = document.createElement('span');
span.textContent = 'click 1';
button.appendChild(span);
div1.appendChild(button);
}, 1000);
/***
* <div id='div1'>
* <button>
* <span>click 1</span>
* </button>
* </div>
* 点击click 1 就是点击span(span并没有绑定事件监听),冒泡到button,触发click事件
*/
on('click', '#div1', 'button', () => {
console.log('button 被点击了');
});
function on(eventType,element,selector,fn){
if(!(element instanceof Element)){
element = document.querySelector(element);
} //判断element是element还是选择器
element.addEventListener(eventType,(e)=>{
let t = e.target;
while(!t.matches(selector)){ //matches用来判断一个元素是否满足一个选择器
if(element === t){
t = null;
break;
} //判断父级元素到div1停止
t = t.parentNode; //如果t!==element,就等于他的爸爸
}
t && fn.call(this, e, t);
});
return element;
}
target currentTarget
e.target 用户操作的元素
e.currentTarget 程序员监听的元素
this是e.currentTarget
-
举例
div>span{文字},用户点击文字e.target就是spane.currentTarget是div
冒泡特例
只有一个div被监听(不考虑父子同时监听),那么用户点击的就是开发者监听的
这种情况就没有先捕获再冒泡的顺序,就是谁先监听谁先执行
参考资料