Web性能优化之 - 事件委托(代理)

* 什么是事件委托

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

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

* 为什么要用事件委托

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

Web性能优化之 - 事件委托(代理)_第1张图片
大表格下有很多相同的操作

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

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

HTML DOM 树

* 事件委托的原理

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

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

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

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

  • 111
  • 222
  • 333
  • 444

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)
    }
  }
}

效果展示

Web性能优化之 - 事件委托(代理)_第3张图片
页面内容

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

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

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

  • 111
  • 222
  • 333
  • 444

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的值。什么?你也不知道节点是干嘛的?朋友,建议你把该文通篇看一遍。

用事件委托实现区分点击不同
  • 执行相同的操作
  • 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

     咋样,有没有被帅到。这样改就只有点击

  • 会触发事件了。 且每次只执行一次dom操作,如
  • 数量很多的话,将大大减少dom的操作,优化的性能可想而知!

    用事件委托实现区分点击不同
  • 执行不同的操作
  • 相信大家,理解了上一步之后,这个需求也不难,既然target能拿到nodeName,自然也能拿到id:
    先看看原始做法:
    html代码

    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('选择');
      }
    }
    

    效果展示

    Web性能优化之 - 事件委托(代理)_第4张图片
    四个不同事件的按钮

    理解: 意图很明显,有四个不同操作的四个按钮,点击响应不同的事件,这种方法需要对每个元素节点都执行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代码

    Hello

    我们用target获取

    元素的文本节点

    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代码

    
    
    • 111
    • 222
    • 333
    • 444

    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

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

    Web性能优化之 - 事件委托(代理)_第5张图片
    鼠标移到444的时候

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


    Web性能优化之 - 事件委托(代理)_第6张图片
    鼠标移到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

    效果展示

    Web性能优化之 - 事件委托(代理)_第7张图片
    公共方法分装成函数后调用

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

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

    
    
    • 111
    • 222
    • 333
    • 444

    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);
      }
    }
    

    效果展示

    Web性能优化之 - 事件委托(代理)_第8张图片
    新成员添加之前的效果

    Web性能优化之 - 事件委托(代理)_第9张图片
    新成员添加,继承了公有方法

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

    * 事件委托 纵向拓展

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

      上面的案例都有一个共性,那就是

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

      • 11111111111

      • 22222222
      • 3333333333
      • 4444444

      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;
          }
        });
      }
      

      效果展示

      Web性能优化之 - 事件委托(代理)_第10张图片
      点击了某个li元素之后显示

      * 总结

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

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

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