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

本文重點:自定義事件

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

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

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;
};
複製代碼

 

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事件,不會得到焦點。

 


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的事件名,又如何處理冒泡?


源碼解讀

附上源碼

        /**
        模擬事件觸發,爲了讓事件模型在各瀏覽器上表現一致 (並不推薦使用)
        * @param {Object} event 事件對象 (原生Event事件對象將被轉化爲jQuery.Event對象)
        * @param {Object} data 自定義傳入到事件處理函數的數據
        * @param {Object} elem HTML Element元素
        * @param {Boolen} onlyHandlers 是否不冒泡 true 表示不冒泡  false表示冒泡        
        */
        trigger: function (event, data, elem, onlyHandlers) {
            var handle, ontype, cur,
                bubbleType, special, tmp, i,
                eventPath = [elem || document],// 需要觸發事件的所有元素隊列
                type = core_hasOwn.call(event, "type") ? event.type : event,// 指定事件類型
                namespaces = core_hasOwn.call(event, "namespace") ? event.namespace.split(".") : []; // 事件是否有命名空間,有則分割成數組
 
            cur = tmp = elem = elem || document;
 
            // Don't do events on text and comment nodes
            // 對於text和comment節點不進行事件處理
            if (elem.nodeType === 3 || elem.nodeType === 8) {
                return;
            }
 
            // focus/blur morphs to focusin/out; ensure we're not firing them right now
            // 僅對focus/blur事件變種成focusin/out進行處理
            // 如果瀏覽器原生支持focusin/out,則確保當前不觸發他們
            if (rfocusMorph.test(type + jQuery.event.triggered)) {
                return;
            }
            // 如果type有命名空間
            if (type.indexOf(".") >= 0) {
                // Namespaced trigger; create a regexp to match event type in handle()
                // 則重新組裝事件
                namespaces = type.split(".");
                type = namespaces.shift();
                namespaces.sort();
            }
            // 檢測是否需要改成ontype形式 即"onclick"
            ontype = type.indexOf(":") < 0 && "on" + type;
 
            // Caller can pass in a jQuery.Event object, Object, or just an event type string            
            // jQuery.expando:檢測事件對象是否由jQuery.Event生成的實例,否則用jQuery.Event改造
            event = event[jQuery.expando] ?
                event :
                new jQuery.Event(type, typeof event === "object" && event);
            // 對event預處理
            event.isTrigger = true; //開關,表示已經使用了trigger (觸發器)
            event.namespace = namespaces.join(".");
            event.namespace_re = event.namespace ?
                new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)") :
                null;
 
            // Clean up the event in case it is being reused
            // 清除事件返回數據,以重新使用
            event.result = undefined;
            // 如果事件沒有觸發元素,則用elem代替
            if (!event.target) {
                event.target = elem;
            }
 
            // Clone any incoming data and prepend the event, creating the handler arg list
            // 如果data爲空,則傳入處理函數的是event,否則由data和event組成
            data = data == null ?
                [event] :
                jQuery.makeArray(data, [event]);
 
            // Allow special events to draw outside the lines
            // 嘗試通過特殊事件進行處理,必要時候退出函數
            special = jQuery.event.special[type] || {};
            if (!onlyHandlers && special.trigger && special.trigger.apply(elem, data) === false) {
                return;
            }
 
            // Determine event propagation path in advance, per W3C events spec (#9951)
            // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
            // 如果需要冒泡,特殊事件不需要阻止冒泡,且elem不是window對象
            // onlyHandlers爲true 表示不冒泡
            if (!onlyHandlers && !special.noBubble && !jQuery.isWindow(elem)) {
 
                // 冒泡時是否需要轉成別的事件(用於事件模擬)
                bubbleType = special.delegateType || type;
 
                // 如果不是變形來的foucusin/out事件
                if (!rfocusMorph.test(bubbleType + type)) {
                    // 則定義當前元素師父節點
                    cur = cur.parentNode;
                }
                // 遍歷自身及所有父節點
                for (; cur; cur = cur.parentNode) {
                    eventPath.push(cur);  // 推入需要觸發事件的所有元素隊列
                    tmp = cur; // 存一下循環中最後一個cur
                }
 
                // Only add window if we got to document (e.g., not plain obj or detached DOM)
                // 如果循環中最後一個cur是document,那麼事件是需要最後觸發到window對象上的
                // 將window對象推入元素隊列
                if (tmp === (elem.ownerDocument || document)) {
                    eventPath.push(tmp.defaultView || tmp.parentWindow || window);
                }
            }
 
            // Fire handlers on the event path
            // 觸發所有該事件對應元素的事件處理器
            i = 0;
            // 遍歷所有元素,並確保事件不需要阻止冒泡
            while ((cur = eventPath[i++]) && !event.isPropagationStopped()) {
 
                // 先確定事件綁定類型是delegateType還是bindType
                event.type = i > 1 ?
                    bubbleType :
                    special.bindType || type;
 
                // jQuery handler
                // 檢測緩存中該元素對應事件中包含事件處理器,
                // 有則取出主處理器(jQuery handle)來控制所有分事件處理器
                handle = (jQuery._data(cur, "events") || {})[event.type] && jQuery._data(cur, "handle");
                // 如果主處理器(jQuery handle)存在
                if (handle) {
                    // 觸發處理器
                    handle.apply(cur, data);
                }
 
                // Native handler
                // 取出原生事件處理器elem.ontype (比如click事件就是elem.onclick)              
                handle = ontype && cur[ontype];
                // 如果原生事件處理器存在,檢測需不需要阻止事件在瀏覽器上的默認動作
                if (handle && jQuery.acceptData(cur) && handle.apply && handle.apply(cur, data) === false) {
                    event.preventDefault();
                }
            }
            // 保存事件類型,因爲這時候事件可能變了
            event.type = type;
 
            // If nobody prevented the default action, do it now
            // 如果不需要阻止默認動作,立即執行
            if (!onlyHandlers && !event.isDefaultPrevented()) {
                // 嘗試通過特殊事件觸發默認動作
                if ((!special._default || special._default.apply(elem.ownerDocument, data) === false) &&
                    !(type === "click" && jQuery.nodeName(elem, "a")) && jQuery.acceptData(elem)) {
 
                    // Call a native DOM method on the target with the same name name as the event.
                    // Can't use an .isFunction() check here because IE6/7 fails that test.
                    // Don't do default actions on window, that's where global variables be (#6170)
 
                    // 調用一個原生的DOM方法具有相同名稱的名稱作爲事件的目標。
                    // 例如對於事件click,elem.click()是觸發該事件
                    // 並確保不對window對象阻止默認事件
                    if (ontype && elem[type] && !jQuery.isWindow(elem)) {
 
                        // Don't re-trigger an onFOO event when we call its FOO() method
                        // 防止我們觸發FOO()來觸發其默認動作時,onFOO事件又觸發了
                        tmp = elem[ontype];
                        // 清除掉該事件監聽
                        if (tmp) {
                            elem[ontype] = null;
                        }
 
                        // Prevent re-triggering of the same event, since we already bubbled it above
 
                        // 當我們已經將事件向上起泡時,防止相同事件再次觸發
                        jQuery.event.triggered = type;
                        try {
                            // 觸發事件
                            elem[type]();
                        } catch (e) {
                            // IE<9 dies on focus/blur to hidden element (#1486,#12518)
                            // only reproducible on winXP IE8 native, not IE9 in IE8 mode
                        }
                        // 完成清除標記
                        jQuery.event.triggered = undefined;
                        // 事件觸發完了,可以把監聽重新綁定回去
                        if (tmp) {
                            elem[ontype] = tmp;
                        }
                    }
                }
            }
 
            return event.result;
        },

 

初看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很好的模擬了瀏覽器事件流程,但是美中不足的是對象的事件混淆其中 這就造成了 觸發對象事件的時候 最後會調用對象的相應方法




發佈了56 篇原創文章 · 獲贊 11 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章