JavaScript事件相關基礎及事件委託

JavaScript 與 HTML 之間的交互是經過事件來實現的, 因此瞭解事件的基本內容是必要的。文章將討論 JavaScript 事件的部分相關內容,包括事件流、事件處理程序、事件對象、事件委託,以上內容分別構成了文章的 1~4 小節。html

1 事件流

已知網頁的 DOM 節點構成的是樹狀結構,稱爲 DOM 樹。瀏覽器

並且又知當頁面上的一個節點觸發了事件時,接收到事件通知的不只僅只有這個節點,而是沿着它所在的DOM樹的一支從 Document 到這個節點自己的全部元素均可以接收到事件的通知,可是接收到通知也是有前後順序的,事件流就描述了這一支DOM樹中上的元素接收到事件通知的順序。函數

不一樣瀏覽器廠商對於事件流的方向的實現不一樣甚至相反,能夠分爲兩種:冒泡流和捕獲流。有趣的是,這兩種事件流幾乎是徹底相反的,下面分別討論這兩種事件流。同時討論DOM規定的事件流的三個階段。性能

1.1 事件冒泡

IE的事件流叫作事件冒泡(event bubbling),顧名思義,這是從下到上來的。即從DOM樹的下層向上層流動,也就是從具體的元素向較爲不具體的元素傳播,也能夠說是子元素向父元素的方向傳播。優化

例如在下面的DOM結構(縮進表示節點的層級)中:ui

html
        head
        body
            div
複製代碼

DOM樹結構和冒泡事件流方向是下面這樣的:this

因此可知若是觸發了上面div元素的事件,則接收到事件通知的元素和順序爲:spa

div -> body -> html -> document
複製代碼

1.2 事件捕獲

與冒泡流相反,事件捕獲的思想是從DOM樹的最上方傳到觸發事件的節點自己,也就是從較爲不具體的元素傳向具體的元素傳播,也能夠說從父元素向子元素傳播。設計

以上一節的DOM結構爲例,可知事件捕獲流的示意圖爲:code

即若是在div元素上觸發了事件,則受到事件通知的元素的順序是:

document -> html -> body -> div
複製代碼

雖然事件是在div身上觸發的,但它倒是最後才收到事件通知的。

1.3 DOM 事件流

DOM2級事件規定的事件流是分階段的,共分紅三個階段,即:

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

也就是說DOM2級規定的事件流包括了冒泡和捕獲這兩個不一樣方向的事件流,同時加上了一個處於目標的階段。

須要注意的是DOM2級定義的捕獲階段並不會發生到觸發事件的元素自己,而是隻到它的上層節點;而處於目標階段則發生且只發生在觸發事件的元素上;冒泡階段管的是最寬的,它從觸發事件的元素自己到DOM樹的頂部都會涉及到。

仍然以1.1節中的DOM結構爲例,從下面的示意圖看這三個階段會比較清晰(爲了方便,略去了無關的head節點):

從上面能夠看到,捕獲階段從根節點到body節點就中止了;處於目標階段只發生在了div身上;冒泡階段則是從div一直通知到了根節點。

雖然DOM規定了在捕獲階段不要涉及到目標元素,可是大多數瀏覽器都沒有遵循這個規定,都涉及到了目標元素,即以下圖所示:

2 事件處理程序

添加事件處理程序的方式能夠分紅如下幾種:

  1. HTML 標籤中直接添加處理程序
  2. DOM 0 級處理程序
  3. DOM 2級處理程序
  4. IE 的事件處理程序

2.1 HTML 標籤中直接處理事件

能夠在HTML標籤中直接用onXXX來指定事件處理程序,例如指定元素被點擊時輸出一個字符串能夠這樣作:

<div onclick="console.log('div is clicked'); alert('clicked')"></div>
複製代碼

則控制檯裏會輸出div is clicked,窗口會彈框輸出clicked

也能夠調用頁面中其餘部分的代碼,例如調用一個在script標籤中定義的函數:

<script>
//  定義了一個函數 fn    
function fn(){
    console.log(this + '調用了函數 fn!');
}
</script>

<!-- 調用頁面中的 fn 函數 -->
<div onclick="fn()"></div>
複製代碼

注意:通常不會用到這種方式直接處理事件,由於有以下幾個缺點:

  1. 若是函數在被解析以前就調用了,則會報錯
  2. 這樣會致使 HTML 和 JavaScript 的強耦合,改動比較麻煩

2.2 DOM 0 級事件處理程序

這種方式爲用 on加上事件名稱來指定事件處理程序,例如

element.onClick = handler
複製代碼

下面舉一個更具體的例子,獲取頁面上的一個idcontainer的元素,並指定點擊事件的處理函數:

<!-- id是target的元素 -->
<div id="target"></div>

<script>
// 獲取id是target的元素
let target = document.getElementById('target');

// 給 target 指定點擊事件的處理程序
target.onclick = function(){
    console.log('div#target is clicked');
}

</script>
複製代碼

當點擊 div#target時,控制檯會輸出div#target is clicked

2.2.1 解綁事件處理程序

能夠將事件處理程序指定爲 null 來解除原來爲元素指定的事件處理程序,例如將上面target的點擊事件的處理程序給解綁:

// 解綁點擊事件的處理程序
target.onclick = null;
複製代碼

這樣再點擊這個元素就不會有任何反應了

2.2.2 注意

這種事件處理程序的指定方式有以下特色和須要注意的點:

  1. 事件處理程序中的this指向的是這個元素自己,因此能夠直接引用它身上的屬性,例如給上個例子裏的 target元素加上三個類名,並在事件處理程序中經過 this屬性來獲得它的類名這個屬性:

    <div id="target" class="cname1 cname2 cname3"></div>
    
    target.onclick = function(){
        console.log(this.className);
    }
    複製代碼

    當點擊div#target時控制檯會輸出:cname1 cname2 cname3

  2. 這種方法只能夠指定一個事件處理程序,後面指定的會覆蓋前面的,例如:

    // 給 target 指定 第一個 點擊事件的處理程序
    target.onclick = function(){
        console.log('我是第一個');
    }
    
    // 給 target 指定 第二個 點擊事件的處理程序
    target.onclick = function(){
        console.log('我是第二個');
    }
    
    // 點擊 target 時會輸出:我是第二個
    複製代碼

    經過上面的實驗能夠看出後面定義的處理程序覆蓋了前面的。

2.3 DOM 2 級處理程序

DOM 2 級定義了兩個函數來給一個元素綁定和解綁事件處理程序,分別是

  1. addEventListener(eventName, handler, flag)來給調用的元素指定事件處理程序
  2. removeEventListener(eventName, handler, flag)來給調用的元素指定事件處理程序

這兩個函數都接收三個參數,分別是:

  • eventName: 事件的名稱,注意事件的名稱是不以on開頭的,例如點擊事件名爲click
  • handler:事件處理程序,能夠是個匿名函數,也能夠是實現定義好的具名函數
  • flag:布爾值,可取true || false;當取true時,會在捕獲階段調用事件處理程序,不然會在冒泡階段調用

例如,爲元素div#target使用這個方法添加事件處理程序:

target.addEventListener('click', function(){
    console.log('我是 addEventListener 添加的事件處理程序');
}, false);
// 點擊 target 時會輸出: 我是 addEventListener 添加的事件處理程序
複製代碼

上面的事件處理程序會在冒泡階段調用, 由於第三個參數 flagfalse

再例,用 removeEventListener 給元素取消某個事件處理程序:

// 添加事件處理程序
target.addEventListener('click', handler, false);
// 在沒寫下面的代碼以前點擊 target 會輸出 '我是事件處理程序'

// 取消事件處理程序
target.removeEventListener('click', handler, false);
// 如今點擊 target 元素就沒有什麼反應了

// 這個函數會被做爲事件處理程序來調用
function handler(){
    console.log('我是事件處理程序');
}
複製代碼

2.3.1 注意

這種添加和取消事件處理程序的方法有以下特色:

  • 能夠添加多個事件處理程序,會按添加的順序觸發

    // 添加兩個事件處理程序
    target.addEventListener('click', function(){
        console.log('我是第一個事件處理程序');
    }, false);
    
    target.addEventListener('click', function(){
        console.log('我是第二個事件處理程序');
    }, false);
    
    /* 點擊 target 元素會輸出 我是第一個事件處理程序 我是第二個事件處理程序 */
    複製代碼
  • 使用 removeEventListener 給元素取消事件處理程序時,參數要和對應的addEventListener的參數徹底對應才能夠,這意味着使用匿名函數做爲事件處理函數時沒法被取消,由於每一個匿名函數都不是同一個:

    target.addEventListener('click', function(){
        console.log(1);
    }, false);
    
    // 試圖取消上面的事件處理函數
    target.removeEventListener('click', function(){
        console.log(1);
    }, false);
    
    // 點擊 target 仍會輸出 1, 由於 removeEventListener 中做爲的事件處理程序的匿名函數
    // 並非 addEventListener 中的事件處理函數同樣,雖然他們內部定義的行爲同樣
    複製代碼

    因此,若是但願隨時取消一個事件處理程序,那麼不要使用匿名函數。

2.4 IE 的事件處理程序

IE 實現了兩個與DOM2級類似的方法:

  1. attachEvent(eventName, handler):添加事件處理程序
  2. detachEvent(eventName, handler):解綁事件處理程序

這兩個函數都接收兩個參數,分別是:

  • eventName: 事件的名稱,注意事件的名稱與DOM 2級的兩個方法addEventListener 和 removeEventListener不一樣,此處是以on開頭的,例如點擊事件名爲onclick
  • handler:事件處理程序,能夠是個匿名函數,也能夠是實現定義好的具名函數

2.4.1 注意

IE 的這兩個事件處理程序有幾個要注意的點:

  • 第一個參數事件名 eventName 是以 on 開頭的,和DOM 2級的兩個方法的 eventName 的不以 on開頭不一樣
  • 一樣能夠添加多個事件處理程序,可是後面添加的程序會在前面被調用,這和DOM 2級的方法依然不一樣
  • 事件處理程序的this屬性指向的是全局對象,不是當前的元素
  • 事件處理程序只會在冒泡階段被調用,不涉及事件捕獲階段

在編寫跨瀏覽器的事件處理程序時,爲了保持和IE 瀏覽器的一致性,應將DOM 2級的兩個方法的第三個參數設置爲 false ,以在事件冒泡階段被調用。

3 事件對象

在一個事件被觸發時,會產生一個事件對象,這個事件對象包含着與當前事件有關的全部信息,並且會被傳入事件處理程序中以便於獲得關於事件的全部信息。

全部瀏覽器都支持事件對象,可是具體實現不一樣,能夠分紅DOM中的事件對象和IE中的事件對象兩種。

3.1 DOM 中的事件對象

不管是DOM0級仍是DOM2級都支持事件對象,並且行爲相同。

例如,在將事件對象以 event 爲名並傳入事件處理程序中,並且在其中訪問它的一個屬性:

target.addEventListener('click', function(event){
    console.log('DOM 2 級中的event.type是: ' + event.type);
}, false);

target.onclick = function(event){
    console.log('DOM 0 級中的event.type是: ' + event.type);
}

/* 點擊時會輸出 DOM 2 級中的event.type是 click DOM 0 級中的event.type是 click */
複製代碼

上面的結果能夠看出兩種事件的指定方法中的事件對象的type屬性值沒有區別。

DOM的事件對象中有不少屬性和方法可供訪問和使用,下面舉幾個經常使用的例子,更詳細的信息能夠去紅寶書上查閱。

  • currentTarget: 當前事件處理程序在哪一個元素上,因爲有事件流,因此事件會在多個元素上被處理,如前文第1節所述所述
  • target:觸發事件的元素
  • preventDefault()函數:取消事件的默認行爲,這個函數最爲熟悉的用法應該是用來取消連接元素<a>的默認跳轉行爲,若是使用了這個函數,則點擊<a>連接時就不會自動跳轉了。注意:只對cancelable屬性值爲true的事件對象纔有效
  • stopPropagation()函數:取消事件繼續冒泡或者捕獲

其中currentTargettarget屬性值得區分一下,簡單的說前者能夠用來判斷一個事件已經流到了哪裏,也就是已經捕獲或者冒泡到了哪裏;後者能夠用來判斷一個事件的觸發者是什麼元素。例以下面的例子:

定義兩層嵌套的div元素,都在事件處理函數中輸出currentTargettarget,觀察兩個元素中的屬性差異:

<div id="outer" style="width: 300px; height: 300px; background-color: brown;">
    <div id="inner" style="width: 100px; height: 100px; background-color: cadetblue;"></div>
</div>

outer.addEventListener('click', function(event){
    console.log('>> 這是 outer 的事件處理函數');
    console.log('如今事件冒泡到了 ' + event.currentTarget.id);
    console.log('這個事件的發起元素是 ' + event.target.id);
}, false);

inner.addEventListener('click', function(event){
    console.log('>> 這是 inner 的事件處理函數');
    console.log('如今事件冒泡到了 ' + event.currentTarget.id);
    console.log('這個事件的發起元素是 ' + event.target.id);
}, false);

/*
當點擊內部元素時,控制檯輸出:
    >> 這是 inner 的事件處理函數
    如今事件冒泡到了 inner
    這個事件的發起元素是 inner
    >> 這是 outer 的事件處理函數
    如今事件冒泡到了 outer
    這個事件的發起元素是 inner
*/
複製代碼

能夠看到 currentTarget屬性隨着事件流的流向而變化,當事件冒泡到 inner 函數時, currentTarget指向的是 inner,當冒泡到 outer時,currentTarget指向的是 outer

同時能夠看出 current 屬性始終是指向inner的,由於innerclick事件的觸發元素,因此不管事件冒泡到了哪一個元素,事件對象的target屬性始終指向的是inner

3.2 IE8及以前的事件對象

IE的事件對象也有本身特有的屬性,在IE8以前的事件對象的屬性雖然和DOM中的事件對象的屬性名稱不一樣,可是具備對應關係,具體以下:

  • canceBubble屬性:布爾類型,當設置爲true時能夠取消事件冒泡,默認爲false
  • returnValue屬性:布爾類型,當設置爲false時能夠取消事件的默認行爲,默認值是true
  • srcElement屬性:指向事件的觸發元素,與DOM中的target屬性對應
  • type屬性:值爲數據類型

IE8及以前中的事件對象的獲取方式根據指定方式的不一樣而不一樣:當經過DOM 0級指定事件處理程序時,事件對象是windowevent屬性,這並非默認傳入事件處理函數中的:

outer.onclick = function(e){
    var event = window.event;
    console.log(event.src);
}
複製代碼

在IE9及以後的版本中,實現了兼容DOM事件對象。因此在IE9及以後,既可使用DOM事件對象,也可使用IE自己特有的事件對象(MSEventObj)。並且在IE11以後,在DOM0級的方式中,經過傳遞的event對象和從window中取出的event對象均指向同一個對象:

outer.onclick = function(e){
    console.log(e === window.event);  // IE11:true; IE10-IE8: false 
}
複製代碼

4 事件委託

頁面中事件處理程序的數量會影響到應用的性能,這是由於每一個事件處理函數都是對象,內存中對象越多,則佔用內存越多;其次,事件處理程序越多,每每意味着其中的DOM操做越多,而DOM操做的頻率是影響頁面性能的重要因素。

因爲1. 事件流 和 2. 事件對象中target屬性始終會指向當前事件的觸發元素自己這兩個性質的存在,便產生了事件委託策略來減小頁面中事件處理程序的綁定數量。

如有下面的DOM結構:

html
	head
    body
    	div#div1
        div#div2
複製代碼

對應的DOM樹爲

能夠看到div#containerbutton#btn兩個元素的事件流都會通過document元素。

若是想給div#containerbutton#btn兩個元素分別加上一個事件處理程序,例如:

div1.addEventListener('click', function(event){
    console.log('>> 這是 div1 的事件處理函數');
}, false);

/* 點擊 div1, 會輸出: >> 這是 div1 的事件處理函數 */


div2.addEventListener('click', function(event){
    console.log('>> 這是 div2 的事件處理函數');
}, false);

/* 按下 div2, 會輸出: >> 這是 div2 的事件處理函數 */
複製代碼

上面的例子給兩個div元素都添加了一個事件處理函數,可是咱們知道這兩個事件都會流到document上,所以能夠在document的事件處理程序中使用事件對象的target屬性來判斷事件的觸發元素是誰,並做出相應的處理:

document.addEventListener('click', function(event){
    switch (event.target.id) {  // 取出事件觸發者的 id 
        case 'div1':  // 若事件的觸發者是 div1
            console.log('>> 這是 div1 的事件處理函數');
            break;
        case 'div2':  // 若事件的觸發者是 div2
            console.log('>> 這是 div2 的事件處理函數');
            break;
        default:  
            break;
    }
}, false);

/* 點擊 div1, 會輸出: >> 這是 div1 的事件處理函數 按下 div2, 會輸出: >> 這是 div2 的事件處理函數 */
複製代碼

從上面能夠看出,靈活的利用事件流和事件對象的屬性能夠完成事件委託策略,將具體元素的事件委託到頂部元素上處理,這樣雖然增長了些許判斷的操做,卻能顯著減小頁面中事件處理程序的數量,達到優化性能的目的。

參考文獻:《JavaScript高級程序設計》
若有錯誤,感謝指正~

#完

相關文章
相關標籤/搜索