溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

好程序員分享JavaScript事件委托代理和函數(shù)封裝詳解

發(fā)布時(shí)間:2020-08-12 17:13:57 來源:ITPUB博客 閱讀:166 作者:好程序員IT 欄目:web開發(fā)

好程序員 分享 JavaScript 事件委托代理和函數(shù)封裝詳解 JavaScript 高級(jí)程序設(shè)計(jì)上講:事件委托就是利用事件冒泡,只指定一個(gè)事件處理程序,就可以管理某一類型的所有事件。下面我們一起看看事件代理相關(guān)介紹。

 

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

 

基本概念

 

事件委托,通俗地來講,就是把一個(gè)元素響應(yīng)事件( click keydown...... )的函數(shù)委托到另一個(gè)元素;

 

一般來講,會(huì)把一個(gè)或者一組元素的事件委托到它的父層或者更外層元素上,真正綁定事件的是外層元素,當(dāng)事件響應(yīng)到需要綁定的元素上時(shí),會(huì)通過事件冒泡機(jī)制從而觸發(fā)它的外層元素的綁定事件上,然后在外層元素上去執(zhí)行函數(shù)。

 

舉個(gè)例子,比如一個(gè)宿舍的同學(xué)同時(shí)快遞到了,一種方法就是他們都傻傻地一個(gè)個(gè)去領(lǐng)取,還有一種方法就是把這件事情委托給宿舍長(zhǎng),讓一個(gè)人出去拿好所有快遞,然后再根據(jù)收件人一一分發(fā)給每個(gè)宿舍同學(xué);

 

在這里,取快遞就是一個(gè)事件,每個(gè)同學(xué)指的是需要響應(yīng)事件的 DOM 元素,而出去統(tǒng)一領(lǐng)取快遞的宿舍長(zhǎng)就是代理的元素,所以真正綁定事件的是這個(gè)元素,按照收件人分發(fā)快遞的過程就是在事件執(zhí)行中,需要判斷當(dāng)前響應(yīng)的事件應(yīng)該匹配到被代理元素中的哪一個(gè)或者哪幾個(gè)。

 

事件冒泡

 

前面提到 DOM 中事件委托的實(shí)現(xiàn)是利用事件冒泡的機(jī)制,那么事件冒泡是什么呢?

 

document.addEventListener 的時(shí)候我們可以設(shè)置事件模型:事件冒泡、事件捕獲,一般來說都是用事件冒泡的模型;

好程序員分享JavaScript事件委托代理和函數(shù)封裝詳解

如上圖所示,事件模型是指分為三個(gè)階段:

 

捕獲階段:在事件冒泡的模型中,捕獲階段不會(huì)響應(yīng)任何事件;

目標(biāo)階段:目標(biāo)階段就是指事件響應(yīng)到觸發(fā)事件的最底層元素上;

冒泡階段:冒泡階段就是事件的觸發(fā)響應(yīng)會(huì)從最底層目標(biāo)一層層地向外到最外層(根節(jié)點(diǎn)),事件代理即是利用事件冒泡的機(jī)制把里層所需要響應(yīng)的事件綁定到外層; ### 事件

 

 

委托的優(yōu)點(diǎn)

 

1. 減少內(nèi)存消耗

 

試想一下,若果我們有一個(gè)列表,列表之中有大量的列表項(xiàng),我們需要在點(diǎn)擊列表項(xiàng)的時(shí)候響應(yīng)一個(gè)事件;

 

<ul id="list">

  <li>item 1</li>

  <li>item 2</li>

  <li>item 3</li>

  ......

  <li>item n</li>

</ul>

// ...... 代表中間還有未知數(shù)個(gè) li

如果給每個(gè)列表項(xiàng)一一都綁定一個(gè)函數(shù),那對(duì)于內(nèi)存消耗是非常大的,效率上需要消耗很多性能;

 

因此,比較好的方法就是把這個(gè)點(diǎn)擊事件綁定到他的父層,也就是 `ul` 上,然后在執(zhí)行事件的時(shí)候再去匹配判斷目標(biāo)元素;

 

所以事件委托可以減少大量的內(nèi)存消耗,節(jié)約效率。

 

2. 動(dòng)態(tài)綁定事件

 

比如上述的例子中列表項(xiàng)就幾個(gè),我們給每個(gè)列表項(xiàng)都綁定了事件;

 

在很多時(shí)候,我們需要通過 ajax 或者用戶操作動(dòng)態(tài)的增加或者去除列表項(xiàng)元素,那么在每一次改變的時(shí)候都需要重新給新增的元素綁定事件,給即將刪去的元素解綁事件;

 

如果用了事件委托就沒有這種麻煩了,因?yàn)槭录墙壎ㄔ诟笇拥模湍繕?biāo)元素的增減是沒有關(guān)系的,執(zhí)行到目標(biāo)元素是在真正響應(yīng)執(zhí)行事件函數(shù)的過程中去匹配的;

 

所以使用事件在動(dòng)態(tài)綁定事件的情況下是可以減少很多重復(fù)工作的。

 

jQuery 中的事件委托

 

jQuery 中的事件委托相信很多人都用過,它主要這幾種方法來實(shí)現(xiàn):

 

$.on: 基本用法 : $('.parent').on('click', 'a', function () { console.log('click event on tag a'); }) ,它是 .parent 元素之下的 a 元素的事件代理到 $('.parent') 之上,只要在這個(gè)元素上有點(diǎn)擊事件,就會(huì)自動(dòng)尋找到 .parent 元素下的 a 元素,然后響應(yīng)事件;

$.delegate: 基本用法 : $('.parent').delegate('a', 'click', function () { console.log('click event on tag a'); }) ,同上,并且還有相對(duì)應(yīng)的 $.delegate 來刪除代理的事件;

$.live: 基本使用方法 : $('a', $('.parent')).live('click', function () { console.log('click event on tag a'); }) ,同上,然而如果沒有傳入父層元素 $(.parent) ,那事件會(huì)默認(rèn)委托到 $(document) 上; ( 已廢除 )

實(shí)現(xiàn)功能

 

基本實(shí)現(xiàn)

 

比如我們有這樣的一個(gè) HTML 片段:

 

<ul id="list">

  <li>item 1</li>

  <li>item 2</li>

  <li>item 3</li>

  ......

  <li>item n</li>

</ul>

// ...... 代表中間還有未知數(shù)個(gè) li

我們來實(shí)現(xiàn)把 #list 下的 li 元素的事件代理委托到它的父層元素也就是 #list 上:

 

// 給父層元素綁定事件

document.getElementById('list').addEventListener('click', function (e) {

  // 兼容性處理

  var event = e || window.event;

  var target = event.target || event.srcElement;

  // 判斷是否匹配目標(biāo)元素

  if (target.nodeName.toLocaleLowerCase === 'li') {

    console.log('the content is: ', target.innerHTML);

  }

});

在上述代碼中, target 元素則是在 #list 元素之下具體被點(diǎn)擊的元素,然后通過判斷 target 的一些屬性(比如: nodeName , id 等等)可以更精確地匹配到某一類 #list li 元素之上;

 

使用 Element.matches 精確匹配

 

如果改變下 HTML 成:

 

<ul id="list">

  <li className="class-1">item 1</li>

  <li>item 2</li>

  <li className="class-1">item 3</li>

  ......

  <li>item n</li>

</ul>

// ...... 代表中間還有未知數(shù)個(gè) li

這里,我們想把 #list 元素下的 li 元素(并且它的 class class-1 )的點(diǎn)擊事件委托代理到 #list 之上;

 

如果通過上述的方法我們還需要在 `if (target.nodeName.toLocaleLowerCase === 'li')` 判斷之中在加入一個(gè)判斷 `target.nodeName.className === 'class-1'` ;

 

但是如果想像 CSS 選擇其般做更加靈活的匹配的話,上面的判斷未免就太多了,并且很難做到靈活性,這里可以使用 Element.matches API 來匹配;

 

Element.matches API 的基本使用方法 : Element.matches(selectorString) , selectorString 既是 CSS 那樣的選擇器規(guī)則,比如本例中可以使用 target.matches('li.class-1') ,他會(huì)返回一個(gè)布爾值,如果 target 元素是標(biāo)簽 li 并且它的類是 class-1 ,那么就會(huì)返回 true ,否則返回 false ;

 

當(dāng)然它的兼容性還有一些問題,需要 IE9 及以上的現(xiàn)代化瀏覽器版本;

 

我們可以使用 Polyfill 來解決兼容性上的問題:

 

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;            

    };

}

加上 Element.matches 之后就可以來實(shí)現(xiàn)我們的需求了:

 

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;            

    };

}

document.getElementById('list').addEventListener('click', function (e) {

  // 兼容性處理

  var event = e || window.event;

  var target = event.target || event.srcElement;

  if (target.matches('li.class-1')) {

    console.log('the content is: ', target.innerHTML);

  }

});

函數(shù)封裝

 

在應(yīng)對(duì)更多場(chǎng)景上我們可以把事件代理的功能封裝成一個(gè)公用函數(shù),這樣就可以重復(fù)利用了。

結(jié)合上面的例子來實(shí)現(xiàn)一個(gè)函數(shù) eventDelegate ,它接受四個(gè)參數(shù):

 

[String] 一個(gè)選擇器字符串用于過濾需要實(shí)現(xiàn)代理的父層元素,既事件需要被真正綁定之上;

[String] 一個(gè)選擇器字符串用于過濾觸發(fā)事件的選擇器元素的后代,既我們需要被代理事件的元素;

[String] 一個(gè)或多個(gè)用空格分隔的事件類型和可選的命名空間,如 click keydown.click ;

[Function] 需要代理事件響應(yīng)的函數(shù);

這里就有幾個(gè)關(guān)鍵點(diǎn):

 

對(duì)于父層代理的元素可能有多個(gè),需要一一綁定事件;

對(duì)于綁定的事件類型可能有多個(gè),需要一一綁定事件;

在處理匹配被代理的元素之中需要考慮到兼容性問題;

在執(zhí)行所綁定的函數(shù)的時(shí)候需要傳入正確的參數(shù)以及考慮到 this 的問題;

function eventDelegate (parentSelector, targetSelector, events, foo) {

  // 觸發(fā)執(zhí)行的函數(shù)

  function triFunction (e) {

    // 兼容性處理

    var event = e || window.event;

    var target = event.target || event.srcElement;

    // 處理 matches 的兼容性

    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;            

        };

    }

    // 判斷是否匹配到我們所需要的元素上

    if (target.matches(targetSelector)) {

      // 執(zhí)行綁定的函數(shù),注意 this

      foo.call(target, Array.prototype.slice.call(arguments));

    }

  }

  // 如果有多個(gè)事件的話需要全部一一綁定事件

  events.split('.').forEach(function (evt) {

    // 多個(gè)父層元素的話也需要一一綁定

    Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(function ($p) {

      $p.addEventListener(evt, triFunction);

    });

  });

}

優(yōu)化

 

當(dāng)被代理的元素不是目標(biāo)元素的時(shí)候,既選擇器 targetSelector 所指向的元素不是 event.target (事件目標(biāo)階段指向的元素)的時(shí)候,這時(shí)候就需要層層遍歷 event.target parentNode 去匹配 targetSelector 了,直到 parentSelector

 

比如:

 

<ul id="list">

  <li><span>item 1</span></li>

  <li><span>item 2</span></li>

</ul>

還是把 li 的事件代理到 #list 之上,但這時(shí)候會(huì)發(fā)現(xiàn) event.target 指向的是 li span ,因此需要層層遍歷外層元素去匹配,直到到代理事件的函數(shù),我們可以用 event.currentTarget 來獲取到代理事件的函數(shù);

 

完整函數(shù):

 

function eventDelegate (parentSelector, targetSelector, events, foo) {

  // 觸發(fā)執(zhí)行的函數(shù)

  function triFunction (e) {

    // 兼容性處理

    var event = e || window.event;

 

    // 獲取到目標(biāo)階段指向的元素

    var target = event.target || event.srcElement;

 

    // 獲取到代理事件的函數(shù)

    var currentTarget = event.currentTarget;

 

    // 處理 matches 的兼容性

    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;            

        };

    }

 

    // 遍歷外層并且匹配

    while (target !== currentTarget) {

      // 判斷是否匹配到我們所需要的元素上

      if (target.matches(targetSelector)) {

        var sTarget = target;

        // 執(zhí)行綁定的函數(shù),注意 this

        foo.call(sTarget, Array.prototype.slice.call(arguments))

      }

 

      target = target.parentNode;

    }

  }

 

  // 如果有多個(gè)事件的話需要全部一一綁定事件

  events.split('.').forEach(function (evt) {

    // 多個(gè)父層元素的話也需要一一綁定

    Array.prototype.slice.call(document.querySelectorAll(parentSelector)).forEach(function ($p) {

      $p.addEventListener(evt, triFunction);

    });

  });

}

使用函數(shù):

 

eventDelegate('#list', 'li', 'click', function () { console.log(this); });

點(diǎn)擊后可以看到 console 出了 `#list li` 元素對(duì)象;

 

局限性

 

當(dāng)然,事件委托也是有一定局限性的;

 

比如 focus 、 blur 之類的事件本身沒有事件冒泡機(jī)制,所以無法委托;

 

mousemove mouseout 這樣的事件,雖然有事件冒泡,但是只能不斷通過位置去計(jì)算定位,對(duì)性能消耗高,因此也是不適合于事件委托的;


向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI