当前位置: 首页 > Web前端 > vue.js

JavaScript事件委托详解

时间:2023-04-01 02:03:19 vue.js

基本概念事件委托,通俗地说,就是将一个元素响应事件(click,keydown...)的功能委托给另一个元素;一般来说,一个或一组元素的事件委托给它的父层或外层元素。外部元素是实际绑定事件的元素。当事件响应需要绑定的元素时,会通过事件冒泡机制触发。在外层元素的绑定事件上,然后在外层元素上执行函数。比如一个宿舍的同学同时到达,一种办法是一个一个去领取,或者把事情委托给宿舍长,让一个人出去把所有的快递都取走。然后根据收件人一一分发给每个宿舍学生;这里,取快递是一个事件,每个学生指的是需要响应该事件的DOM元素,出门收快递的宿舍长就是agent元素。因此,实际上是这个元素绑定了事件。根据收件人分配快递的过程在事件的执行中。需要确定被代理元素中的哪一个或哪几个应该匹配当前响应事件。事件冒泡前面说过,DOM中事件委托的实现使用了事件冒泡的机制,那么什么是事件冒泡呢?在document.addEventListener的时候,我们可以设置事件模型:事件冒泡,事件捕获,一般来说都是使用事件冒泡模型;如上图所示,事件模型分为三个阶段:捕获阶段:在事件冒泡模型中,捕获阶段不会响应任何事件;目标阶段:目标阶段是指对触发事件的底层元素的事件响应;冒泡阶段:冒泡阶段是事件触发响应会从底层target逐层到最外层(根节点),事件代理使用事件冒泡机制绑定内层需要响应的事件外层;###事件委托的优点1.减少内存消耗想象一下,如果我们有一个包含大量列表项的列表,我们需要在列表项被点击时响应一个事件;

  • 第1项
  • 第2项
  • 第3项
  • ......
  • 第n项
  • //......表示中间还有未知数如果li给每一个列表项都绑定一个函数,会消耗大量内存,效率上也会消耗很多性能;因此,更好的办法是将这个点击事件绑定到它的父层,也就是ul上,然后在执行事件的时候匹配判断目标元素;所以事件委托可以减少很多内存消耗,节省效率。2、动态绑定事件比如上面例子中只有几个列表项,我们给每个列表项绑定事件;很多情况下,我们需要通过AJAX或者用户操作来动态添加或者删除列表项元素,那么每次有变化,都需要重新绑定事件到新添加的元素,解除绑定事件到该元素即将被删除;如果使用事件委托就没有这样的麻烦,因为事件是绑定到父层的。与目标元素的增减无关,在实际响应执行事件函数的过程中匹配到目标元素的执行;所以在动态绑定事件的情况下使用事件可以减少很多重复性的工作。jQuery中的事件委托相信很多人都用过jQuery中的事件委托。主要有这几种方式实现:**$.on**:基本用法:$('.parent').on('click','a',function(){console.log('clickeventontaga');}),是.parent元素下的a元素对$('.parent')的事件代理,只要这个元素有点击事件,就会自动找到a元素在.parent元素下,然后响应事件;**$.delegate**:基本用法:$('.parent').delegate('a','click',function(){console.log('clickeventontaga');}),as上面,还有对应的$.delegate,用来删除委托事件;**$.live**:基本用法:$('a',$('.parent')).live('click',function(){console.log('标签a上的点击事件');}),同上,但是如果没有输入父层元素$(.parent),那么事件默认会委托给$(document);(废除)功能的基本实现。比如我们有这样一个HTML片段:
  • item1
  • item2
  • item3
  • ......
  • 第n项
  • //.....意味着中间还有未知数量的lis。我们来实现#list下的li元素对其父层元素的事件委托,也就是#list://给父层元素绑定事件document.getElementById('list').addEventListener('click',function(e){//兼容性处理varevent=e||window.event;vartarget=event.target||event.源元素;//判断是否匹配目标元素if(target.nodeName.toLocaleLowerCase==='li'){console.log('内容为:',target.innerHTML);}});上面代码中,target元素就是#list元素下具体点击的元素,然后通过判断target的一些属性(如:nodeName,id等),可以更准确的匹配到某个类型#listli元素;使用Element.matches完全匹配如果将HTML更改为:item1
  • item2
  • item3......
  • itemn
  • //......表示还有未知数量的li中间,我们要将#list元素下的li元素(其类为class-1)的点击事件委托给#list;如果使用上面的方法,我们还需要在if(target.nodeName.toLocaleLowerCase==='li')的判断中添加一个判断target.nodeName.className==='class-1';但是如果你想像CSS选择那样做更灵活的匹配,上面的判断太多了,很难做到灵活这里可以使用Element.matchesAPI来匹配;Element.matchesAPI的基本用法:Element.matches(selectorString),selectorString是一个类似CSS的选择器规则,比如这个例子中,你可以使用target.matches('li.class-1'),他会返回一个boolean值,如果目标元素是一个标签li并且它的类是class-1,则返回true,否则返回false;当然它的兼容性还存在一些问题,需要IE9及以上的现代浏览器版本;我们可以使用Polyfill来解决兼容性问题:元素.prototype.mozMatchesSelector||元素.prototype.msMatchesSelector||元素.prototype.oMatchesSelector||元素.prototype.webkitMatchesSelector||function(s){varmatches=(this.document||this.ownerDocument).querySelectorAll(s),i=matches.length;while(--i>=0&&matches.item(i)!==this){}returni>-1;};}添加Element.matches之后我们就可以实现我们的需求了:元素.prototype.mozMatchesSelector||元素.prototype.msMatchesSelector||元素.prototype.oMatchesSelector||元素.prototype.webkitMatchesSelector||function(s){varmatches=(this.document||this.ownerDocument).querySelectorAll(s),i=matches.length;while(--i>=0&&matches.item(i)!==this){}returni>-1;};}document.getElementById('列表')。addEventListener('click',function(e){//兼容性处理varevent=e||window.event;vartarget=event.target||event.srcElement;if(target.matches('li.class-1')){console.log('内容是:',target.innerHTML);}});函数封装为了应对更多的场景,我们可以将事件代理函数封装成一个公共函数,这样就可以复用了结合上面的例子实现一个函数eventDelegate,它接受四个参数:[String]一个选择器字符串,用于过滤需要实现代理的父元素,即需要实际绑定的事件;[String]aselection选择器字符串用于过滤触发事件的选择器元素的后代,即我们需要委托的元素;[String]一种或多种事件类型,以空格和可选命名空间分隔,例如click或keydown.click;[Function]需要响应代理事件的函数;**这里有几个重点:父代理可能有多个元素,需要一个一个绑定事件;绑定的事件类型可能有多个,Events需要一个一个绑定;在处理和匹配委托元素时需要考虑兼容性问题;需要传入正确的参数,执行绑定函数时要注意这个问题;functioneventDelegate(parentSelector,targetSelector,events,foo){//触发函数triFunction(e){//兼容性处理varevent=e||窗口事件;vartarget=event.target||事件源元素;//处理匹配兼容性if(!Element.prototype.matches){Element.prototype.matches=Element.prototype.matchesSelector||元素.prototype.mozMatchesSelector||元素.prototype.msMatchesSelector||元素.prototype.oMatchesSelector||元素.原型.webkitMatchesSelector||function(s){varmatches=(this.document||this.ownerDocument).querySelectorAll(s),i=matches.length;while(--i>=0&&matches.item(i)!==this){}returni>-1;};}//判断是否匹配我们在需要的元素上if(target.matches(targetSelector)){//执行绑定函数,注意这个foo.call(target,Array.prototype.slice.call(arguments));}}//如果有多个For事件,需要将所有事件一一绑定。events.split('.').forEach(function(evt){//多个父层元素也需要一一绑定Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(function($p){$p.addEventListener(evt,triFunction);});});}当代理元素不是目标元素时,选择器targetSelector指向的元素不是event.target时(指向的元素到eventtarget阶段),那么需要逐层遍历event.target的parentNode去匹配targetSelector,直到parentSelector例如:
  • item1
  • item2
  • 或li事件委托给了#list,但是这时候你会发现event.target指向lispan,所以需要逐层遍历外层元素进行匹配,直到到达代理事件的函数,我们就可以使用event.currentTarget获取代理Event函数;completefunction:functioneventDelegate(parentSelector,targetSelector,events,foo){//触发函数functiontriFunction(e){//兼容性处理varevent=e||窗口事件;//获取到目标阶段指向的元素vartarget=event.target||事件源元素;//获取代理事件的函数varcurrentTarget=event.currentTarget;//处理匹配的兼容性if(!Element.prototype.matches){Element.prototype.matches=Element.prototype.matchesSelector||元素.prototype.mozMatchesSelector||元素.prototype.msMatchesSelector||元素.prototype.oMatchesSelector||元素.prototype.webkitMatchesSelector||function(s){varmatches=(this.document||this.ownerDocument).querySelectorAll(s),i=matches.length;while(--i>=0&&matches.item(i)!==this){}returni>-1;};}//遍历外层并匹配while(target!==currentTarget){//判断是否匹配到我们需要的元素if(target.matches(targetSelector)){varsTarget=target;//执行绑定函数,注意这个foo.call(sTarget,Array.prototype.slice.call(arguments))}target=target.parentNode;}}//如果有多个事件,则需要绑定所有事件events.split('.').forEach(function(evt){//多个父层元素也需要一一绑定Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(function($p){$p.addEventListener(evt,triFunction);});});}使用函数:eventDelegate('#list','li','点击',function(){console.log(this);});点击后可以看到控制台输出#listlielementobject;局限性当然,事件委托也有一定的局限性;比如focus、blur等事件没有事件冒泡机制,所以不能委托;mousemove、mouseout等事件,虽然有事件冒泡,但只能通过location不断计算定位,性能消耗较大,不适合事件委托;如果文章有误,请指正!