解密jQuery事件核心 - 自定義設計(三)


接上文http://www.cnblogs.com/aaronjs/p/3447483.html

 本文重點:自定義事件

“通過事件機制,可以將類設計為獨立的模塊,通過事件對外通信,提高了程序的開發效率。”

對象之間通過直接方法調用來交互

1)對象A直接調用對象B的某個方法,實現交互;直接方法調用本質上也是屬於一種特殊的發送與接受消息,它把發送消息和接收消息合並為一個動作完成;

方法調用方和被調用方被緊密耦合在一起;因為發送消息和接收消息是在一個動作內完成,所以無法做到消息的異步發送和接收;

2)對象A生成消息->將消息通知給一個事件消息處理器(Observable)->消息處理器通過同步或異步的方式將消息傳遞給接收者;

這種方式是通過將消息發送和消息接收拆分為兩個過程,通過一個中間者來控制消息是同步還是異步發送;

在消息通信的靈活性方面比較有優勢,但是也帶來了一定的復雜度。但是復雜度一般可以由框架封裝,消息的發送方和接收方仍然可以做到比較簡單;

總的來說就是一種松耦合的處理,2個對象之間有太多緊密的直接關聯,應該要考慮通過消息通信解耦,從而提高應用程序的可維護性和重用性

 


在JS中,消息的通知是通過事件表達的,當代碼庫增長到一定的規模,就需要考慮將行為和自定義事件進行解耦。

了解自定義事件的概念

  • 類似DOM的行為:你在DOM節點(包括document對象)監聽並觸發自定義事件。這些事件既可以冒泡,也可以被攔截。這正是Prototype、jQuery和MooTools所做的。如果事件不能擴散,就必須在觸發事件的對象上進行監聽。
  • 命名空間:一些框架需要你為你的事件指定命名空間,通常使用一個點號前綴來把你的事件和原生事件區分開。
  • 自定義額外數據:JavaScript框架允許你在觸發自定義事件時,向事件處理器傳送額外的數據。jQuery可以向事件處理器傳遞任意數量的額外參數。
  • 通用事件API:只用Dojo保留了操作原生DOM事件的正常API。而操作自定義事件需要特殊的發布/訂閱API。這也意味着Dojo中的自定義事件不具有DOM事件的一些行為(比如冒泡)。 
  • 聲明:我們往往需要在預定義的事件中加入一些特殊的變化(例如,需要Alt鍵按下才能觸發的單擊事件),MooTools運行你定義此類自定義事件。此類事件需要預先聲明,即便你只是聲明他們的名字。任何未聲明的自定義事件不會被觸發。

理論太抽象,看看jQuery框架中如何使用事件

 


案例

jQuery的事件自定義事件還是通過on綁定的,然后再通過trigger來觸發這個事件

//給element綁定hello事件
element.bind("hello",function(){
    alert("hello world!");
});
       
//觸發hello事件
element.trigger("hello");

這段代碼這樣寫似乎感覺不出它的好處,看了下面的例子也許你會明白使用自定義事件的好處了:

   

我們已一個選項卡的插件為例:

我們讓ul列表來響應點擊事件,當用戶點擊一個列表項時,給這個列表項添加一個名為active的類,同時將其他列表項中的active類移除,

以此同時讓剛剛點擊的列表對應的內容區域也添加active類。

HTML:

<ul id="tabs">
    <li data-tab="users">Users</li>
    <li data-tab="groups">Groups</li>
</ul>
<div id="tabsContent">
    <div data-tab="users">part1</div>
    <div data-tab="groups">part2</div>
</div>

 

jQuery

$.fn.tabs=function(control){
    var element=$(this);
    control=$(control);
    element.delegate("li","click",function(){
        var tabName=$(this).attr("data-tab");
         //點擊li的時候觸發change.tabs自定義事件 
        element.trigger("change.tabs",tabName);
    });
         
    //給element綁定一個change.tabs自定義事件
    element.bind("change.tabs",function(e,tabName){
        element.find("li").removeClass("active");
        element.find(">[data-tab='"+ tabName +"']").addClass("active");
    });    
    element.bind("change.tabs",function(e,tabName){
        control.find(">[data-tab]").removeClass("active");
        control.find(">[data-tab='"+ tabName +"']").addClass("active");
    });
    //激活第一個選項卡 
    var firstName=element.find("li:first").attr("data-tab");
    element.trigger("change.tabs",firstName);
                 
    return this;
};

 

從上面的例子我們可以看到使用自定義事件回調使得選項卡狀態切換回調彼此分離,讓代碼變得整潔易讀。

$("ul#tabs").tabs("#tabsContent");

 


jQuery.trigger 與 document.dispatchEvent 區分

瀏覽器提供自定義事件接口,那么就jQuery是不是利用這個原理呢?


 

第一種情況:DOM-events使用jQuery觸發。 觸發不會處理通過addEventListener綁定的事件
另一種:DOM-events觸發使用本機createEvent / dispatchEvent方法與用jQuery.bind注冊事件偵聽器

這是一個問題,如果你與非jQuery代碼混合jQuery代碼。例如,jQuery移動模擬orientationchange事件基於窗口尺寸和大小事件但它使用jQuery觸發orientationchange事件。 觸發,因此不調用本機事件偵聽器。

 


trigger的幾種常見用法

1.常用模擬

在jQuery中,可以使用trigger()方法完成模擬操作。例如可以使用下面的代碼來觸發id為btn按鈕的click事件。

$("#btn").trigger("click");

這樣,當頁面加載完畢后,就會立刻輸出想要的效果。

也可以直接用簡化寫法click(),來達到同樣的效果:

$("#btn").click();
 

2.觸發自定義事件

trigger()方法不僅能觸發瀏覽器支持的具有相同名稱的事件,也可以觸發自定義名稱的事件。

例如為元素綁定一個“myClick”的事件,jQuery代碼如下:

$("#btn").bind("myClick", function () {
    $("#test").append("<p>我的自定義事件。</p>");
});

想要觸發這個事件,可以使用下面的代碼來實現:

$("btn").trigger("myClick");
 

3.傳遞數據

trigger(tpye[,datea])方法有兩個參數,第一個參數是要觸發的事件類型,第二個單數是要傳遞給事件處理函數的附加數據,以數組形式傳遞。通常可以通過傳遞一個參數給回調函數來區別這次事件是代碼觸發的還是用戶觸發的。

下面的是一個傳遞數據的例子:

$("#btn").bind("myClick", function (event, message1, message2) { //獲取數據
    $("#test").append("p" + message1 + message2 + "</p>");
});
$("#btn").trigger("myClick",["我的自定義","事件"]); //傳遞兩個數據
$(“#btn”).trigger(“myClick”,["我的自定義","事件"]); //傳遞兩個數據

 

4.執行默認操作

triger()方法觸發事件后,會執行瀏覽器默認操作。例如:

$("input").trigger("focus");

以上代碼不僅會觸發為input元素綁定的focus事件,也會使input元素本身得到焦點(瀏覽器默認操作)。

如果只想觸發綁定的focus事件,而不想執行瀏覽器默認操作,可以使用jQuery中另一個類似的方法-triggerHandler()方法。

$("input").triggerHandler("focus");

該方法會觸發input元素上綁定的特定事件,同時取消瀏覽器對此事件的默認操作,即文本框指觸發綁定的focus事件,不會得到焦點。

 


jQuery自定義事件原理

根據API,trigger支持 .trigger()事件會在DOM樹上冒泡,在事件處理程序中返回false或調用事件對象中的.stopPropagation() 方法可以使事件停止冒泡

看看demo


 

 

按照tigger綁定的方式

$('ele').on('aaa',function(){})
$('ele').on('click',function(){})

第一種是自定義的事件名aaa,第二種是瀏覽器事件click

根據trigger的API,會處理冒泡這個關鍵點,

 


trigger需要處理的問題

1.模擬事件對象,用戶模擬處理停止事件冒泡

這個很明了,因為不是通過瀏覽器系統觸發的,而是自動觸發的,所以這個事件對象要如何處理?

2.區分事件類型,觸發標准的瀏覽器事件 和 自定義事件名綁定的處理程序。

例如:事件名稱+命名空間

p4.on('click.aaa.ccc',function(e,vv,c){
       console.log('p4')
   })

    p4.trigger('click.aaa')

所以trigger觸發的時

3.模擬冒泡機制

那么瀏覽器click類型,自然是本身支持冒泡這樣的行為,通過stopPropagation阻止即可

當然一些事件,如focusin和 blur本身不冒泡,但 jQuery 為了跨瀏覽器一致性, jQuery 需要在這些事件上模擬了冒泡行為,jQuery要如何處理?

那么如果是自定義的aaa的事件名,又如何處理冒泡?


源碼解讀

附上源碼

trigger源碼

 

初看trigger源碼部分,真有點暈,處理的hack太多了,但是仔細規划下,無非就是解決上面提到的幾點問題

 

1 命名空間的過濾

if ( type.indexOf(".") >= 0 ) {
            // Namespaced trigger; create a regexp to match event type in handle()
            namespaces = type.split(".");
            type = namespaces.shift();
            namespaces.sort();
        }

按照規范p4.trigger('click.aaa.ccc'),'click.aaa.ccc' 就是事件+命名空間的組合

判斷也挺巧妙,indexOf判斷有.是索引,即存在命名空間,然后踢掉第一個事件名

 

2 模擬事件對象

event = event[ jQuery.expando ] ?
            event :
            new jQuery.Event( type, typeof event === "object" && event );

在on機制里面就分析了,其實就是jQuery.Event類了

 

4 返回的事件數據合集

data = data == null ?
            [ event ] :
            jQuery.makeArray( data, [ event ] );

所以data就是事件回調返回的[event,data],如果傳遞了數據就合並到data中

 

5  jQuery.event.special

這個在很多地方用到,這個是用來做模擬事件的,比如提到的模擬聚焦冒泡之類的,下章再講

 

6 模擬事件冒泡

trigger與triggerHandler的本質區別實現在這里了

if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {

            bubbleType = special.delegateType || type;
            if ( !rfocusMorph.test( bubbleType + type ) ) {
                cur = cur.parentNode;
            }
            for ( ; cur; cur = cur.parentNode ) {
                eventPath.push( cur );
                tmp = cur;
            }

            // Only add window if we got to document (e.g., not plain obj or detached DOM)
            if ( tmp === (elem.ownerDocument || document) ) {
                eventPath.push( tmp.defaultView || tmp.parentWindow || window );
            }
        }

其實大致的手法都差不多了,無非就是遍歷所有的元素節點了,排個隊列出來

image

如果循環中最后一個cur是document,那么事件是需要最后觸發到window對象上的,將window對象推入元素隊列

為什么最后要加window?


 

7 處理事件

接下來的處理邏輯,無非就是遍歷每個節點,取出對應節點上的事件句柄,並確保事件不需要阻止冒泡

i = 0;
        while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) {

            event.type = i > 1 ?
                bubbleType :
                special.bindType || type;

            // jQuery handler
            handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" );
            if ( handle ) {
                handle.apply( cur, data );
            }

            // Native handler
            handle = ontype && cur[ ontype ];
            if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) {
                event.preventDefault();
            }
        }

當然每個元素上可能有多個事件,所以先確定事件綁定類型是delegateType還是bindType

檢測緩存中該元素對應事件中包含事件處理器,有則取出主處理器(jQuery handle)來控制所有分事件處理器

所以最終代碼又走到了

handle.apply(cur, data);

其實就是交給了事件派發管理了

jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :

這時候事件就是按照dispatch的觸發規則,自行處理了,如果是瀏覽器事件就會按照dispatch處理冒泡了,自定義的就過濾了

所以jQuery的結構 是一層套一層,必須要從頭看起來知道流程

還有一部分代碼,需要在特定的環境下才會觸發的,遇到的時候在說

 


總結

所以整個trigger的核心,還是圍繞着數據緩存在處理的,通過on機制在jQuery.event.add的時候預處理好了

最終通過jQuery.event.dispatch派發

通過trigger很好的模擬了瀏覽器事件流程,但是美中不足的是對象的事件混淆其中 這就造成了 觸發對象事件的時候 最后會調用對象的相應方法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM