1 DOM事件模型
1.1 监听函数
浏览器的事件模型,就是通过监听函数(listener)对事件做出反应。事件发生后,浏览器听到了这个事件,就会执行对应的监听函数,这是事件驱动编程模式(event-driven)的主要编程方式。
1.2 JS绑定监听函数
- HTML 的 on 属性
<div onclick = 'console.log('')>
<div onclick = 'fn()' //函数名要加圆括号
- 元素节点的事件属性
widow.onload = fn; //等号右边是函数名,不加圆括号
div.onclick = function(event){
console.log('xx')
}
- DOM节点的 .addEventListener 方法
div.addEventListener('click',fn,true/false)
- 比较 第一种违反了 HTML 与 JavaScript 代码相分离的原则;
第二种,同一个事件只能定义一个监听函数,也就是说,如果定义两次 onclick属性,后一次定义会覆盖前一次;
第三种,推荐使用,优点有:同一个事件可以添加多个监听函数;能够指定捕获阶段还是冒泡阶段触发监听函数;除了DOM节点,其他对象(比如 window、XMLHttpRequest等)也有这个接口,它等于是整个 JavaScript 统一的监听函数接口;
1.3 this 的指向
监听函数内部的 this 指向触发事件的那个元素节点(e.currentTarget),以下三种方式的 this 都输出 btn ,不推荐使用 this 。
<button id = 'btn' onclick = 'console.log(this.id)'>点击</button>
<button id='btn'>点击</button>
btn.onclick=function(){
console.log(this.id)
}
btn.addEventListener('click',function(e){
console.log(this.id)
},false)
2 事件的传播
-
第一阶段:从 window 对象传到目标节点(从外到内),称为“捕获阶段(capture phase)”;
-
第二阶段:在目标节点上触发,称为“目标阶段”(target phase);
-
第三阶段:从目标节点传导回 window 对象(从内到外),称为“冒泡阶段”(bubbling phase)
-
先捕获,再冒泡,中间有函数就执行,没函数就跳过;
-
e 对象被传给所有监听函数;
-
事件结束后,e 对象会被浏览器悄悄修改,不推荐在事件结束后访问 e ,要用的话提前将属性复制到自己的变量里:
conse a = e.currentTarget
3 target 和 currentTarget
- 区别:
-
e.target :用户操作的元素
-
e.currentTarget :程序员监听的元素
-
this 是 e.currentTarget ,不推荐使用
<div>
<span>文字</span> //用户点击文字,e.target 就是
</div> //文字,e.currentTarget 就是 div
4 取消冒泡和默认动作
-
event.stopPropagation() :阻止捕获和冒泡阶段中当前事件的进一步传播;
-
event.stopPropagation() 只能阻止这个事件的传播,不能取消这个事件;
// 事件传播到 p 元素后,就不再向下传播了`
p.addEventListener('click', function (event) {
event.stopPropagation();
console.log(1) //冒泡被阻止,函数会触发,输出1;
}, true);
// 事件冒泡到 p 元素后,就不再向上冒泡了`
p.addEventListener('click', function (event) {
event.stopPropagation();
console.log(2) //冒泡被阻止,函数会触发,输出2;
}, false);
- 如果要彻底取消函数,不在触发后面所有 click 的监听函数,可以使用 stopImmediatePropagation 方法:
p.addEventListener('click', function (event) {
event.stopImmediatePropagation();
console.log(1); //只输出1;
});
p.addEventListener('click', function(event) {
// 不会被触发
console.log(2);
});
- Cancelable :是否可以阻止默认事件,与冒泡无关;
5 事件代理
由于事件在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方法就叫事件代理(delegation):
let ul = document.querySelector('ul')
ul.addEventListener('click',function(e){
if(e.target.tagName.toLowerCase() === 'li'){
console.log('xxx')
}
}
// click 事件的监听函数定义在 ul 节点,但是处理的是子节点 li 的 click 事件。好处是:只要定义一个监听函数,就能处理多个子节点的事件,而不用在每个 li 节点上定义监听函数。而且以后再添加子节点,监听函数依然有效。
- 如何监听目前不存在的元素(动态元素) 监听祖先,等点击的时候看看是不是自己想要监听的元素即可;
<div id="div1">
</div>
setTimeout(()=>{
const button = document.createElement('button')
button.textContent = 'click1'
div1.appendChild(button)
},3000)
div1.addEventListener('click',(e)=>{
const t = e.target
if(t.tagName.toLowerCase() === 'button'){
console.log('button 被 click 了')
}
})
6 阻止滚动
-
scroll事件不能阻止默认动作;
-
要阻止滚动,可阻止 wheel 和 touchstart 的默认动作;
-
要找准滚动条所在的元素;
-
可以用 CSS 让滚动条 width:0;
-
可以用 overflow:hidden 直接取消滚动条,但此时 JS 依然可以修改 scrollTop ;
x.addEventListener('wheel', (e)=>{
console.log(2)
e.preventDefault() //阻止滚动事件
})
x.addEventListener('touchstart', (e)=>{
console.log(2)
e.preventDefault() //阻止触摸事件
})
::-webkit-scrollbar { width: 0 !important } //移动端隐藏滚动条
- 封装事件委托
<div id="div1">
</div>
setTimeout(()=>{
const button = document.createElement('button')
button.textContent = 'click1'
div1.appendChild(button)
},3000)
on('click','#div1','button',()=>{
console.log('xxxx')
})
function on(eventType,element,selector,fn){
if(!(element instanceof Element)){
element = document.querySelector(element)
}
element.addEventListener(eventType,(e)=>{
const t = e.target // t 为button
if(t.matches(selector)){
fn(e)
}
})
}
//将上述代码改为:内容被 span 包围,span 放在 button 中:
setTimeout(()=>{
const button = document.createElement('button')
const span = document.createElement('span')
span.textContent = 'click1'
div1.appendChild(button)
button.appendChild(span)
},3000)
on('click','#div1','button',()=>{
console.log('xxxx')
})
function on(eventType,element,selector,fn){
if(!(element instanceof Element)){
element = document.querySelector(element)
}
element.addEventListener(eventType,(e)=>{
let el = e.target //el初始值为 span ,与selector的值 button 不匹配;
while(!el.matches(selector)){
if(element === el){
el = null //认为元素不存在
break
}
el = el.parentNode
}
el && fn.call(el,e,el)
})
return element
}
7 浏览器事件
-
DOM 事件不属于 JS 的功能,是浏览器提供的 DOM 的功能,JS 只是调用了 DOM 提供的 addEventListener 而已。