1. DOM事件模型
在W3C规范中,DOM level 0 有两种方式绑定事件,
一是直接将事件作为属性写在HTML标签上,如下面所示
<button onclick = 'click'>click it</button>
function click(){
console.log('hello')
}
二是,在JavaScript中获取元素后绑定
let button = document.querySelector('button')
button.onclick = f1
button.onclick = f2 // 会覆盖上一句
上述例子中,点击button后,只会触发f2方法。
对button解除事件绑定,如下所示
button.onclick = null
DOM level 2,是通过addEventListener对元素绑定事件
let button = document.querySelector('button')
button.addEventListener('click',f1)
button.addEventListener('click',f2)
根据DOM 2标准,点击button会触发f1,f2
因为addEventListener有事件队列,当事件触发后,会按照绑定的顺序,依次执行对应的方法
对元素解除绑定,使用removeEventListener
function f1(){
console.log('11111');
}
function f2(){
console.log('22222');
}
btn.addEventListener('click',f1);
btn.addEventListener('click',f2);
btn.removeEventListener('click',f1);
点击button后,只会触发f2
上述两种事件模型的区别是
关于上图提到的事件传播,决定了元素以哪个顺序接收事件,具体指的是事件捕获和事件冒泡
2. 事件流
事件流:描述页面元素接受事件的顺序,分为事件捕获和事件冒泡,两种顺序
DOM事件流的三个阶段
-
事件捕获阶段,根据
DOM树结构,事件对象通过目标的祖先从窗口传播到目标的父级,即从上往下找监听函数,称之为事件捕获。 -
处于目标阶段,事件对象到达目标元素。如果事件类型表明没有冒泡,则事件对象将在此阶段完成后停止传播。
-
事件冒泡阶段,事件对象以相反的顺序,从目标的父级开始,通过目标的祖先元素传播,以
Window结束,即从下往上找监听函数,称之为事件冒泡。
早期IE浏览器使用
attachEvent('onclick',fn) //事件冒泡
但是网景公司采用
addEventListener('click',fn) //事件捕获
最后,W3C制定标准为
addEventListener('click',fn,boolean) // boolean为falsy或者不传,默认事件冒泡;为true,事件捕获
下面例子中捕获阶段的部分顺序是div-->p-->button,冒泡阶段的顺序为button-->p-->div,有一个p是属于捕获阶段,所以先打印,剩下的走冒泡阶段,所以顺序是button-->div
<div id="parentp">
<p id="child">
<button id='btn'>click</button>
</p>
</div>
btn.addEventListener('click',()=>{console.log('btn clicked')});
child.addEventListener('click',()=>{console.log('child clicked')},true);
parentp.addEventListener('click',()=>{console.log('parent clicked')});
child clicked
btn clicked
parent clicked
无论是事件捕获,还是事件冒泡阶段,都有是事件相关信息e,当事件触发时,获取到的事件信息是准确可信的,
e.target && e.currentTarget
上述例子中,改写一下
btn.addEventListener('click',(e)=>{console.dir(e.target);console.dir(e.currentTarget)});
child.addEventListener('click',(e)=>{console.dir(e.target);console.dir(e.currentTarget)});
parentp.addEventListener('click',(e)=>{console.dir(e.target);console.dir(e.currentTarget)});
点击按钮,打印出如下
button#btn
button#btn
button#btn
p#child
button#btn
div#parentp
从上面可以看出
e.target 指的是当前被点击的元素
e.currentTarget 指的是被监听的元素,此时,使用的this也指的是currentTarget,不推荐使用this,以防误解
注意:
- 若是对同一个元素同时监听捕获和冒泡阶段,执行顺序是否与方法写的位置有关呢?
btn.addEventListener('click',(e)=>{console.log('冒泡')});
btn.addEventListener('click',(e)=>{console.log('捕获')},true);
输出如下,可以得出,与位置无关,仍然是先捕获后冒泡
捕获
冒泡
- 捕获阶段不可以取消,但冒泡可以
e.stopPropagation()
btn.addEventListener('click',(e)=>{e.stopPropagation();console.log('btn clicked')});
child.addEventListener('click',()=>{console.log('child clicked')},true);
parentp.addEventListener('click',()=>{console.log('parent clicked')});
输出为
child clicked
btn clicked
-
阻止默认行为,部分事件不可以阻止默认行为(可以通过搜索
scroll event MDN,Cancelable为Yes表示可以阻止,No为不可以阻止)e.preventDefault()阻止事件默认行为click 事件可以阻止默认行为
<a id="test" href="https://www.google.com/">跳转</a> test.addEventListener('click',(e)=>{e.preventDefault();console.log('跳转了')}) //点击后,会打印出‘跳转了’,但是不会跳转scroll事件不可以阻止默认行为,需要先滚动才有滚动事件,如果无法滚动,产生不了事件对象,无法阻止想要阻止滚动,PC端可以使用阻止
wheel事件默认行为,移动端可以使用阻止touchstart事件的默认行为
3. 自定义事件
3.1 事件的创建与触发
最简单的创建事件的方法是,使用Event构造函数
let myEvent = new Event('event_name')
如果要传递数据,需要使用CustomEvent构造函数
let myEvent = new CustomEvent('event_name', {
detail:{
// 此处填充待传递的数据
}
})
DOM中的事件,都是有相应操作后触发的,而自定义事件,需要自己手动(dispatchEvent)触发
let myEvent = new CustomEvent('testE', {
detail: {
name: 'zhangSan',
age: '18'
}
});
let btn = document.querySelector('button');
btn.onclick = () => {
btn.dispatchEvent(myEvent) //触发自定义事件
}
btn.addEventListener('testE', (e) => {
console.log(e.detail.name) //点击button,后打印zhangSan
})
3.2 自定义事件的使用场景
自定义事件相当于是 观察者模式,可以把复杂逻辑解耦,代码可以写的很清晰,而且很容易复用
比如,点击按钮后,触发A动作,A完成后,触发B,B完成后,触发C。可以将A、B、C分为三个不同的事件,可以分开实现,便于维护
(待完善...)
4 事件委托
4.1 事件委托介绍
事件委托:利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。即委托祖先元素代为执行事件
常见的使用场景
-
监听子元素的事件。
比如
ul下有多个li,li被点击后,背景颜色发生变化,我们可以监听ul,然后在其click事件中通过e.target匹配到对应的li元素,让其背景颜色变化 -
监听新增元素的事件
比如,上述例子中动态新增了一个
li,新增的也要满足上述的要求
借鉴大牛们解释事件委托的例子,
有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台MM代为签收。现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收。
这里其实还有2层意思的:
第一,现在委托前台的同事是可以代为签收的,即程序中的现有的DOM节点是有事件的;
第二,新员工也是可以被前台MM代为签收的,即程序中新添加的DOM节点也是有事件的。
let ul = document.querySelector('ul')
ul.addEventListener('click', (e) => {
let target = e.target
if (target.tagName.toLowerCase() === 'li') {
target.style.backgroundColor = 'red'
}
})
// 新增元素也同样响应此事件
let btn = document.querySelector('button')
btn.onclick = () => {
let li6 = document.createElement('li')
li6.style.className = 'li6'
li6.innerHTML = '第6个li'
ul.appendChild(li6);
}
如果对不同的li操作不一样,也可以对不同的li进行标记(比如添加不同的类,或者不同的自定义属性),然后一一匹配,实现对应的操作,减少获取DOM节点的操作
事件委托 参考此处
4.2 封装事件委托
function on(element, eventType, selector, fn) {
element.addEventListener(eventType, (e) => {
let el = e.target
while(!(el.matches(selector))){
if(element === el){
el = null
break
}
el = el.parentNode
}
el && el.fn.call(el,e,el)
})
}
matches 如果元素被指定的选择器字符串选择,Element.matches() 方法返回true; 否则返回false
如果被点击元素,不是被操作的元素,例如,ul>li>span,原本需要操作的元素是li,但是e.target是span,所以需要循环获取父元素,直到e.target能够和selector匹配;但如果查找到被委托元素仍未匹配成功,则不再查找(正常情况下应该不会出现这种场景)