DOM事件和事件委托

95 阅读3分钟

1. DOM事件模型

1.1 调用顺序

    <div class="oldfather">
        <div class="father">
            <div class="son">
                文字
            </div>
        </div>
    </div>

分别为三个div添加事件监听为:fnYe/ fnBa /fnEr
fnYe -> fnBa -> fnEr 的调用顺序叫做事件捕获。
fnEr -> fnBa -> fnEr 的调用顺序叫做事件冒泡。
事件捕获:从外向内寻找监听函数。
事件冒泡:从内向外寻找监听函数。

1.2 DOM事件模型

事件触发: 当添加事件的监听时,由开发者决定事件捕获阶段是否触发,冒泡阶段是否触发。
事件查询: 当事件被触发后,会先按照事件捕获的顺序查询是否有事件触发,再按照事件冒泡的顺序查询是否有事件触发。
事件绑定:

EventTarget.addEventListener('click',fn,bool)

参数bool决定是否在冒泡阶段被触发。默认为false,不传或falsy先冒泡,true先捕获。只会在冒泡阶段或捕获阶段触发,不能在两个阶段都触发。

1.3 冒泡无效

场景:当用户监听的元素与用户操作元素一致时,addEventListener()的bool参数无效。 因为在上述场景不存在标签的父子关系,哪个事件先监听,哪个事件先触发。

1.4 冒泡终止

event.stopPropagation()可以中断冒泡,让浏览器对事件冒泡的查询终止。

2.事件委托

2.1 事件委托

场景:当需要给100个button设置事件监听,如何处理?
通过给button加上一个共有的父标签,利用事件冒泡,监听事件。

    <div id="div1">
        <button>按钮1</button>
        <button>按钮2</button>
        ...
        <button>按钮100</button>
    </div>
div1.addEventListener('click',(e)=>{
    const t = e.target
    if(t.tagName.toLowerCase() === 'button'){
        console.log('点击了button')
        console.log(t.textContent)
    }
})

设置对共有的父标签的监听,监听其子标签的事件触发。

通过获取触发事件的元素内容,得知具体的触发元素。

通过这种方式,仅需要一个监听器就能实现,该方式被称作事件委托。事件委托能够对还未存在的元素进行监听。

更好的方式是通过给标签加入属性data-id实现。

    <div id="div1">
        <button data-id='1'>按钮1</button>
        <button data-id='2'>按钮2</button>
        ...
        <button data-id='100'>按钮100</button>
    </div>

为每个标签设置属性data-id。

div1.addEventListener('click',(e)=>{
    const t = e.target
    if(t.tagName.toLowerCase() === 'button'){
        console.log('点击了button')
        console.log(t.dataset.id)
    }
})

并通过.dataset.id获取其data-id。

2.2 封装事件委托

输入:事件类型,监听元素,匹配具体元素的选择表达式,函数动作。

输出:用户触发元素的data-id。

function on(eventType,element,selector,fn){
    if(!(element instanceof Element)){
        element = document.querySelector(element)
    }
    element.addEventListener(eventType,(event)=>{
        const target = event.target
        if(target.matches(selector)){
            fn(event)
        }
    })
}

对先前事件委托的实现进行了封装。

.matches()用于获取元素内满足选择器条件的元素。

当参数element为选择器时,获取其选择器对应的元素。

2.3 递归判断

存在一个问题,场景如下:

    <div id="div1">
        <button data-id='1'><span>按钮1</span></button>
        <button data-id='2'><span>按钮2</span></button>
        ...
        <button data-id='100'><span>按钮3</span></button>
    </div>

当button内用span标签存储文字内容,此时用户点击的是span标签。

通过on('click','#div1','button',fn)这种方式进行事件委托,需要将参数'button'更改为'span'。

如此每次进行事件委托,都需要确定具体的html结构,很麻烦,无法根据视图直接决定selector参数。

因此需要通过一个递归判断,用户操作的元素的父级衍生是否存在button。 因此需要通过一个递归判断,用户操作的元素的父级衍生是否存在button。

function on(eventType,element,selector,fn){
    if(!(element instanceof Element)){
        element = document.querySelector(element)
    }
    element.addEventListener(eventType,(event)=>{
        const target = event.target
        while(!target.matches(selector)){
            if(element === target){
                target = null
                break
            }
            target = target.parentNode
        }
        target ? fn.call(target,element,target) : console.log('没有满足选择器'+selector+'的元素')
        
    })
}

逻辑:target为用户操作的元素,当用户操作的元素与参数selector的条件不匹配,则获取target的父元素,并在判断是否与selector匹配,直到找到匹配的元素,并作为fn的参数。若一直找到element,则不存在匹配的元素。