面试题:你知道DOM事件有哪些级别吗?
面试题:描述一下DOM事件模型和DOM事件流?
面试题:描述一下DOM事件捕获的具体流程?
面试题:Event对象有哪些常见应用?你知道什么是自定义事件吗?
一、DOM事件级别
DOM一共分为4个级别:DOM0级,DOM1级,DOM2和DOM3级。
DOM事件一共分为3个级别:DOM0级事件处理,DOM2级事件处理,DOM3级事件处理。
由于DOM1级并没有规定事件相关内容,所以没有DOM1级事件。
1、DOM0级事件
el.οnclick=function(){}
如果给同一个元素/标签绑定多个同类型事件,只有最后一次会被执行。
<button id="btn">Click Me!</button>
<script>
const btn = document.getElementById('btn')
btn.onclick = function(){
alert("第一个事件处理函数")
}
btn.onclick = function(){
alert("第二个事件处理函数")
}
btn.onclick = function(){
alert("第三个事件处理函数")
}
</script>
2、DOM2级事件
el.addEventListener(event-name,callback,useCapture)
- event-name:事件名称,可以是标准DOM事件
- callback:回调函数,当事件触发时,函数会被注入一个参数:但概念的事件对象event
- useCapture:默认是false,代表事件句柄在冒泡阶段执行。ture则为捕获阶段。
通过el.addEventListener添加的事件只能通过el.removeEventListener来移除,传入的参数与addEventListener相同。
el.addEventListener添加的事件时,如果传入的是匿名函数,那么将无法移除事件监听。因为两个匿名函数的内存地址是不相同的,故推荐传入具名函数。
通过el.addEventListener可以为同一元素添加多个事件监听。
(如以下代码,事件1和事件2均会执行,事件3被移除了)
<button id="btn">Click Me!</button>
<script>
const btn = document.getElementById('btn')
btn.addEventListener('click',test1)
btn.addEventListener('click',test2)
btn.addEventListener('click',test3)
function test1(e){
console.log(e)
alert('第一个事件')
}
function test2(e){
console.log(e)
alert('第二个事件')
}
function test3(){
console.log(e)
alert('第三个事件')
}
btn.removeEventListener('click',test3)
</script>
3、DOM3级事件
在DOM2级事件的基础上添加了更多的事件类型。
- UI事件:当用户与页面上的元素交互时触发,如load,scroll
- 焦点事件:当元素获得或失去焦点时触发,如focus,blur
- 鼠标事件:当用户通过鼠标在页面执行操作时触发,如click/dblclick,mousedown/mouseup,mouseout/mouseover
- 滚轮事件:当使用鼠标滚轮时触发,如mousewheel
- 键盘事件:当用户通过键盘触发页面操作时,如keyup,keydown
- 合成事件:当为输入法编辑器输入字符时触发,compositionstart
- 变动事件:当底层DOM结构发生变化时触发,如DOMsubtreeModified
- DOM3级事件允许使用者自定义一些事件
二、DOM事件模型和事件流
DOM事件模型分为捕获和冒泡。
事件流:一个事件发生之后,会在子元素和父元素之间传播。这种传播分为3个阶段。
(1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段
(2)目标阶段:真正的目标节点正在处理事件的阶段。
(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。
三、DOM事件捕获流程
捕获过程:(自上而下)window→document→html→body→...普通html结构..→目标元素
而冒泡过程则是捕获过程的逆过程。
<div id="outer">
<div id="inner">inner</div>
</div>
<script>
window.onclick=function(){
console.log('window');
}
document.onclick=function(){
console.log('document');
}
document.documentElement.onclick=function(){
console.log('html');
}
document.body.onclick=function(){
console.log('body')
}
const outer = document.getElementById('outer')
const inner = document.getElementById('inner')
outer.onclick=function(){
console.log('out')
}
inner.onclick=function(){
console.log('inner')
}
</script>
输出结果: inner → out → body → html → document → window
绑定的事件处理函数是在当前元素事件行为的冒泡阶段(或目标阶段)执行的。
四、事件代理(事件委托)
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数绑定在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方法叫事件代理(事件委托)。
面试题:有一个id为list的列表,要求:往其中插入100个节点,点击每个列表项时,能弹出是第几个。
<ul id="list"></ul>
<script>
(()=>{
const container = document.getElementById('list')
// 创建文档片段,此时还没有插入到DOM树中
const fragments = document.createDocumentFragment()
// 动态创建节点并组合到文档片段中
for(let i = 0; i < 100; i++){
const li = document.createElement("li")
li.innerHTML = `item-${i+1}`
fragments.appendChild(li)
}
// 一次性插入DOM树中
container.appendChild(fragments)
container.addEventListener('click',function(e){
if(e.target.tagName === 'LI'){
alert(e.target.innerHTML)
}
})
})()
</script>
追问:那如果是插入50000个节点呢?
<ul id="list"></ul>
<script>
(()=>{
const container = document.getElementById('list')
const TOTAL = 50000
const BATCH_SIZE = 10 // 每批插入节点次数,越大越卡
const BATCH_COUNT = TOTAL / BATCH_SIZE // 需要批处理多少次
let batchDone = 0 // 已经完成的批处理个数
function appendItems(){
const fragments = document.createDocumentFragment()
for(let i = 0; i < BATCH_SIZE; i++){
const item = document.createElement("li")
item.innerHTML = `item-${(batchDone * BATCH_SIZE ) + i + 1}`
fragments.appendChild(item)
}
container.appendChild(fragments)
batchDone += 1
batchAppend()
}
function batchAppend(){
if(batchDone < BATCH_COUNT){
window.requestAnimationFrame(appendItems)
}
}
batchAppend()
container.addEventListener('click',function(e){
if(e.target.tagName === 'LI'){
alert(e.target.innerHTML)
}
})
})()
</script>
五、Event对象常见应用
1、event.preventDefault()
阻止默认行为。
例如表单点击提交按钮跳转页面,a标签默认页面跳转或者锚点定位
如果使用a标签当做普通按钮,实现点击功能,既不跳转页面,也不锚点定位:
① href="javascript:;"
<a href="javascript:;">Click</a>
②return false
<a href="http://www.baidu.com">Click</a>
<script>
const a = document.getElementsByTagName("a")[0]
a.onclick=function(e){
return false
}
</script>
③e.preventDefault()
<a href="http://www.baidu.com">Click</a>
<script>
const a = document.getElementsByTagName("a")[0]
a.onclick=function(e){
e.preventDefault()
}
</script>
2、event.stopPropagation()
阻止事件冒泡到父元素,阻止任何父元素处理程序被执行
以下代码仅输出inner
<div id="outer">
<div id="inner">inner</div>
</div>
<script>
window.onclick=function(){
console.log('window');
}
document.onclick=function(){
console.log('document');
}
document.documentElement.onclick=function(){
console.log('html');
}
document.body.onclick=function(){
console.log('body')
}
const outer = document.getElementById('outer')
const inner = document.getElementById('inner')
outer.onclick=function(){
console.log('out')
}
inner.onclick=function(e){
console.log('inner')
e.stopPropagation()
}
</script>
3、event.stopImmediatePropagation()
既能阻止事件向父元素冒泡,也能阻止元素同类型事件的其它监听被触发。而stopPropagation只能实现前者的效果。
如下代码,id为inner的元素绑定了2个事件处理函数,在第一个事件处理函数中,如果只写了e.stopPropagation(),将阻止事件向父元素冒泡,但事件2的处理函数仍然会执行。
但如果换成e.stopImmediatePropagation(),则不仅阻止事件向父元素冒泡,事件2的处理函数也不执行。
<div id="outer">
<div id="inner">inner</div>
</div>
<script>
window.onclick=function(){
console.log('window');
}
document.onclick=function(){
console.log('document');
}
document.documentElement.onclick=function(){
console.log('html');
}
document.body.onclick=function(){
console.log('body')
}
const outer = document.getElementById('outer')
const inner = document.getElementById('inner')
outer.onclick=function(){
console.log('out')
}
inner.addEventListener('click',function(e){
console.log('inner -- 事件1');
// e.stopPropagation()
e.stopImmediatePropagation()
})
inner.addEventListener('click',function(e){
console.log('inner -- 事件2');
})
</script>
4、event.currentTarget & event.target
event.target是事件触发的元素,event.currentTarget是当事件沿着DOM触发时事件的当前目标,它总是指向事件绑定的元素。
如下代码,当点击id4的div时,控制台依次输出:
e.target:id4,e.currentTarget:id4
e.target:id4,e.currentTarget:id3
e.target:id4,e.currentTarget:id2
e.target:id4,e.currentTarget:id1
<div id="id1">
<div id="id2">
<div id="id3">
<div id="id4"></div>
</div>
</div>
</div>
<script>
const box1 = document.getElementById('id1')
const box2 = document.getElementById('id2')
const box3 = document.getElementById('id3')
const box4 = document.getElementById('id4')
box1.addEventListener('click',function(e){
console.log(`e.target:${e.target.id},e.currentTarget:${e.currentTarget.id}`)
})
box2.addEventListener('click',function(e){
console.log(`e.target:${e.target.id},e.currentTarget:${e.currentTarget.id}`)
})
box3.addEventListener('click',function(e){
console.log(`e.target:${e.target.id},e.currentTarget:${e.currentTarget.id}`)
})
box4.addEventListener('click',function(e){
console.log(`e.target:${e.target.id},e.currentTarget:${e.currentTarget.id}`)
})
</script>
六、自定义事件
Event 与 CustomEvent
window.addEventListener() 添加事件监听
window.dispatchEvent() 抛出事件
Event算是一个顶级接口,CustomEvent基于Event,增加了部分参数
setTimeout(() => {
// 自定义事件
const test = new Event('test')
test.app = '额外参数'
const result = window.dispatchEvent(test)
console.log(result); // 返回的是一个布尔值
}, 3000);
function testHandler(params) {
console.log('自定义事件',params) // params是一个event对象
}
window.addEventListener('test',testHandler)
setTimeout(() => {
// 第二个参数可以直接传入参数
const customEvent = new CustomEvent('customEvent',{
detail:{
name:"eric",
height:180,
weight:120
},
bubbles:true, // 表示事件是否可以冒泡,默认不冒泡
cancelable:false // 表示该事件是否可以取消
})
// 也可以直接在事件对象上绑定参数
customEvent.app = 'customEvent的额外参数'
window.dispatchEvent(customEvent)
}, 3000);
function customEventHandler(params) {
console.log("customEvent",params) // params是一个CustomEvent对象
}
window.addEventListener('customEvent',customEventHandler)