JavaScript事件委託原理

概念

事件委託,通俗來講就是將元素的事件委託給它的父級或者更外級元素處理。javascript

事件流

事件流描述的是從頁面中接收事件的順序。

事件冒泡:事件開始由最具體的元素接收,而後逐級向上傳播到較爲不具體的節點(或文檔)。java

事件捕獲:事件開始由不太具體的節點接收,而後逐級向下傳播到最具體的節點。它與事件冒泡是個相反的過程。node

DOM2級事件規定的事件流包括三個階段:jquery

  • 事件捕獲
  • 目標階段
  • 事件冒泡

原理

事件委託就是利用事件冒泡機制實現的。

假設有一個列表,要求點擊列表項彈出對應字段。git

<ul id="myLink">
  <li id="1">aaa</li>
  <li id="2">bbb</li>
  <li id="3">ccc</li>
</ul>

不使用事件委託github

var myLink = document.getElementById('myLink');
var li = myLink.getElementsByTagName('li');

for(var i = 0; i < li.length; i++) {
  li[i].onclick = function(e) {
    var e = event || window.event;
    var target = e.target || e.srcElement;
    alert(e.target.id + ':' + e.target.innerText);  
  };
}

這樣作存在的問題:web

  • 給每一個列表項都綁定事件,消耗內存
  • 當有動態添加的元素時,須要從新給元素綁定事件

使用事件委託瀏覽器

var myLink = document.getElementById('myLink');

myLink.onclick = function(e) {
  var e = event || window.event;
  var target = e.target || e.srcElement;
  if(e.target.nodeName.toLowerCase() == 'li') {
    alert(e.target.id + ':' + e.target.innerText);
  }
};

上述代碼是將事件委託給列表項的父級,經過 target 下的 nodeName 屬性做出判斷。函數

也能夠給每一個列表項綁定與其對應的事件。如:性能

var myLink = document.getElementById('myLink');

myLink.onclick = function(e) {
  var e = event || window.event;
  var target = e.target || e.srcElement;
  switch(target.id) {
    case '1':
      target.style.backgroundColor = 'red';
      break;
    case '2':
      alert('這是第二項');
      break;
    case '3':
      alert(e.target.id + ':' + e.target.innerText);
      break;
    default:
      alert('...');
  }
};

上述代碼是經過判斷 target 下的 id 屬性,執行不一樣的事件。

事件委託的優勢:

  • 只須要將同類元素的事件委託給父級或者更外級的元素,不須要給全部元素都綁定事件,減小內存空間佔用,提高性能
  • 動態新增的元素無需從新綁定事件

須要注意的地方:

  • 事件委託的實現依靠事件冒泡,所以不支持事件冒泡的事件就不適合用事件委託。

最適合採用事件委託技術的事件包括 clickmousedownmouseupkeydownkeyupkeypress。雖然 mouseovermouseout 事件也冒泡,但要適當處理它們並不容易,並且常常須要計算元素的位置。(由於當鼠標從一個元素移到其子節點時,或者當鼠標移出該元素時,都會觸發 mouseout 事件。)

  • 不是全部的事件綁定都適合使用事件委託,不恰當使用反而可能會致使不須要綁定事件的元素也被綁定上了事件。

Jquery中的事件委託

jquery中實現事件委託的幾種方法:

  • on
on(events, [selector], [data], fn)
// 將 li 的事件委託給它的父元素
$('#myLink').on('click', 'li', function() {
    // todo...
});
  • live

該方法在 jquery 1.7 版本已被廢棄。

  • delegate
delegate(selector, [type], [data], fn)
$('#myLink').delegate('li', 'click', function() {
    // todo...
});

該方法在 jquery 3.0 版本已被廢棄。用 on() 代替。

jquery 中, delegate()live()one()bind()等最終都是依賴 on() 方法實現的。所以能夠直接使用 on() 替代其餘方法。

封裝一個事件委託方法

須要注意的地方:

  • 保證兼容性,包括:事件綁定、元素選擇器 Element.matches 、事件 event 對象
  • 回調函數 this 指向
  • 上面的示例中,當目標元素下還有子元素時,子元素不能觸發事件。解決辦法是在觸發過程當中對元素進行判斷,若是當前觸發的元素不是目標元素,就繼續往該元素的 parentNode 查找,不然循環結束。
/**
 * [delegateEvent description]
 * @param  {[type]}   parentSelector 父元素
 * @param  {[type]}   targetSelector 目標元素
 * @param  {[type]}   events         事件
 * @param  {Function} fn             回調函數
 * @return {[type]}                  null
 */
function delegateEvent(parentSelector, targetSelector, events, fn) {
            
  // 事件綁定兼容性處理
  function addEvent(ele, type, handle) {
    if(ele.addEventListener) {
      ele.addEventListener(type, handle, false);
    } else if(ele.attachEvent){
      ele.attachEvent('on' + type, handle);
    } else {
      ele['on' + type] = handle;
    }
  }

  // 若是元素被指定的選擇器字符串選擇,Element.matches()  方法返回 true; 不然返回 false。
  // 對於不支持 Element.matches() 或 Element.matchesSelector(),但支持 document.querySelectorAll() 方法的瀏覽器,存在如下替代方案
  if (!Element.prototype.matches) {
    Element.prototype.matches = 
    Element.prototype.matchesSelector || 
    Element.prototype.mozMatchesSelector ||
    Element.prototype.msMatchesSelector || 
    Element.prototype.oMatchesSelector || 
    Element.prototype.webkitMatchesSelector ||
    function(s) {
      var matches = (this.document || this.ownerDocument).querySelectorAll(s),
        i = matches.length;
      while (--i >= 0 && matches.item(i) !== this) {}
        return i > -1;            
      };
    }

    // 事件處理邏輯
    addEvent(parentSelector, events, function(e) {
              
      // 兼容性處理
      var e = e || window.event;
      var t = e.target || e.srcElement;
      
      // currentTarget === parentSelector           
      var currentTarget = e.currentTarget;

      // 遍歷並判斷是否爲目標元素,若是不是,則往元素的 parentNode 繼續查找
      while(!t.matches(targetSelector)) {
        // 若是是目標元素則跳出循環
        if(t === currentTarget) {
          t = null;
          break;
        }
        t = t.parentNode;
      }

      if(t) {
        // 將回調函數的 this 指向目標元素
        fn.call(t, Array.prototype.slice.call(arguments));
      }
    });

}

調用示例:

<ul id="myLink">
  <li id="1" class="link"><a href="javascript:;"><span>aaa</span></a></li>
  <li id="2" class="link"><a href="javascript:;">bbb</a></li>
  <li id="3" class="link">ccc</li>
</ul>
var myLink = document.querySelector('#myLink');

delegateEvent(myLink, 'li.link', 'click', function() {
  console.log(this, this.id + ':' + this.innerText);
});

原文地址:https://github.com/daijingfeng/blog/issues/1

參考資料:

一、《JavaScript高級程序設計(第3版)》

二、Element.matches() API https://developer.mozilla.org...

相關文章
相關標籤/搜索