Web性能優(yōu)化之 - 事件委托(代理)

* 什么是事件委托

委托,就是讓別人幫我們做事。某件事情本身應(yīng)該由你來做,而你卻加到別人身上來完成。事件委托,也叫事件代理,JavaScript高級程序設(shè)計上講:事件委托就是利用事件冒泡,只指定一個事件處理程序,就可以管理某一類型的所有事件。

??為了更好的幫助理解,這里有個很經(jīng)典的一個例子: 快遞員送快遞,如果一個快遞員送一個公司的快遞,他可以選擇在公司聯(lián)系每個人來取這個快遞,當然另一種方法就是把快遞讓前臺的MM代收,然后公司的人只要自己來前臺取就OK了,雖然結(jié)果是一樣的,但是效率卻變快了許多。

* 為什么要用事件委托

??正常情況下,對用戶的一次操作進行響應(yīng)和互動,我們需要對DOM節(jié)點(點我深入理解DOM )綁定相應(yīng)的事件處理程序,執(zhí)行對DOM的一次訪問;那如果我們有很多類似的操作呢?如下圖:

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

??你可能會用到for循環(huán)的方法,來遍歷每一個item,然后給他們添加事件,這么做是低級程序員的思維。
??我們知道,DOM操作本身就是很慢的,在每一次執(zhí)行DOM操作過程中,瀏覽器都需要在內(nèi)存中生成DOM樹,如果我們添加了過多的頁面處理程序,瀏覽器就需要不斷的與DOM節(jié)點進行交互,訪問的次數(shù)越多,引起瀏覽器重繪和重排的次數(shù)也就越多,直接影響整個頁面的就緒時間。我們知道Web APP 和 Native APP相比的痛點就在這里(不知道的朋友,可以猛戳這里)。這也是為什么性能優(yōu)化主要思想之一就是減少DOM操作的原因。
??但是,如果用事件委托,利用事件冒泡到父元素節(jié)點【parentNode】,我們將事件處理程序綁定在父元素節(jié)點上,此時與DOM交互就只有一次,大大的減少與DOM的交互次數(shù),提高性能;

也許有人問,那什么是事件冒泡?
??知道的人直接往下拉,畢竟基礎(chǔ)文,講詳細點沒壞處。
??什么是事件冒泡?我們已經(jīng)知道HTML DOM的樹結(jié)構(gòu)如下,事件冒泡就是從最深層的節(jié)點開始,然后逐漸向上傳播事件。比如我們給如下<a>元素節(jié)點綁定click事件,當我們點擊<a>時,這個事件就會一層一層往外執(zhí)行。再舉個例子,假如我們有這么一個節(jié)點樹div > ul > li > a,我們點擊a節(jié)點時,因事件冒泡,執(zhí)行順序就是a > li > ul > div。

HTML DOM 樹

* 事件委托的原理

??因為有事件冒泡這種機制,我們給上例中的 div 添加點擊事件,那么,當他的所有子元素節(jié)點觸發(fā)點擊事件時,都會冒泡到該div上,所以,既使子元素上沒有綁定點擊事件,在用戶執(zhí)行點擊操作的時候,也依舊能被觸發(fā)響應(yīng)。這就是事件委托,委托他們的父元素節(jié)點代為執(zhí)行。
??經(jīng)典例子中,前臺MM就是這個父元素節(jié)點,各個員工到前臺來領(lǐng)快遞就是事件冒泡過程。

特別提醒: 事件委托是通過事件冒泡實現(xiàn)的,所以如果子級的元素阻止了事件冒泡,那么事件委托也將失效!
?

* 事件委托的實現(xiàn)(核心)

讓我們用例子來幫助理解:
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)
    }
  }
}

效果展示

頁面內(nèi)容

當我們點擊任意 li 標簽的時候,頁面結(jié)果都為 444。每次點擊,首先要找到ul,然后遍歷li,然后點擊li的時候,又要找一次目標的li的位置,才能執(zhí)行最后的操作,每次點擊都要找一次li,找一次li,就執(zhí)行了一次DOM操作

點擊 li 頁面均打印 444
用事件委托怎么實現(xiàn)?

想想事件委托的原理,就是把事件綁定到父節(jié)點上去,那就有了:
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");   // 不需要訪問子元素節(jié)點了
  parent.onclick = function() {   // 在父元素節(jié)點上觸發(fā)點擊事件
    document.write("hello");
  } 
}

?現(xiàn)在,每次點擊子元素節(jié)點后,先由事件冒泡將事件冒泡到父節(jié)點上,再由父元素代為執(zhí)行。每次點擊,只執(zhí)行了一次DOM操作。
?但是,我們觀察到,再上個例子每次執(zhí)行結(jié)果都是444,這個例子也不能區(qū)分是哪個子元素觸發(fā)了點擊時間,這與我們分別綁定到 li 事件的響應(yīng)效果不同,并且,點擊ul標簽,也會觸發(fā)點擊事件。這就不行了,這都搞不定,事件委托就用不了了啊。
?還好,Event對象提供了一個屬性叫 target:可以返回事件的目標節(jié)點,也就是說,target就可以表示為當前的事件操作的dom,但是不是真正操作dom,這個是有兼容性的;標準瀏覽器用 ev.target,IE瀏覽器用 event.srcElement。
?這里我們用 nodeName 來獲取具體是什么標簽名,這個返回的是一個大寫的,我們需要轉(zhuǎn)成小寫再做比較,那我們再看下面的代碼
什么?你不知道nodeName是干嘛的?沒關(guān)系,戳這里。中部位置有介紹各種節(jié)點的nodeName的值。什么?你也不知道節(jié)點是干嘛的?朋友,建議你把該文通篇看一遍。

用事件委托實現(xiàn)區(qū)分點擊不同 <li> 執(zhí)行相同的操作

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

實現(xiàn)效果

點擊了第二個li,打印了222

?咋樣,有沒有被帥到。這樣改就只有點擊 <li> 會觸發(fā)事件了。 且每次只執(zhí)行一次dom操作,如<li>數(shù)量很多的話,將大大減少dom的操作,優(yōu)化的性能可想而知!

用事件委托實現(xiàn)區(qū)分點擊不同 <li> 執(zhí)行不同的操作

相信大家,理解了上一步之后,這個需求也不難,既然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('選擇');
  }
}

效果展示

四個不同事件的按鈕

理解: 意圖很明顯,有四個不同操作的四個按鈕,點擊響應(yīng)不同的事件,這種方法需要對每個元素節(jié)點都執(zhí)行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 就是一個元素節(jié)點,是被觸發(fā)的元素節(jié)點
      case 'add' :
        alert('添加');
        break;
      case 'remove' :
        alert('刪除');
        break;
      case 'move' :
        alert('移動');
        break;
      case 'select' :
        alert('選擇');
        break;
      }
    }
  }
}

完美,用事件委托就可以只用一次dom操作就能完成所有的效果。
其實 target 是 被觸發(fā)事件的目標節(jié)點,我們還能獲取更多信息,比如
html代碼

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

我們用target獲取<p>元素的文本節(jié)點

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

?

* 事件委托 橫向拓展

對新增節(jié)點是否生效? - 是

??之前講的都是document加載完成的現(xiàn)有dom節(jié)點下的操作,那么如果是新增的節(jié)點,新增的節(jié)點會有事件嗎?也就是說,一個新員工來了,他能收到快遞嗎?
??等等,我們先解決新增一個節(jié)點的問題

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

完美,繼續(xù)。
來看看原始代碼,你是不是這樣寫:
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';
    }
  }

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

很抱歉告訴你,這樣新增的節(jié)點是不會有我們的onmouseoveronmouseout方法的。
執(zhí)行結(jié)果:

鼠標移到444的時候

新增了555這個子節(jié)點之后,移入該節(jié)點


鼠標移到555的時候

意識到錯誤后,可能會這樣改進:將公共方法封裝成函數(shù),然后新增的時候調(diào)用這個函數(shù):
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 ();  // 利用函數(shù)包含,然后新增時調(diào)用
  //添加新節(jié)點
  oBtn.onclick = function(){
    num++;
    var oLi = document.createElement('li');
    oLi.innerHTML = 111*num;
    oUl.appendChild(oLi);
    mHover ();
  };
}

效果展示

公共方法分裝成函數(shù)后調(diào)用

恭喜你,效果實現(xiàn)了,但是,本身DOM操作次數(shù)就很多了,這又增加了一次訪問,性能上還是不可取的。

還有辦法嗎?當然有啊,事件委托用起來
看招
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的字體和背景顏色都改變了。這種實現(xiàn)方式,已經(jīng)十分接近原生效果,只產(chǎn)生一次DOM操作。

* 事件委托 縱向拓展

元素節(jié)點深度參差不齊,能否處理? - 能

??上面的案例都有一個共性,那就是<ul>標簽下就是<li>了,并且<li>是最小子節(jié)點,那么,如果一個列表中,有的<li>還有子節(jié)點,有的<li>又沒有子節(jié)點,那怎么辦?
??你說怎么拌,我覺得涼拌容易拉肚子,還是熱的好吃??凑校?br> 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元素之后顯示

* 總結(jié)

??我們可以發(fā)現(xiàn),當用事件委托的時候,根本就不需要去遍歷元素的子節(jié)點,只需要給父級元素添加事件就好了,其他的都是在js里面的執(zhí)行,這樣可以大大的減少dom操作,這才是事件委托的精髓所在。

最后總結(jié)一下:什么事件可以用事件委托,什么樣的事件不可以用呢?

  • 適合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。值得注意的是,mouseover和mouseout雖然也有事件冒泡,但是處理它們的時候需要特別的注意,因為需要經(jīng)常計算它們的位置,處理起來不太容易。
  • 不適合的就有很多了,舉個例子,mousemove,每次都要計算它的位置,非常不好把控,在不如說focus,blur之類的,本身就沒用冒泡的特性,自然就不能用事件委托了。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容