JavaScript DOM 事件委託 ★

1、事件委託的比喻

在網上看到一個關於事件委託的例子,比如一個宿舍的同學快遞同時到了,一種方法就是他們都傻傻地一個個去領取,還有一種方法就是把這件事情委託給宿舍長,讓一個人出去拿好所有快遞,然後再根據收件人一一分發給每個宿舍同學;

在這裏,取快遞就是一個事件,每個同學指的是需要響應事件的 DOM 元素,而出去統一領取快遞的宿舍長就是代理的元素,所以真正綁定事件的是這個元素,按照收件人分發快遞的過程就是在事件執行中,需要判斷當前響應的事件應該匹配到被代理元素中的哪一個或者哪幾個。

不說人話就是:事件委託(delegate),也稱爲事件託管或事件代理,簡單描述就是把目標節點的事件綁定到祖先節點上。這種簡單而優雅的事件註冊方式基於:事件傳播過程中,逐層冒泡總能被祖先節點捕獲。

這樣做的好處:優化代碼,提升運行性能,真正把 HTML 和 JavaScript 分離,也能防止在動態添加或刪除節點的過程中註冊的事件丟失。基於上面的取快遞的例子,如果不使用事件委託(舍長代取),那宿舍的每個人都要去取快遞,浪費時間。將取快遞的例子映射到 JavaScript DOM 事件知識中,如果不使用事件委託,那就會造成事件的一些性能和使用的問題,比如:

  • 綁定事件越多,瀏覽器內存佔用越大,嚴重影響性能
  • ajax 的出現,局部刷新的盛行,導致每次加載完,都要重新綁定事件
  • 部分瀏覽器移除元素時,綁定的事件並沒有被及時移除,導致的內存泄漏,嚴重影響性能
  • ajax 局部刷新的大部分只是顯示的數據,而操作卻大部分相同,重複綁定,會導致代碼的耦合性過大,嚴重影響後期的維護

上述的限制,都是直接給元素事件綁定帶來的問題,所以經過了一些前輩的總結試驗,也就有了事件委託這個解決方案。

2、DOM 事件模型/機制

事件委託,通俗地來講,就是把一個元素響應事件(click、keydown......)的函數委託到另一個元素;

一般來講,會把一個或者一組元素的事件委託到它的父層或者更外層元素上,真正綁定事件的是外層元素,當事件響應到需要綁定的元素上時,會通過 事件冒泡機制 從而觸發它的外層元素的綁定事件上,然後在外層元素上去執行函數。

那什麼是事件冒泡?這就需要講到 DOM 事件模型/機制

2.1、事件冒泡

前面提到 DOM 中事件委託的實現是利用事件冒泡的機制,那麼事件冒泡是什麼呢?

element.addEventListener('click', fn, bool) 的時候我們可以設置事件模型:事件冒泡、事件捕獲:

  • 如果 bool 不傳或爲 falsy 就讓 fn 走冒泡,即當瀏覽器在冒泡階段發現 element 有 fn 監聽函數,就會調用 fn,並提供事件信息
  • 如果 bool 爲 true就讓 fn 走捕獲,即當瀏覽器在捕獲階段發現 element 有 fn 監聽函數,就會調用 fn,並提供事件信息

2.2、事件模型

如上圖所示,事件模型是指分爲 三個階段

  • 捕獲階段:在事件冒泡的模型中,捕獲階段不會響應任何事件;
  • 目標階段:目標階段就是指事件響應到觸發事件的最底層元素上;
  • 冒泡階段:冒泡階段就是事件的觸發響應會從最底層目標一層層地向外到最外層(根節點),事件代理即是利用事件冒泡的機制把裏層所需要響應的事件綁定到外層;

總結:先捕獲,後冒泡,捕獲從上到下,冒泡從下到上(形象點說法:捕獲像石頭沉入海底,冒泡則像氣泡冒出水面),捕獲:window-> document -> body -> 當前元素,冒泡:當前元素 -> body -> document -> window;

注意:如果在元素上 `同時` 綁定捕獲事件和冒泡事件,哪一個先觸發是要分情況的:如果事件是通過此元素的子級元素觸發,則優先觸發捕獲事件,若不通過此元素的子級元素觸發,則按照 Javascript 執行順序觸發。

2.3、阻止事件冒泡

我們經常利用事件冒泡機制去減少給 DOM 添加過多的綁定事件,即 事件委託,但是有時候事件冒泡也會比較煩人,影響我們的事件正常處理機制,這個時候就需要阻止事件冒泡了。

冒泡和捕獲是 JavaScript 事件模型的兩種行爲,使用 event.stopPropagation() 起到阻止捕獲和冒泡階段中當前事件的進一步傳播。使用 event.preventDefault() 可以取消默認事件。

$("box3").onclick = function (event) {
    console.log("裏面的盒子");
    event.stopPropagation(); //阻止事件冒泡   event鼠標的事件對象
}

也有例外:有些事件不可取消冒泡,比如 scroll event,Bubbles:Yes 的意思是該事件是否冒泡,Cancelable:No 的意思是開發者是否可以取消冒泡,有興趣的同學推薦去看一下 MDN 文檔。

如果 scroll 滾動事件不能取消冒泡,那應該通過什麼方法 阻止滾動事件 ?可阻止 wheel 和 touchstart 的默認動作,如下:

x.addEventLisenter('scroll', (event)=>{    //這樣做是沒用的,
    event.stopPropagation();               //scroll不可取消冒泡
    event.preventDefault();                //滾動的默認動作是滾動後的動作
})

//1.阻止鼠標滾輪事件 - 但是還能用鼠標點擊滾動條
x.addEventListener('wheel', (event)=>{
    event.preventDefault()
})

/*2.CSS部分:隱藏滾動條 - 但是手機端可以滾動*/    //3.阻止手機端滾動的方法 - 徹底阻止了滾動
::-webkit-scrollbar {                          x.addEventListener('touchstart',(event)=>{
    width:0 !important                             event.preventDefault()|
}                                              })

2.4、阻止默認行爲

在 HTML 中有很多自帶默認事件的元素,很典型的例子:a 標籤,如果給 a 標籤綁定點擊事件,觸發後頁面會有一個刷新,是 a 鏈接默認的跳轉事件,阻止這個有很多方法:

<!--方法一:給a標籤中href屬性添加:-->
<a href="javascript:;">鏈接</a>
<a href="javascript:void(0);">鏈接</a>

<!--方法二:給綁定的事件添加return false:-->
<a href="" id="link">鏈接</a>
<script>
    document.getElementById("link").onclick = function (){
        console.log("666");
        return false;
    }
</script>

<!--方法三:使用event事件裏的方法:-->
<a href="" id="link">鏈接</a>
<script>
    document.getElementById("link").onclick = function (event){
        console.log("666");
        e.preventDefault();
    }
</script>

3、事件委託的優點

3.1、減少內存消耗

試想一下,若果我們有一個列表,列表之中有大量的列表項,我們需要在點擊列表項的時候響應一個事件;

<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  <li>......</li>    <!--...... 代表中間還有未知數個 li-->
  <li>item n</li>
</ul>

如果給每個列表項一一都綁定一個函數,那對於內存消耗是非常大的,效率上需要消耗很多性能;因此,比較好的方法就是把這個 點擊事件綁定到他的父層,也就是 `ul` 上,然後在執行事件的時候再去匹配判斷目標元素;所以事件委託可以減少大量的內存消耗,節約效率。

3.2、動態綁定事件

比如上述的例子中列表項就幾個,我們給每個列表項都綁定了事件;

在很多時候,我們需要通過 AJAX 或者用戶操作動態的增加或者去除列表項元素,那麼在每一次改變的時候都需要重新給新增的元素綁定事件,給即將刪去的元素解綁事件;

如果用了事件委託就沒有這種麻煩了,因爲事件是綁定在父層的,和目標元素的增減是沒有關係的,執行到目標元素是在真正響應執行事件函數的過程中去匹配的;

所以使用事件在動態綁定事件的情況下是可以減少很多重複工作的。

4、事件委託的基本實現

需求 1:假如你要給 100 個按鈕添加點擊事件,咋辦?答:監聽這 100 個按鈕的祖先,等冒泡的時候判斷 target 是不是這100個按鈕中的一個。優點:可以節省監聽器,節省內存。

比如我們有這樣的一個 HTML 片段,我們來實現把 #list 下的 li 元素的事件代理委託到它的父層元素也就是 #list 上:

<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  <li>......</li>    <!--...... 代表中間還有未知數個 li-->
  <li>item n</li>
</ul>
// 給父層元素綁定事件
document.getElementById('list').addEventListener('click', function (e) {
  // 兼容性處理
  var event = e || window.event;
  var target = event.target || event.srcElement;
  // 判斷是否匹配目標元素
  if (target.nodeName.toLocaleLowerCase === 'li') {
    console.log('the content is: ', target.innerHTML);
  }
});

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

補充:除了 target 之外,還有 currentTarget,如果 <li> 裏面還有 <span> 元素,則給 <ul> 添加點擊事件後, event.target 指的是 <span>,而 event.currentTarget 指的是 <li>。

需求 2:你要監聽目前不存在的元素的點擊事件,咋辦?答:監聽祖先,等點擊的時候看看是不是我想要監聽的元素即可。優點:可以監聽動態元素。

<div id = "div1">

</div>
<script>
    setTimeout(() => {    //動態添加元素
        const button = document.createElement('button');
        button.textContent = 'click 1'
        div1.appendChild(button)
    }, 1000)

    divl.addEventListener('click', (event) => {    //通過事件委託監聽動態添加的元素
        const target = event.target
        if(target.tagName.tolowerCase() === 'button'){
            console.log('button 被 click 了');
        }
    })
</script>

5、封裝事件委託函數

<div id = "div1">

</div>
<script>
    setTimeout(() => {    //動態添加元素
        const button = document.createElement('button');
        const span = document.createElement('span');
        span.textContent = 'click 1';
        button.appendChild(span);
        div1.appendChild(button)
    }, 1000)

    function on(eventType, element, selector, fn) {    //封裝一個事件委託函數
        if(!(element instanceof Element)){
            element = document.querySelektor(element)
        }
        element.addEventListener(eventType, e => {
          let el = e.target
          while (!el.matches(selector)) {
            if (element === el) {    //找父節點的過程中不能超過被委託元素
              el = null
              break
            }
            el = el.parentNode
          }
          el && fn.call(el, e, el)
        })
        return element
    }
    on('click', '#div1', 'button', () => {
        console.log('button被點擊了')
    })
</script>

6、事件委託的侷限性

當然,事件委託也是有一定侷限性的:比如 focus、blur 之類的事件本身沒有事件冒泡機制,所以無法委託;mousemove、mouseout 這樣的事件,雖然有事件冒泡,但是隻能不斷通過位置去計算定位,對性能消耗高,因此也是不適合於事件委託的;

注意:JavaScript 是不支持事件的,事件是 DOM 上的東西,JavaScript 和 DOM 是瀏覽器上的兩個平行功能分支,他們兩個之間沒用從屬關係,JS 只是調用了 DOM 提供的 addEventListener 接口而已。試着用 JS 寫一個事件系統?

一句話:就是把子節點的事件綁定到最近的父節點上,利用事件冒泡來實現綁定。利用 event 對象下的 target 來尋找目標元素!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章