DOM事件机制
Hello,今天来总结一下JS编程接口中的 DOM 事件。
DOM 事件其实分为几个等级,分别是 DOM0 级,DOM2 级和 DOM3 级。其中 DOM0 级事件主要是在当前元素的冒泡阶段(或目标阶段)执行的;DOM2 级事件则在 DOM0 级事件的基础上支持同时绑定多个处理函数,主要定义了addEventListener和removeEventListener两个方法;DOM3 级事件则添加了诸如UI事件、焦点事件、鼠标事件、滚轮事件以及使用者自定义事件等。详细的描述比较深入,感兴趣的朋友可以参考juejin.cn/post/684490…
一、 DOM事件模型
1. 首先来看一个简单的代码例子:
<div class = granfa> <div class = fa> <div class = son> 123 </div> </div> </div>复制代码
其中,标签为 son 的 div 被标签为 fa 的 div 包裹,granfa 类似。
首先我们要明确,当用户点击'123'时,既是点击了 sun ,又点击了 fa,也点击了 granfa。如果这三层 div 都有执行函数的话,那么三个执行函数都将被执行。
而至于执行的顺序,不同的浏览器给出了不同的方案:
IE9 以下的浏览器使用事件冒泡,先从最内层的元素开始寻找监听函数,从内到外依次执行,有监听函数时即调用,并提供事件信息,无则跳过;
Netscapte 采用事件捕获,从外到内寻找监听函数;
W3C 制定的标准则是二者都采纳,可以多传进一个 bool 值来决定是在冒泡时执行函数还是捕获时执行。
但是值得注意的是,无论采用哪种方式,事件捕获、目标阶段、事件冒泡这个过程一定会按顺序走完,不同的布尔值只是决定了在什么阶段调用函数
以下可以来稍作总结:
事件捕获阶段:事件从 window 对象自上而下向目标节点传播的阶段;
目标阶段:真正的目标节点正在处理的事件阶段;
冒泡阶段:事件从目标节点自下而上向 window 对象传播的阶段。
2. 最重要的函数:addEventListener
想要进行事件绑定,我们可以使用 W3C 提供的标准API:
baba.addEventListener('click', fn, bool)
该API依次传入的是事件类型,监听函数以及布尔值。其中不传布尔值或传入 falsy 值,就表示让 fn 函数走冒泡阶段,当浏览器在冒泡阶段发现 baba 元素有 fn 监听函数时,就会调用 fn ,并提供事件信息;当传入的布尔值为 true 时,则走捕获。
3. target 与 currentTarget
在处理DOM事件时,我们可以使用 e.currentTarget 来表示当前时刻的事件。由于监听事件会在点击等操作结束后消失,所以我们必须要用一个中间值来记录下该事件。
其中,target 与 currentTarget 最大的不同是:
target 表示用户所操作的函数
currentTarget 表示程序员所监听的元素,也即this所知的元素
但是当只有一个 div 被监听时(即不考虑父元素和子元素同时被监听),fn 分别在捕获阶段和冒泡阶段被监听 click 事件,那么此时的 e.target === e.currentTarget。此时按照顺序,如果捕获监听在先那就先执行捕获,如果冒泡在先就先冒泡。
4. 取消冒泡
我们可以强制取消冒泡,但是没有办法取消捕获。此时只需要执行e.stopPropagation()
即可。
所有的冒泡都可以被取消,而有些默认动作不可被取消。
以 scroll 事件为例,因为是先滚动,滚动结束后才有默认动作,所以我们即使取消了滚动的默认动作,滚动效果依然存在,只是滚动结束之后的默认动作被我们取消了。我们就以 scroll 事件为例,讲一下如何取消页面滚动。
此时,我们只需要找到‘滚动’这个动作所在的元素即可。通过开发者工具不难发现,滚动是发生在 document 上的,并非我们所监听的 div 或 body,所以我们只需要把监听的元素找准,然后阻止其默认动作即可:
x.addEventListener('wheel',(e)=>{ e.preventDefault(); })复制代码
然后我们再用 CSS 隐藏 scrollbar,就可以实现强制取消滚动,并且隐藏滚动条。而当我们在手机上实现时,只需要把 wheel 改成 touchstart 即可。
5. 自定义事件
我们举个例子,当我们拥有一个 div1 ,其中包含一个 button1 ,内容是'点击触发 dodo 事件'
<div id="div1"> <button id="1"> 点击触发 dodo 事件 </button> </div>复制代码
此时只需要在JS中输入:
button1.addEventListener('click',()=>{ const event = new CustomEvent('dodo',{ detail:{name:'dodo',age:18}; bubbles: true; //表示可冒泡 cancelable: false //与冒泡无关,表示开发者不可阻止默认事件 }) button1.dispatchEvent(event) })复制代码
二、事件委托
事件委托也叫事件代理,由于事件会在冒泡的过程中向上传播到父节点,那么我们只需要把监听函数定义在父节点上,即可实现同时监听多个子元素事件的目的。
使用事件委托可以在需要在同一父节点下同时监听多个子元素时大大减少内存的占用,提高性能;同时我们可以动态绑定事件,当事件为被创造出来时也可避免须重新绑定事件的麻烦。
假设 div1 是多个子元素的父节点,要实现事件委托,我们可以:
div1.addEventListener('click',(e)=>{ const t = e.target; if(t.targetName.toLowerCase()==='button'){ console.log('button is been clicked') } })复制代码
那么我们就实现了当用户点击 div1 中任意 button 元素时,都会打印出'button is been clicked'。
当然,为了方便使用,我们可以对事件委托进行封装:
//要实现的封装效果如下: on('click','#div1','button',()=>{ console.log('button is been clicked') }) //封装内容如下: function on(eventType,element,selector,fn){ if(!(element instanceof Element)){ element = document.querySelector(element) } element.addEventListener(eventType,(e)=>{ const t=e.target; if(t.matches(selector)){ fn(e); } }) }复制代码
但是该方法不完全正确,我们还需要添加一个递归判断,来防止这个东西被包裹住:
on: function(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 && fn.call(el, e, el) }) return element },复制代码
这样就是一个完整的事件委托的封装了。
作者:藤原拓柒
链接:https://juejin.cn/post/7022851393677426695