Javascript事件模型系列(三)jQuery中的事件監聽方式及異同點

呂大豹發表於2013-08-30

  作為全球最知名的js框架之一,jQuery的火熱程度堪稱無與倫比,簡單易學的API再加豐富的外掛,幾乎是每個前端程式設計師的必修課。從讀《鋒利的jQuery》開始,到現在使用jQuery有一年多的時間了,對jQuery算是比較瞭解了,唯一沒做到的就是讀原始碼。網上看到有人寫jQuery原始碼解析的,我也沒細看,個人覺得如果光是為了解析原始碼而解析原始碼,未免有點太勞神了,沒有實際用途,我更傾向於在實際應用中遇到不懂的方法或是文件說明不清楚的地方,可以查詢到相應的位置看下原始碼,足矣。

  閒話不多講了,今天的主題是jQuery中的事件監聽器的繫結方式。在學習jQuery之初,就在網上不只一次搜過相關主題,看了幾篇被抄來抄去的文章,算是瞭解了內情。今天去搜網上的文章依舊有很多,所以我就在思考我這篇文章該寫成什麼樣子,既能表達清楚主題而又與其他的文章不同。思考良久我決定,從jQuery原始碼的角度結合各種例子來把整個來龍去脈說個透徹,讓讀者看過這篇文章後就再也不需看別人的,理想有點豐滿哈~好像又是閒話了,速度奔主題。。。

jQuery中的四種事件監聽方式

  jQuery中提供了四種事件監聽方式,分別是bind、live、delegate、on,對應的解除監聽的函式分別是unbind、die、undelegate、off。在開始看他們之前,先來宣告一個例子,各函式的用法將圍繞這個例子進行,html程式碼如下:

<ol id="myol">
    <li>列表元素1</li>
    <li>列表元素2</li>
    <li>列表元素3</li>
    <li>列表元素4</li>
</ol>

  同時再宣告一個函式,用來作為監聽函式,JS程式碼如下:

function getHtml(){
    alert(this.innerHTML);
}

  看完例子大家應該明白想要幹什麼了,沒錯,就是實現點選每個列表元素的時候,把它的內部html彈出來,灰常簡單~

  忍不了了,奔主題奔主題!下面來分別看一下這四種方式:

bind(type,[data],function(eventObject))

  在我初學jQuery的時候,這個函式用的是最多的(基本上就認它),作用就是在選擇到的元素上繫結特定事件型別的監聽函式,引數的含義如下:

  type:事件型別,如click、change、mouseover等;

  data:傳入監聽函式的引數,通過event.data取到。可選;

  function:監聽函式,可傳入event物件,這裡的event是jQuery封裝的event物件,與原生的event物件有區別,使用時需要注意。

  來看看bind的原始碼:

bind: function( types, data, fn ) {
        return this.on( types, null, data, fn );
    }

  可以看到內部是呼叫了on方法,這個on是什麼樣的呢?稍後我們再看。先用我們上面的例子來試試:

$('#myol li').bind('click',getHtml);
  1. 列表元素1
  2. 列表元素2
  3. 列表元素3
  4. 列表元素4

  bind的特點就是會把監聽器繫結到目標元素上,有一個綁一個,在頁面上的元素不會動態新增的時候使用它沒什麼問題。但如果列表中動態增加一個“列表元素5”,點選它是沒有反應的,必須再bind一次才行。要想不這麼麻煩,我們可以使用live。jQuery還有一種事件繫結的簡寫方式如a.click(function(){});、a.change(function(){});等,它們的作用與bind一樣,僅僅是簡寫而已。

live(type, [data], fn)

  引數與bind一樣,原始碼是怎樣的呢?

live: function( types, data, fn ) {
        jQuery( this.context ).on( types, this.selector, data, fn );
        return this;
    }

  可以看到live方法並沒有將監聽器繫結到自己(this)身上,而是繫結到了this.context上了。這個context是什麼東西呢?其實就是元素的限定範圍,看了下面的程式碼就清楚了:

$('#myol li').context; //document
$('#myol li','#myol').context; //document
$('#myol li',$('#myol')[0]); //ol 

  通常情況下,我們都不會像第三種方式那樣使用選擇器,所以也就認為這個context通常就是document了,即live方法把監聽器繫結到了document上了。不把監聽器直接繫結在元素上,你是不是想起事件委託機制來了呢?若沒有,可以點選這裡回憶一下。live正是利用了事件委託機制來完成事件的監聽處理,把節點的處理委託給了document。在監聽函式中,我們可以用event.currentTarget來獲取到當前捕捉到事件的節點。下面的例子來揭曉:

$('#myol li').live('click',getHtml);
  1. 列表元素1
  2. 列表元素2
  3. 列表元素3
  4. 列表元素4

 

  使用事件委託的優點一目瞭然,新新增的元素不必再繫結一次監聽器。看來live這貨還真不錯,以後拋棄bind就用它了!可以嗎?答案是否定的,而且是大大的否定。因為將監聽器繫結到了document上,所以事件的處理得等待層層冒泡,直到冒泡到根節點才開始處理,在DOM樹較深或者節點的巢狀關係很複雜時,會有意想不到的結果,根節點的負擔太重了。就像四世同堂、五世同堂,甚至八世同堂(現實中不太可能,但在HTML中層級關係可能遠比這還多),老爺子肯定記不清哪個孫子是哪個兒子的,哪個重孫又是哪個兒子的兒子的,老爺子腦子一亂,糊塗了,事情就辦錯了。為此,jQuery官方已宣佈在1.7版本開始廢棄live,改用其他方式代替。所以我們也順應號召,罷用此方法。

  正因為live存在那樣的缺點,所以我們就思考,既然老爺子負擔那麼重,可不可以別把監聽器繫結在document上呢,繫結在就近的父級元素上不就好了。順應正常邏輯,delegate誕生了。

delegate(selector,type,[data],fn)

  引數多了一個selector,用來指定觸發事件的目標元素,監聽器將被繫結在呼叫此方法的元素上。看看原始碼:

delegate: function( selector, types, data, fn ) {
        return this.on( types, selector, data, fn );
    }

  又是呼叫了on,並且把selector傳給了on。看來這個on真的是舉足輕重的東西。照樣先不管它。看看示例先:

$('#myol').delegate('li','click',getHtml);
  1. 列表元素1
  2. 列表元素2
  3. 列表元素3
  4. 列表元素4

  我們在例子中將監聽器繫結到ol上,event.currentTarget顯示當前捕獲到事件的元素是ol。這下,我們的選擇又多了一些靈活性,不單可以利用事件委託,還可以選擇委託的物件。畢竟老麻煩同一個人幫忙很不好嘛。對於如何選擇委託物件,還是需要一定的策略的,畢竟父級元素可以有很多。我覺得原則應該是選擇最近的“穩定”元素,選擇最近是因為事件可以更快的冒泡上去,能夠在第一時間進行處理。所謂“穩定”是指該父級元素是一開始就在頁面上的,不是動態新增上來的,而且將來也不會消失掉,這樣可以保證它可以時時監控著自己的孩子。

  看了這麼多,你是不是迫不及待想看看這個on的真實面目了呢,這就來:

on(type,[selector],[data],fn)

  引數與delegate差不多但還是有細微的差別,首先type與selector換位置了,其次selector變為了可選項。交換位置的原因不好查證,應該是為了讓視覺上更舒服一些吧。至於selector為什麼是可選了呢,速度回想。。。對了,bind也呼叫了它,因為bind是繫結在了自己身上,所以只能傳個null進來。對嗎?不對,傳null和不傳是兩回事啊,差點掉坑裡。看看on的原始碼裡能不能找到線索呢,開啟原始碼只發現一句關鍵的:

return this.each( function() {
            jQuery.event.add( this, types, fn, data, selector );
        })

  在on的內部,又呼叫了event.add方法,納尼?順著再走一步好了,進入event.add檢視,越加複雜,不過還好,看到了一段熟悉的程式碼:

if ( elem.addEventListener ) {
     elem.addEventListener( type, eventHandle, false );
     } else if ( elem.attachEvent ) {
         elem.attachEvent( "on" + type, eventHandle );
        }

  看來已經開始進行事件監聽,不會再往深走了,舒一口氣繼續往下看:

// Add to the element's handler list, delegates in front
if ( selector ) {
     handlers.splice( handlers.delegateCount++, 0, handleObj );
     } else {
       handlers.push( handleObj );
       }

  傳不傳selector的區別就在此處了!但是!區別到底是什麼啊,智商捉急!翻來覆去就是看不明白。哎,本來想通過原始碼把事情說明白的,可惜在下不才,還是獻醜了。暫時還是說不明白了。等待日後更新此處吧。

         不過,解釋不明白原始碼還是可以通過例子來理解嘛!我們先不傳selector來看看:

$('#myol li').on('click',getHtml);
  1. 列表元素1
  2. 列表元素2
  3. 列表元素3
  4. 列表元素4

  可以看到event.currentTarget是li自己,與bind的效果一樣。我為什麼一直要糾結這個傳不傳selector的區別呢?老老實實傳進去不就完了。其實是因為之前有看過文章提到如何用on來代替bind和live的寫法,其中代替bind的寫法就是不傳selector進去,今日就想探清楚這個究竟。至於傳selector進去,就是跟delegate一樣的意義了,除了引數順序不同,其他完全一樣。

  終於看到on的真實作用了,那麼,這麼多的事件繫結方式,我們該如何進行選擇呢?

bind、live、delegate、on如何選擇?

  其實這個問題是完全不必糾結的,因為你已經知道他們之間的區別了不是麼?根據實際情況斟酌使用就行。不過官方有一個推薦就是儘量使用on,因為其他方法都是內部呼叫on來完成的,直接使用on可以提高效率,而且你完全可以用on來代替其他三種寫法。至於如何代替我想就不必這麼直白的寫出來了,真正理解它們的區別之後自然而然也就不是難事了。

總結一下

  jQuery的事件監聽方式就分析完了,除了後面看event.add原始碼有點水分之外,應該是能把整個原理解釋明白了。若對事件的冒泡和委託機制有不懂的,可以看下這篇文章《事件的捕獲-冒泡機制及事件委託機制》 有助於理解。能不能達到我最開始的理想呢?還請讀者評價吧,歡迎指點!

相關文章