本文主要介紹:javascript
針對不一樣級別的DOM,咱們的DOM事件處理方式也是不同的。css
DOM級別一共能夠分爲4個級別:DOM0級「一般把DOM1規範造成以前的叫作DOM0級」,DOM1級,DOM2級和 DOM3級,而DOM事件分爲3個級別:DOM0級事件處理,DOM2級事件處理和DOM3級事件處理。以下圖所示:html
在瞭解DOM0級事件以前,咱們有必要先了解下HTML事件處理程序,也是最先的這一種的事件處理方式,代碼以下:java
<button type="button" onclick="fn" id="btn">點我試試</button> <script> function fn() { alert('Hello World'); } </script> 複製代碼
那有一個問題來了,那就是fn要不要加括號呢?node
在html的onclick屬性中,使用時要加括號,在js的onclick中,給點擊事件賦值,不加括號。爲何呢?咱們經過事實來講話:segmentfault
// fn不加括號 <button type="button" onclick="fn" id="btn">點我試試</button> <script> function fn() { alert('Hello World'); } console.log(document.getElementById('btn').onclick); // 打印的結果以下:這個函數裏面包括着fn,點擊以後並無彈出1 /* ƒ onclick(event) { fn } */ </script> // fn 加括號,這裏就不重複寫上面代碼,只須要修改一下上面便可 <button type="button" onclick="fn()" id="btn">點我試試</button> <script> // 打印的結果以下:點擊以後能夠彈出1 /* ƒ onclick(event) { fn() } */ </script> 複製代碼
上面的代碼咱們經過直接在HTML代碼當中定義了一個onclick的屬性觸發fn方法,這樣的事件處理程序最大的缺點就是HTML與JS強耦合,當咱們一旦須要修改函數名就得修改兩個地方。固然其優勢就是不須要操做DOM來完成事件的綁定。瀏覽器
DOM0事件綁定,給元素的事件行爲綁定方法,這些方法都是在當前元素事件行爲的冒泡階段(或者目標階段)執行的。markdown
那咱們如何實現HTML與JS低耦合?這樣就有DOM0級處理事件的出現解決這個問題。DOM0級事件就是將一個函數賦值給一個事件處理屬性,好比:dom
<button id="btn" type="button"></button> <script> var btn = document.getElementById('btn'); btn.onclick = function() { alert('Hello World'); } // btn.onclick = null; 解綁事件 </script> 複製代碼
上面的代碼咱們給button定義了一個id,而後經過JS獲取到了這個id的按鈕,並將一個函數賦值給了一個事件處理屬性onclick,這樣的方法即是DOM0級處理事件的體現。咱們能夠經過給事件處理屬性賦值null來解綁事件。DOM 0級的事件處理的步驟:先找到DOM節點,而後把處理函數賦值給該節點對象的事件屬性。編輯器
DOM0級事件處理程序的缺點在於一個處理程序「事件」沒法同時綁定多個處理函數,好比我還想在按鈕點擊事件上加上另一個函數。
var btn = document.getElementById('btn'); btn.onclick = function() { alert('Hello World'); } btn.onclick = function() { alert('沒想到吧,我執行了,哈哈哈'); } 複製代碼
DOM2級事件在DOM0級事件的基礎上彌補了一個處理程序沒法同時綁定多個處理函數的缺點,容許給一個處理程序添加多個處理函數。也就是說,使用DOM2事件能夠隨意添加多個處理函數,移除DOM2事件要用removeEventListener。代碼以下:
<button type="button" id="btn">點我試試</button> <script> var btn = document.getElementById('btn'); function fn() { alert('Hello World'); } btn.addEventListener('click', fn, false); // 解綁事件,代碼以下 // btn.removeEventListener('click', fn, false); </script> 複製代碼
DOM2級事件定義了addEventListener和removeEventListener兩個方法,分別用來綁定和解綁事件
target.addEventListener(type, listener[, useCapture]); target.removeEventListener(type, listener[, useCapture]); /* 方法中包含3個參數,分別是綁定的事件處理屬性名稱(不包含on)、事件處理函數、是否在捕獲時執行事件處理函數(關於事件冒泡和事件捕獲下面會介紹) */ 複製代碼
注:
IE8級如下版本不支持addEventListener和removeEventListener,須要用attachEvent和detachEvent來實現:
// IE8級如下版本只支持冒泡型事件,不支持事件捕獲因此沒有第三個參數 // 方法中包含2個參數,分別是綁定的事件處理屬性名稱(不包含on)、事件處理函數 btn.attachEvent('onclick', fn); // 綁定事件 btn.detachEvent('onclick', fn); // 解綁事件 複製代碼
DOM3級事件在DOM2級事件的基礎上添加了更多的事件類型,所有類型以下:
同時DOM3級事件也容許使用者自定義一些事件。
DOM事件級別的發展使得事件處理更加完整豐富,而下一個問題就是以前提到的DOM事件模型。「事件冒泡和事件捕獲」
假如在一個button上註冊了一個click事件,又在其它父元素div上註冊了一個click事件,那麼當咱們點擊button,是先觸發父元素上的事件,仍是button上的事件呢,這就須要一種約定去規範事件的執行順序,就是事件執行的流程。
瀏覽器在發展的過程當中出現了兩種不一樣的規範
DOM事件模型分爲捕獲和冒泡。一個事件發生後,會在子元素和父元素之間傳播(propagation)。這種傳播分紅三個階段。
(1)捕獲階段:事件從window對象自上而下向目標節點傳播的階段;
(2)目標階段:真正的目標節點正在處理事件的階段;
(3)冒泡階段:事件從目標節點自下而上向window對象傳播的階段。
上文中講到了addEventListener的第三個參數爲指定事件是否在捕獲或冒泡階段執行,設置爲true表示事件在捕獲階段執行,而設置爲false表示事件在冒泡階段執行。那麼什麼是事件冒泡和事件捕獲呢?能夠用下圖來解釋:
捕獲是從上到下,事件先從window對象,而後再到document(對象),而後是html標籤(經過document.documentElement獲取html標籤),而後是body標籤(經過document.body獲取body標籤),而後按照普通的html結構一層一層往下傳,最後到達目標元素。咱們只須要將addEventListener的第三個參數改成true就能夠實現事件捕獲。代碼以下:
<!-- CSS 代碼 --> <style> body{margin: 0;} div{border: 1px solid #000;} #grandfather1{width: 200px;height: 200px;} #parent1{width: 100px;height: 100px;margin: 0 auto;} #child1{width: 50px;height: 50px;margin: 0 auto;} </style> <!-- HTML 代碼 --> <div id="grandfather1"> 爺爺 <div id="parent1"> 父親 <div id="child1">兒子</div> </div> </div> <!-- JS 代碼 --> <script> var grandfather1 = document.getElementById('grandfather1'), parent1 = document.getElementById('parent1'), child1 = document.getElementById('child1'); grandfather1.addEventListener('click',function fn1(){ console.log('爺爺'); },true) parent1.addEventListener('click',function fn1(){ console.log('爸爸'); },true) child1.addEventListener('click',function fn1(){ console.log('兒子'); },true) /* 當我點擊兒子的時候,我是否點擊了父親和爺爺 當我點擊兒子的時候,三個函數是否調用 */ // 請問fn1 fn2 fn3 的執行順序? // fn1 fn2 fn3 or fn3 fn2 fn1 </script> 複製代碼
先來看結果吧:
當咱們點擊id爲child1的div標籤時,打印的結果是爺爺 => 爸爸 => 兒子,結果正好與事件冒泡相反。
所謂事件冒泡就是事件像泡泡同樣從最開始生成的地方一層一層往上冒。咱們只須要將addEventListener的第三個參數改成false就能夠實現事件冒泡。代碼以下:
//html、css代碼同上,js代碼只是修改一下而已 var grandfather1 = document.getElementById('grandfather1'), parent1 = document.getElementById('parent1'), child1 = document.getElementById('child1'); grandfather1.addEventListener('click',function fn1(){ console.log('爺爺'); },false) parent1.addEventListener('click',function fn1(){ console.log('爸爸'); },false) child1.addEventListener('click',function fn1(){ console.log('兒子'); },false) /* 當我點擊兒子的時候,我是否點擊了父親和爺爺 當我點擊兒子的時候,三個函數是否調用 */ // 請問fn1 fn2 fn3 的執行順序? // fn1 fn2 fn3 or fn3 fn2 fn1 複製代碼
先來看結果吧:
好比上圖中id爲child1的div標籤爲事件目標,點擊以後後同時也會觸發父級上的點擊事件,一層一層向上直至最外層的html或document。
注:當第三個參數爲false
或者爲空的時候,表明在冒泡階段綁定。
因爲事件會在冒泡階段向上傳播到父節點,所以能夠把子節點的監聽函數定義在父節點上,由父節點的監聽函數統一處理多個子元素的事件。這種方法叫作事件的代理(delegation)。
舉個例子,好比一個宿舍的同窗同時快遞到了,一種方法就是他們都傻傻地一個個去領取,還有一種方法就是把這件事情委託給宿舍長,讓一我的出去拿好全部快遞,而後再根據收件人一一分發給每一個宿舍同窗;
在這裏,取快遞就是一個事件,每一個同窗指的是須要響應事件的 DOM 元素,而出去統一領取快遞的宿舍長就是代理的元素,因此真正綁定事件的是這個元素,按照收件人分發快遞的過程就是在事件執行中,須要判斷當前響應的事件應該匹配到被代理元素中的哪個或者哪幾個。
那麼利用事件冒泡或捕獲的機制,咱們能夠對事件綁定作一些優化。 在JS中,若是咱們註冊的事件愈來愈多,頁面的性能就愈來愈差,由於:
假設有一個列表,列表之中有大量的列表項,咱們須要在點擊每一個列表項的時候響應一個事件
// 例4 <ul id="list"> <li>item 1</li> <li>item 2</li> <li>item 3</li> ...... <li>item n</li> </ul> 複製代碼
若是給每一個列表項一一都綁定一個函數,那對於內存消耗是很是大的,效率上須要消耗不少性能。藉助事件代理,咱們只須要給父容器ul綁定方法便可,這樣無論點擊的是哪個後代元素,都會根據冒泡傳播的傳遞機制,把容器的click行爲觸發,而後把對應的方法執行,根據事件源,咱們能夠知道點擊的是誰,從而完成不一樣的事。
在不少時候,咱們須要經過用戶操做動態的增刪列表項元素,若是一開始給每一個子元素綁定事件,那麼在列表發生變化時,就須要從新給新增的元素綁定事件,給即將刪去的元素解綁事件,若是用事件代理就會省去不少這樣麻煩。
接下來咱們來實現上例中父層元素 #list 下的 li 元素的事件委託到它的父層元素上:
<ul id="list"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul> <script> // 給父層元素綁定事件 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); } }); </script> 複製代碼
這是常規的實現事件委託的方法,可是這種方法有BUG,當監聽的元素裏存在子元素時,那麼咱們點擊這個子元素事件會失效,因此咱們能夠聯繫文章上一小節說到的冒泡事件傳播機制來解決這個bug。改進的事件委託代碼:
<ul id="list"> <li>1 <span>aaaaa</span></li> <li>2 <span>aaaaa</span></li> <li>3 <span>aaaaa</span></li> <li>4</li> </ul> <script> // 給父層元素綁定事件 document.getElementById('list').addEventListener('click', function (e) { // 兼容性處理 var event = e || window.event; var target = event.target || event.srcElement; // 判斷是否匹配目標元素 /* 從target(點擊)元素向上找currentTarget(監聽)元素, 找到了想委託的元素就觸發事件,沒找到就返回null */ while(target.tagName !== 'LI'){ if(target.tagName === 'UL'){ target = null break; } target = target.parentNode } if (target) { console.log('你點擊了ul裏的li') } }); 複製代碼
若是調用這個方法,默認事件行爲將再也不觸發。什麼是默認事件呢?例如表單一點擊提交按鈕(submit)刷新頁面、a標籤默認頁面跳轉或是錨點定位等。
使用場景1:使用a標籤僅僅是想當作一個普通的按鈕,點擊實現一個功能,不想頁面跳轉,也不想錨點定位。
<a href="javascript:;">連接</a> 複製代碼
使用JS方法來阻止,給其click事件綁定方法,當咱們點擊A標籤的時候,先觸發click事件,其次纔會執行本身的默認行爲
<a id="test" href="http://www.google.com">連接</a> <script> test.onclick = function(e){ e = e || window.event; return false; } </script> 複製代碼
<a id="test" href="http://www.google.com">連接</a> <script> test.onclick = function(e){ e = e || window.event; e.preventDefault(); } </script> 複製代碼
使用場景2:輸入框最多隻能輸入六個字符,如何實現?
實現代碼以下:
<input type="text" id='tempInp'> <script> tempInp.onkeydown = function(ev) { ev = ev || window.event; let val = this.value.trim() //trim去除字符串首位空格(不兼容) // this.value=this.value.replace(/^ +| +$/g,'') 兼容寫法 let len = val.length if (len >= 6) { this.value = val.substr(0, 6); //阻止默認行爲去除特殊按鍵(DELETE\BACK-SPACE\方向鍵...) let code = ev.which || ev.keyCode; if (!/^(46|8|37|38|39|40)$/.test(code)) { ev.preventDefault() } } } </script> 複製代碼
event.stopPropagation() 方法阻止事件冒泡到父元素,阻止任何父事件處理程序被執行。demo代碼以下:
// 在事件冒泡demo代碼的基礎上修改一下 child1.addEventListener('click',function fn1(e){ console.log('兒子'); e.stopPropagation() },false) 複製代碼
stopImmediatePropagation 既能阻止事件向父元素冒泡,也能阻止元素同事件類型的其它監聽器被觸發。而 stopPropagation 只能實現前者的效果。咱們來看個例子:
<button id="btn">點我試試</button> <script> const btn = document.querySelector('#btn'); btn.addEventListener('click', event => { console.log('btn click 1'); event.stopImmediatePropagation(); }); btn.addEventListener('click', event => { console.log('btn click 2'); }); document.body.addEventListener('click', () => { console.log('body click'); }); </script> 複製代碼
根據打印出來的結果,咱們發現使用 stopImmediatePropagation後,點擊按鈕時,不只body綁定事件不會觸發,與此同時按鈕的另外一個點擊事件也不觸發。
從上面這張圖片中咱們能夠看到,event.target
指向引發觸發事件的元素,而event.currentTarget
則是事件綁定的元素。
所以沒必要記何時e.currentTarget
和e.target
相等,何時不等,理解二者的究竟指向的是誰便可。
e.target
指向觸發事件監聽的對象「事件的真正發出者」。e.currentTarget
指向添加監聽事件的對象「監聽事件者」。