事件傳播


在事件發生時,會有個 Event 實例收集事件的相關資訊,在遵守標準的瀏覽器上,Event 實例會作為事件處理器的的第一個參數,若要取得操作的目標物件,可以透過 Event 實例的 target 特性。

那麼操作的目標物件是指什麼呢?如果在按鈕上點選,那麼按鈕就是操作的目標物件,在〈基本事件模型〉中有說明過,觸發事件時,事件處理器的 this 會設定為當時的元素,那麼為何還要有特性指出操作目標物件?

事實上,在〈基本事件模型〉中,操作時若發生事件,並事件不僅停於操作的元素,還會從操作的元素往外傳播,若外層元素亦有設定對應的事件處理器,亦會呼叫事件處理器,這可以用下面的範例來示範:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body id="bodyId">

    <div id="divId">
        <button id="btnId">按我</button>            
    </div>
    <span id="console"></span>

<script type="text/javascript">
      function handler(event) {
          let target = event.target;
          document.getElementById('console').innerHTML += 
             `<br><b>this.id:</b> ${this.id}, <b>target.id:</b> ${target.id}`;
      }

      document.getElementById('bodyId').onclick = handler;
      document.getElementById('divId').onclick = handler;
      document.getElementById('btnId').onclick = handler;
</script>  
</body>
</html>

按我觀看結果

在上例中,按鈕是包括在 <div> 中,而 <body><div> 的外層元素,三者都設定了事件處理器。如果試著按下按鈕,則會看到結果如下:

this.id: btnId, target.id: btnId
this.id: divId, target.id: btnId
this.id: bodyId, target.id: btnId 

不僅按鈕的事件處理器被呼叫,外層 <div><body> 也依序被呼叫,這樣的行為叫作事件氣泡傳播(Event Bubbling),事件傳播至元素並呼叫事件處理器時,this 就設定為該元素,這可以從 this.id 的顯示結果觀察到,並注意到,由於操作時按下的是按鈕,所以操作目標元素就是按鈕,這可以由 target.id 觀察到。

事件氣泡傳播可以善用。例如在〈修改文件〉中動態新增圖片的例子,每建立一個新的 <img>,就設定該 <img>click 事件處理器,以便在點選圖片時自動移除圖片。若利用事件氣泡傳播,可以只在 <div> 上設定一次事件處理器,完成相同的結果。例如:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body>

    <input id="src" type="text"><button id="add">新增圖片</button>
    <div id="images"></div>

<script type="text/javascript">
    let images = document.getElementById('images');
    images.onclick = function(event) {
        this.removeChild(event.target);
    };
    document.getElementById('add').onclick = function() {
        let img = document.createElement('img');
        img.src = document.getElementById('src').value;
        images.appendChild(img);
    };
</script>  

  </body>
</html>

按我觀看結果

如果想要停止事件傳播,在遵守標準的瀏覽器上,必須呼叫 EventstopPropagation() 方法。例如要將第一個範例停止目標元素外的事件傳播,可以如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body id="bodyId">

    <div id="divId">
        <button id="btnId">按我</button>            
    </div>
    <span id="console"></span>

<script type="text/javascript">
      function handler(event) {
          let target = event.target;
          document.getElementById('console').innerHTML += 
             `<br><b>this.id:</b> ${this.id}, <b>target.id:</b> ${target.id}`;
          event.stopPropagation();
      }

      document.getElementById('bodyId').onclick = handler;
      document.getElementById('divId').onclick = handler;
      document.getElementById('btnId').onclick = handler;
</script>  
</body>
</html>

按我觀看結果

在〈DOM Level 2 事件模型〉中,事件會歷經兩個傳播階段,當事件發生時,會先從 document 往內傳播至操作目標元素,這個階段稱之為捕捉階段(Capturing phase),接著事件再從操作目標元素往外傳播至 document,這個階段稱之為氣泡階段(Bubbling phase)。

addEventListener 方法的第三個參數若為 true,表示事件處理器將作為捕捉階段處理器,若為 false 則為氣泡階段處理器。

例如,可改寫第一個範例,同時設定兩個階段的處理器來觀察事件:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body id="bodyId">
    <div id="divId">
        <button id="btnId">按我</button>
    </div>
    <span id="console"></span>

<script type="text/javascript">

      function handler(event) {
          let currentTarget = event.currentTarget;
          let target = event.target;
          document.getElementById('console').innerHTML += 
              `<br><b>currentTarget.id:</b> ${currentTarget.id}, <b>target.id:</b> ${target.id}`;
      }

      document.getElementById('bodyId').addEventListener('click', handler, true);
      document.getElementById('bodyId').addEventListener('click', handler, false);

      document.getElementById('divId').addEventListener('click', handler, true);
      document.getElementById('divId').addEventListener('click', handler, false);

      document.getElementById('btnId').addEventListener('click', handler, true);
      document.getElementById('btnId').addEventListener('click', handler, false);

</script>  
</body>
</html>

按我觀看結果

操作的目標元素,可以使用 Eventtarget 特性取得。如果按下按鈕,會發現以下的結果,可發現事件先從外往內,再從內往外傳播:

currentTarget.id: bodyId, target.id: btnId
currentTarget.id: divId, target.id: btnId
currentTarget.id: btnId, target.id: btnId
currentTarget.id: btnId, target.id: btnId
currentTarget.id: divId, target.id: btnId
currentTarget.id: bodyId, target.id: btnId

如果想要停止事件傳播,也是呼叫 EventstopPropagation 方法。例如上一個範例若僅註冊浮昇處理器,要停止目標元素外的事件傳播,可以如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body id="bodyId">
    <div id="divId">
        <button id="btnId">按我</button>
    </div>
    <span id="console"></span>

<script type="text/javascript">

      function handler(event) {
          let currentTarget = event.currentTarget;
          let target = event.target;
          document.getElementById('console').innerHTML += 
              `<br><b>currentTarget.id:</b> ${currentTarget.id}, <b>target.id:</b> ${target.id}`;
          event.stopPropagation();
      }

      document.getElementById('bodyId').addEventListener('click', handler, false);
      document.getElementById('divId').addEventListener('click', handler, false);
      document.getElementById('btnId').addEventListener('click', handler, false);

</script>  
</body>
</html>

按我觀看結果