JavaScript事件捕獲冒泡與捕獲

四冥發表於2021-11-09

事件流

JavaScript中,事件流指的是DOM事件流。

概念

事件的傳播過程即DOM事件流。
事件物件在 DOM 中的傳播過程,被稱為“事件流”。
舉個例子:開電腦這個事,首先你是不是得先找到你的電腦,然後找到你的開機鍵,最後用手按下開機鍵。完成開電腦這個事件。這整個流程叫做事件流。

DOM事件流

DOM事件,也是有一個流程的。從事件觸發開始到事件響應是有三個階段。

  1. 事件捕獲階段
  2. 處於目標階段
  3. 事件冒泡階段

上面例子中,開電腦這個事件的過程就像JavaScript中的事件流,找開機鍵這個過程就是 事件捕獲 的過程,你找到開機鍵後,然後用手按開機鍵,這個選擇用手去按的過程就是 處於目標階段 按下開機按鈕,電腦開始開機這也就是 事件的冒泡。 順序為先捕獲再冒泡。

瞭解了事件源,讓我們看看它的三個過程吧!

1.事件捕獲

注:由於事件捕獲不被舊版本瀏覽器(IE8 及以下)支援,因此實際中通常在冒泡階段觸發事件處理程式。

事件捕獲處於事件流的第一步,
DOM事件觸發時(被觸發DOM事件的這個元素被叫作事件源),瀏覽器會從根節點開始 由外到內 進行事件傳播。即事件從文件的根節點流向目標物件節點。途中經過各個層次的DOM節點,最終到目標節點,完成事件捕獲。

2.目標階段

當事件到達目標節點的,事件就進入了目標階段。事件在目標節點上被觸發。
就是事件傳播到觸發事件的最底層元素上。

3.事件冒泡

事件冒泡與事件捕獲順序相反。事件捕獲的順序是從外到內,事件冒泡是從內到外。
當事件傳播到了目標階段後,處於目標階段的元素就會將接收到的時間向上傳播,就是順著事件捕獲的路徑,反著傳播一次,逐級的向上傳播到該元素的祖先元素。直到window物件。

看一個例子,點選 box3 會將 box2 與 box1 的點選事件觸發。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>JavaScript 事件冒泡</title>
</head>
<style type="text/css">
    #box1 { background: blueviolet;}
    #box2 {background: aquamarine;}
    #box3 {background: tomato;}
    div { padding: 40px; margin: auto;}
</style>

<body>
    <div id="box1">
        <div id="box2">
            <div id="box3"></div>
        </div>
    </div>
    <script>
        window.onload = function () {
            const box1 = document.getElementById('box1')
            const box2 = document.getElementById('box2')
            const box3 = document.getElementById('box3')
            box1.onclick = sayBox1;
            box2.onclick = sayBox2;
            box3.onclick = sayBox3;
            function sayBox3() {
                console.log('你點了最裡面的box');
            }
            function sayBox2() {
                console.log('你點了最中間的box');
            }
            function sayBox1() {
                console.log('你點了最外面的box');
            }
        }
    </script>
</body>

</html>

這個時候 click 捕獲的傳播順序為:
window -> document -> <html> -> <body> -> <div #box1> -> <div #box2> -> <div #box3>
這個時候 click 冒泡的傳播順序為:
<div #box3> -> <div #box2> -> <div #box1> -> <body> -> <html> -> document -> window

流程圖

現代瀏覽器都是從 window 物件開始捕獲事件的,冒泡最後一站也是 window 物件。而 IE8 及以下瀏覽器,只會冒泡到 document 物件。
事件冒泡:是由元素的 HTML 結構決定,而不是由元素在頁面中的位置決定,所以即便使用定位或浮動使元素脫離父元素的範圍,單擊元素時,其依然存在冒泡現象。

現在我們知道了事件流的三個階段後,那我們可以利用這個特性做什麼呢?

事件委託

設想這樣一個場景,當你有一堆的<li>標籤在一個<ul>標籤下,需要給所有的<li>標籤繫結onclick事件,這個問題我們可以用迴圈解決,但還有沒有更簡便的方式呢?
我們可以給這些<li>共同的父元素<ul>新增onclick事件,那麼裡面的任何一個<li>標籤觸發onclick事件時,都會通過冒泡機制,將onclick事件傳播到<ul>上,進行處理。這個行為叫做事件委託,<li>利用事件冒泡將事件委託到<ul>上。
也可以利用事件捕獲進行事件委託。用法是一樣的,只是順序反了。

  <ul id="myUl">
    <li>item 1</li>
    <li>item 2</li>
    <li>item 3</li>
    ...
  </ul>

可能還是有點不好理解,簡單來說,就是利用事件冒泡,將某個元素上的事件委託給他的父級。

舉個生活中的例子,雙十一快遞到了,需要快遞小哥送快遞一般是挨家挨戶送貨上門,這樣效率慢,小哥想了個辦法,把一個小區的快遞都放在小區裡面的快遞驛站,進行送快遞的事件委託,小區的收件人能通過取件碼去快遞驛站領取到自己的快遞。
在這裡,快遞小哥送快遞就是一個事件,收件人就是響應事件的元素,驛站就相當於代理元素,收件人憑著收穫碼去驛站裡面領快遞就是事件執行中,代理元素判斷當前響應的事件匹配該觸發的具體事件。

可是這樣做有什麼好處呢?

事件委託的優點

事件委託有兩個好處

  1. 減少記憶體消耗

  2. 動態繫結事件

  3. 減少記憶體消耗,優化頁面效能

在JavaScript中,每個事件處理程式都是物件,是物件就會佔用頁面記憶體,記憶體中的物件越多,頁面的效能當然越差,而且DOM的操作是會導致瀏覽器對頁面進行重排和重繪(這個不清楚的話,小夥伴可以瞭解頁面的渲染過程),過多的DOM操作會影響頁面的效能。效能優化主要思想之一就是為了最小化的重排和重繪也就是減少DOM操作。

在上面給<li>標籤繫結onclick事件的例子中,使用事件委託就可以不用給每一個<li>繫結一個函式,只需要給<ul>繫結一次就可以了,當li的數量很多時,無疑能減少大量的記憶體消耗,節約效率。

  1. 動態繫結事件

如果子元素不確定或者動態生成,可以通過監聽父元素來取代監聽子元素。
還是上面在<li>標籤繫結onclick事件的例子中, 很多時候我們的這些<li>標籤的數量並不是固定的,會根據使用者的操作對一些<li>標籤進行增刪操作。在每次增加或刪除標籤都要重新對新增或刪除元素繫結或解綁對應事件。

可以使用事件委託就可以不用給每一個<li>都要操作一遍,只需要給<ul>繫結一次就可以了,因為事件是繫結在<ul>上的,<li>元素是影響不到<ul>的 ,執行到<li>元素是在真正響應執行事件函式的過程中去匹配的,所以使用事件委託在動態繫結事件的情況下是可以減少很多重複工作的。

我們知道了事件委託的優點,那麼該如何使用呢?

事件委託的使用

事件委託的使用需要用的addEventListener()方法,事件監聽。
方法將指定的監聽器註冊到呼叫該函式的物件上,當該物件觸發指定的事件時,指定的回撥函式就會被執行。

  • 用法
element.addEventListener(eventType, function, useCapture);
  • 引數描述
引數 必/選填 描述
eventType 必填 指定事件的型別。
function 必填 指定事件觸發後的回撥函式。
useCapture 選填 指定事件是在捕獲階段執行還是在冒泡階段執行。

第三個引數 useCapture 是個布林型別,預設值為false

  • true - 表示事件在捕獲階段執行執行
  • false- 表示事件在冒泡階段執行執行

看下面例子

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>JavaScript 事件委託</title>
</head>

<body>

  <ul>
    <li>item 1</li>
    <li>item 2</li>
    <li>item 3</li>
    <li>item 4</li>
  </ul>

  <script>
    const myUl = document.getElementsByTagName("ul")[0];

    myUl.addEventListener("click", myUlFn);

    function myUlFn(e) {
      if (e.target.tagName.toLowerCase() === 'li') { // 判斷是否為所需要點選的元素
        console.log(`您點選了${e.target.innerText}`);
      }
    }

  </script>
</body>

</html>

⚠️ 這是一般的事件委託方法,但是這種寫法有問題,就是當_<li>_中還有子元素時,點選這個子元素就不會進行觸發事件。這個問題是一個坑。

事件冒泡有時候確實很有用,但是有時候也討人煩,當你不需要它的時候能不能取消掉呢?

禁止事件冒泡與捕獲

⚠️ 並不是所有事件都會冒泡,比如focus,blur,change,submit,reset,select等。

禁止冒泡和捕獲可以用到方法stopPropagation()
stopPropagation()起到阻止捕獲和冒泡階段中當前事件的進一步傳播。
這是阻止事件的冒泡方法,進行冒泡,但是預設事件任然會執行,當你呼叫了這個方法後。
如果點選一個a標籤,這個a標籤會進行跳轉。

使用起來也很簡單,沒有返回值也沒有引數。

event.stopPropagation();

請看下面例子,這個例子實在上文事件冒泡例子基礎上稍加修改得到的

    <div id="box1">
        <div id="box2">
            <div id="box3"></div>
        </div>
    </div>
    <script>
        const box1 = document.getElementById('box1')
        const box2 = document.getElementById('box2')
        const box3 = document.getElementById('box3')
        box1.onclick = sayBox1;
        box2.onclick = sayBox2;
        box3.onclick = sayBox3;
        function sayBox3() {
            console.log('你點了最裡面的box');
        }
        function sayBox2(e) {
            console.log('你點了最中間的box');
            e.stopPropagation(); //禁止事件捕獲和冒泡
        }
        function sayBox1() {
            console.log('你點了最外面的box');
        }
    </script>

當事件冒泡到box2時呼叫了在函式sayBox2,呼叫了e.stopPropagation(); 就停止冒泡了。

參考文獻

MDN中文版 https://developer.mozilla.org/zh-CN/
知乎 https://zhuanlan.zhihu.com/p/26536815

相關文章