JS 事件冒泡和事件捕獲

  • 2019 年 10 月 3 日
  • 筆記

寫在前面

W3C規定DOM事件流(event flow )存在三個階段:事件捕獲階段、處於目標階段、事件冒泡階段。dom標準事件流的觸發的先後順序為:先捕獲再冒泡,即當觸發dom事件時,會先進行事件捕獲,捕獲到事件源之後通過事件傳播進行事件冒泡。

event flow

對事件冒泡和捕捉的解釋

事件冒泡

在本示例中,當我們點擊孫子div的時候瀏覽器就會去檢查這個div身上沒有綁定事件,如果有就執行它;然後再檢查它的父元素(兒子div)身上有沒有綁定事件,如果有就執行;然後再檢查兒子div的父元素身上有沒有綁定事件,如果有就執行,依此類推,直到html根元素,這樣由內到外的向外觸發事件就叫做事件冒泡。

let parent = document.querySelector('.parent')  let child = document.querySelector('.child')  let grandson = document.querySelector('.grandson')    parent.addEventListener('click', function(e){    console.log('爸爸')  })    child.addEventListener('click', function(e){    console.log('兒子')  })    grandson.addEventListener('click', function(e){    console.log('孫子')  })  

事件冒泡

事件冒泡demo在線預覽

事件捕獲

事件捕獲剛好和事件冒泡相反,事件觸發時是先檢查最外層祖先html元素沒有綁定事件,如果有則執行它,然後在檢查,然後,它移動到<html>中單擊元素的下一個祖先元素,並執行相同的操作,然後是單擊元素再下一個祖先元素,依此類推,直到到達實際點擊的元素,就和剝洋蔥一樣。

let parent = document.querySelector('.parent')  let child = document.querySelector('.child')  let grandson = document.querySelector('.grandson')    parent.addEventListener('click', function(e){    console.log('爸爸')  }, true) // 注意:這個地方多了一個參數    child.addEventListener('click', function(e){    console.log('兒子')  }, true) // 注意:這個地方多了一個參數    grandson.addEventListener('click', function(e){    console.log('孫子')  }, true) // 注意:這個地方多了一個參數

事件捕獲

事件捕獲demo在線預覽

理解DOM事件的例子

addEventListener

在開發中,我們都是通過addEventListener給元素添加事件處理程式碼,可以同時給同一個元素添加多個事件處理程式程式碼

grandson.addEventListener('click', function(e){    console.log('孫子')  })  grandson.addEventListener('click', function(e){    console.log('孫子')  })  // 控制台會列印2次孫子

默認情況下,所有事件處理程式都是在冒泡階段註冊的,如果想在捕獲階段註冊一個事件,那麼你可以通過使用addEventListener()註冊您的處理程式,並將可選的第三個參數useCapture設置為true,capture 就是捕獲的意思

grandson.addEventListener('click', function(e){    console.log('孫子')  }, true) // 第三個參數 useCapture 設置為 true 

阻止默認行為

e.preventDefault()

參考

阻止事件傳播

e.stopPropagation()

實戰

點擊別處關閉浮層Demo

<!DOCTYPE html>  <html>  <head>    <meta charset="utf-8">    <meta name="viewport" content="width=device-width">    <title>JS Bin</title>  </head>  <body>    <div class="container">      <button id="btnOpen">打開浮動層</button>      <div class="popover" id="popoverlayer">        這裡是浮動層中顯示的內容        <br>        <label ><input type="checkbox" />一個checkbox</label>      </div>    </div>  </body>  </html>
body{    border: 5px solid red  }  .container{    border: 1px solid blue;    position: relative;    display: inline-block  }  .popover{    display: none;    position: absolute;    top: 0;    left: 100%;    width: 200px;    height: 200px;    margin-left: 10px;    background: #fff;    border: 1px solid #000;  }  .popover::before{    content: '';    position: absolute;    top: 5px;    left: -20px;    border: 10px solid transparent;    border-right: 10px solid #000;  }  .popover::after{    content: '';    position: absolute;    top: 5px;    left: -19px;    border: 10px solid transparent;    border-right: 10px solid #fff;  }  .popover.active{    display: block;  }
let container = document.querySelector('.container');  let btnOpen = document.querySelector('#btnOpen');  let popoverlayer = document.querySelector('#popoverlayer');    btnOpen.addEventListener('click', function(e){    console.log('click');    popoverlayer.classList.add('active');    e.stopPropagation();      // 方法2:註冊 document click 只執行一次  //   document.addEventListener('click', function(){  //     console.log('document once click');  //     popoverlayer.classList.remove('active');  //   }, {once: true})    })    popoverlayer.addEventListener('click', function(e){    console.log('layer click');    // 阻止傳播,為了避免點擊浮層時不關閉    e.stopPropagation();    })    // 注意這裡監聽的是 document,而不是body  // 因為當body的高度不夠的時候,點擊浮層的外部是不會關閉的  // 方法1  document.addEventListener('click', function(){    console.log('document click');    popoverlayer.classList.remove('active');  })

關閉浮層demo在線預覽

事件委託

使用事件委託實現點擊點擊不同的 li 元素 來獲取歌曲的 id

<!DOCTYPE html>  <html>  <head>    <meta charset="utf-8">    <meta name="viewport" content="width=device-width">    <title>JS Bin</title>  </head>  <body>  <ul class="musiclist">    <li data-id="1"><span>歌曲1</span><span>作者1</span></li>    <li data-id="2"><span>歌曲2</span><span>作者2</span></li>    <li data-id="3"><span>歌曲3</span><span>作者3</span></li>    <li data-id="4"><span>歌曲4</span><span>作者4</span></li>    <li data-id="5"><span>歌曲5</span><span>作者5</span></li>  </ul>  </body>  </html>
*{    padding: 0;    margin:0;    box-sizing: border-box;  }  body{    border: 1px solid #000;    padding: 10px;  }  ul,ol{    list-style: none;  }  .musiclist{    }  .musiclist li{    display: block;    padding: 15px 10px;    background: #fff;    border-bottom: 1px solid #ddd;  }  .musiclist li:hover{    background: #eee;  }  .musiclist li>span:first-child{    display: inline-block;    font-size: 16px;    color: #000;  }  .musiclist li>span:nth-child(2){    font-size: 10px;    color: #000;    margin-left: 10px;  }  
let musiclist = document.querySelector('.musiclist');  // 事件處理程式綁定在歌曲列表上面  musiclist.addEventListener('click', function(e){    console.log('target', e.target);    console.log('currentTarget', e.currentTarget);      let target = e.target;      // 當點擊的元素是 li 時,才能獲取到 dataset 屬性    if(target.nodeName === 'LI'){      console.log('當前歌曲ID是' + target.dataset.id)    }  })

事件委託demo

e.target 和 e.currentTarget 的區別

e.target 是當前觸發事件的元素

e.currentTarget 是事件綁定的元素

e.target 和 e.currentTarget 的區別

參考

常見的事件列表

事件介紹

EventListener

Event Flow

編寫一個EventUtil工具類實現事件管理兼容