欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

提升网页速度的技巧:了解事件委托(代理)

最编程 2024-08-07 14:46:55
...

* 什么是事件委托

委托,就是让别人帮我们做事。某件事情本身应该由你来做,而你却加到别人身上来完成。事件委托,也叫事件代理,JavaScript高级程序设计上讲:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

  为了更好的帮助理解,这里有个很经典的一个例子: 快递员送快递,如果一个快递员送一个公司的快递,他可以选择在公司联系每个人来取这个快递,当然另一种方法就是把快递让前台的MM代收,然后公司的人只要自己来前台取就OK了,虽然结果是一样的,但是效率却变快了许多。

* 为什么要用事件委托

  正常情况下,对用户的一次操作进行响应和互动,我们需要对DOM节点(点我深入理解DOM )绑定相应的事件处理程序,执行对DOM的一次访问;那如果我们有很多类似的操作呢?如下图:

大表格下有很多相同的操作

  你可能会用到for循环的方法,来遍历每一个item,然后给他们添加事件,这么做是低级程序员的思维。
  我们知道,DOM操作本身就是很慢的,在每一次执行DOM操作过程中,浏览器都需要在内存中生成DOM树,如果我们添加了过多的页面处理程序,浏览器就需要不断的与DOM节点进行交互,访问的次数越多,引起浏览器重绘和重排的次数也就越多,直接影响整个页面的就绪时间。我们知道Web APP 和 Native APP相比的痛点就在这里(不知道的朋友,可以猛戳这里)。这也是为什么性能优化主要思想之一就是减少DOM操作的原因。
  但是,如果用事件委托,利用事件冒泡到父元素节点【parentNode】,我们将事件处理程序绑定在父元素节点上,此时与DOM交互就只有一次,大大的减少与DOM的交互次数,提高性能;

也许有人问,那什么是事件冒泡?
  知道的人直接往下拉,毕竟基础文,讲详细点没坏处。
  什么是事件冒泡?我们已经知道HTML DOM的树结构如下,事件冒泡就是从最深层的节点开始,然后逐渐向上传播事件。比如我们给如下<a>元素节点绑定click事件,当我们点击<a>时,这个事件就会一层一层往外执行。再举个例子,假如我们有这么一个节点树div > ul > li > a,我们点击a节点时,因事件冒泡,执行顺序就是a > li > ul > div

HTML DOM 树

* 事件委托的原理

  因为有事件冒泡这种机制,我们给上例中的 div 添加点击事件,那么,当他的所有子元素节点触发点击事件时,都会冒泡到该div上,所以,既使子元素上没有绑定点击事件,在用户执行点击操作的时候,也依旧能被触发响应。这就是事件委托,委托他们的父元素节点代为执行。
  经典例子中,前台MM就是这个父元素节点,各个员工到前台来领快递就是事件冒泡过程。

特别提醒: 事件委托是通过事件冒泡实现的,所以如果子级的元素阻止了事件冒泡,那么事件委托也将失效!

* 事件委托的实现(核心)

让我们用例子来帮助理解:
html代码

<ul id="parent">
  <li>111</li>
  <li>222</li>
  <li>333</li>
  <li>444</li>
</ul>

javascript代码

window.onload = function() {
  var parent = document.getElementById("parent");
  var sons = document.getElementsByTagName("li");

  for (var i = 0; i < sons.length; i++) {   
    sons[i].onclick = function() {
      document.write(sons[i-1].innerHTML)
    }
  }
}

效果展示

页面内容

当我们点击任意 li 标签的时候,页面结果都为 444。每次点击,首先要找到ul,然后遍历li,然后点击li的时候,又要找一次目标的li的位置,才能执行最后的操作,每次点击都要找一次li,找一次li,就执行了一次DOM操作

点击 li 页面均打印 444
用事件委托怎么实现?

想想事件委托的原理,就是把事件绑定到父节点上去,那就有了:
html代码

<ul id="parent">
  <li>111</li>
  <li>222</li>
  <li>333</li>
  <li>444</li>
</ul>

javascript代码

window.onload = function() {
  var parent = document.getElementById("parent");
  // var sons = document.getElementsByTagName("li");   // 不需要访问子元素节点了
  parent.onclick = function() {   // 在父元素节点上触发点击事件
    document.write("hello");
  } 
}

 现在,每次点击子元素节点后,先由事件冒泡将事件冒泡到父节点上,再由父元素代为执行。每次点击,只执行了一次DOM操作。
 但是,我们观察到,再上个例子每次执行结果都是444,这个例子也不能区分是哪个子元素触发了点击时间,这与我们分别绑定到 li 事件的响应效果不同,并且,点击ul标签,也会触发点击事件。这就不行了,这都搞不定,事件委托就用不了了啊。
 还好,Event对象提供了一个属性叫 target:可以返回事件的目标节点,也就是说,target就可以表示为当前的事件操作的dom,但是不是真正操作dom,这个是有兼容性的;标准浏览器用 ev.target,IE浏览器用 event.srcElement
 这里我们用 nodeName 来获取具体是什么标签名,这个返回的是一个大写的,我们需要转成小写再做比较,那我们再看下面的代码
什么?你不知道nodeName是干嘛的?没关系,戳这里。中部位置有介绍各种节点的nodeName的值。什么?你也不知道节点是干嘛的?朋友,建议你把该文通篇看一遍。

用事件委托实现区分点击不同 <li> 执行相同的操作

javascript代码

window.onload = function() {
  var parent = document.getElementById("parent");
  parent.onclick = function() {
    var ev = ev || window.event;  // 写全: ev = ev ? ev || window.event; 兼容ie
    var target = ev.target || ev.srcElement;  // 兼容ie
    if (target.nodeName.toLocaleLowerCase() == "li") {
      document.write(target.innerHTML);
    }
  } 
}

实现效果

点击了第二个li,打印了222

 咋样,有没有被帅到。这样改就只有点击 <li> 会触发事件了。 且每次只执行一次dom操作,如<li>数量很多的话,将大大减少dom的操作,优化的性能可想而知!

用事件委托实现区分点击不同 <li> 执行不同的操作

相信大家,理解了上一步之后,这个需求也不难,既然target能拿到nodeName,自然也能拿到id:
先看看原始做法:
html代码

<div id="box">
  <input type="button" id="add" value="添加" />
  <input type="button" id="remove" value="删除" />
  <input type="button" id="move" value="移动" />
  <input type="button" id="select" value="选择" />
</div>

javascript代码

window.onload = function(){
  var Add = document.getElementById("add");
  var Remove = document.getElementById("remove");
  var Move = document.getElementById("move");
  var Select = document.getElementById("select");
  Add.onclick = function(){
    alert('添加');
  };
  Remove.onclick = function(){
    alert('删除');
  };
  Move.onclick = function(){
    alert('移动');
  };
  Select.onclick = function(){
    alert('选择');
  }
}

效果展示

四个不同事件的按钮

理解: 意图很明显,有四个不同操作的四个按钮,点击响应不同的事件,这种方法需要对每个元素节点都执行DOM访问,如果多了,还是慢和卡顿的问题。

究极进化:事件委托

javascript代码

window.onload = function(){
  var oBox = document.getElementById("box");
  oBox.onclick = function (ev) {
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if(target.nodeName.toLocaleLowerCase() == 'input'){
      switch(target.id){      // target 就是一个元素节点,是被触发的元素节点
      case 'add' :
        alert('添加');
        break;
      case 'remove' :
        alert('删除');
        break;
      case 'move' :
        alert('移动');
        break;
      case 'select' :
        alert('选择');
        break;
      }
    }
  }
}

完美,用事件委托就可以只用一次dom操作就能完成所有的效果。
其实 target 是 被触发事件的目标节点,我们还能获取更多信息,比如
html代码

<div id="box">
  <p class="content" id="con">Hello</p>
</div>

我们用target获取<p>元素的文本节点

if ( target.nodeName.toLowerCase() == "p") {
  alert(target.firstChild.nodeValue);
  alert(target.childNodes[0].nodeValue);
}

* 事件委托 横向拓展

对新增节点是否生效? - 是

  之前讲的都是document加载完成的现有dom节点下的操作,那么如果是新增的节点,新增的节点会有事件吗?也就是说,一个新员工来了,他能收到快递吗?
  等等,我们先解决新增一个节点的问题

var newnode = document.createElement("li");
newnode.innerHtml = "new member";
parent.appendChild(newnode);

完美,继续。
来看看原始代码,你是不是这样写:
html代码

<input type="button" name="" id="btn" value="添加" />
<ul id="ul1">
  <li>111</li>
  <li>222</li>
  <li>333</li>
  <li>444</li>
</ul>

javascript代码

window.onload = function(){
  var oBtn = document.getElementById("btn");
  var oUl = document.getElementById("ul1");
  var aLi = oUl.getElementsByTagName('li');
  var num = 4;

  for(var i=0; i<aLi.length;i++){
    aLi[i].onmouseover = function(){
      this.style.background = 'red';
    };
    aLi[i].onmouseout = function(){
      this.style.background = '#fff';
    }
  }

  //添加新节点
  oBtn.onclick = function(){
    num++;
    var oLi = document.createElement('li');
    oLi.innerHTML = 111*num;
    oUl.appendChild(oLi);
  };
}

很抱歉告诉你,这样新增的节点是不会有我们的onmouseoveronmouseout方法的。
执行结果:

鼠标移到444的时候

新增了555这个子节点之后,移入该节点


鼠标移到555的时候

意识到错误后,可能会这样改进:将公共方法封装成函数,然后新增的时候调用这个函数:
javascript代码

window.onload = function(){
  var oBtn = document.getElementById("btn");
  var oUl = document.getElementById("ul1");
  var aLi = oUl.getElementsByTagName('li');
  var num = 4;

  function mHover () {
    //鼠标移入变红,移出变白
    for(var i=0; i<aLi.length;i++){
      aLi[i].onmouseover = function(){
        this.style.background = 'red';
      };
      aLi[i].onmouseout = function(){
        this.style.background = '#fff';
      }
    }
  }
  mHover ();  // 利用函数包含,然后新增时调用
  //添加新节点
  oBtn.onclick = function(){
    num++;
    var oLi = document.createElement('li');
    oLi.innerHTML = 111*num;
    oUl.appendChild(oLi);
    mHover ();
  };
}

效果展示

公共方法分装成函数后调用

恭喜你,效果实现了,但是,本身DOM操作次数就很多了,这又增加了一次访问,性能上还是不可取的。

还有办法吗?当然有啊,事件委托用起来
看招
html代码

<input type="button" name="" id="btn" value="添加" />
<ul id="ul1">
  <li>111</li>
  <li>222</li>
  <li>333</li>
  <li>444</li>
</ul>

javascript代码

window.onload = function(){
  var oBtn = document.getElementById("btn");
  var oUl = document.getElementById("ul1");
  var num = 4;

  oUl.onmouseover = function () {
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if (target.nodeName.toLowerCase() == "li") {
      target.style.backgroundColor = "red";
      target.style.fontSize = "20px";
    }
  }
  oUl.onmouseout = function () {
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if (target.nodeName.toLowerCase() == "li") {
      target.style.backgroundColor = "#fff";
      target.style.fontSize = "16px";
    }
  }
  oBtn.onclick = function () {
    num++;
    var newnode = document.createElement('li');
    newnode.innerHTML = 111 * num;
    oUl.appendChild(newnode);
  }
}

效果展示

新成员添加之前的效果

新成员添加,继承了公有方法

可以看到,555的字体和背景颜色都改变了。这种实现方式,已经十分接近原生效果,只产生一次DOM操作。

* 事件委托 纵向拓展

元素节点深度参差不齐,能否处理? - 能

  上面的案例都有一个共性,那就是<ul>标签下就是<li>了,并且<li>是最小子节点,那么,如果一个列表中,有的<li>还有子节点,有的<li>又没有子节点,那怎么办?
  你说怎么拌,我觉得凉拌容易拉肚子,还是热的好吃。看招:
html代码

<ul id="box">
  <li>
    <p>11111111111</p>
  </li>
  <li>
    <div>22222222</div>
  </li>
  <li>
    <span>3333333333</span>
  </li>
  <li>4444444</li>
</ul>

javascript代码

window.onload = {
  var box = document.getElementById("box");
  
  box.addEventListener('click', function(ev) {
    var target = ev.target;
    while (target !== box) {
      if (target.tagName.toLowerCase() == 'li') {
        alert("li clicked~");
        break;
      }
      target = target.parentNode;
    }
  });
}

效果展示

点击了某个li元素之后显示

* 总结

  我们可以发现,当用事件委托的时候,根本就不需要去遍历元素的子节点,只需要给父级元素添加事件就好了,其他的都是在js里面的执行,这样可以大大的减少dom操作,这才是事件委托的精髓所在。

最后总结一下:什么事件可以用事件委托,什么样的事件不可以用呢?

  • 适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。值得注意的是,mouseover和mouseout虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。
  • 不适合的就有很多了,举个例子,mousemove,每次都要计算它的位置,非常不好把控,在不如说focus,blur之类的,本身就没用冒泡的特性,自然就不能用事件委托了。