1. 程式人生 > >dojo事件驅動程式設計之事件繫結

dojo事件驅動程式設計之事件繫結

什麼是事件驅動?

  事件驅動程式設計是以事件為第一驅動的程式設計模型,模組被動等待通知(notification),行為取決於外來的突發事件,是事件驅動的,符合事件驅動式程式設計(Event-Driven Programming,簡稱EDP)的模式。

  何謂事件?通俗地說,它是已經發生的某種令人關注的事情。在軟體中,它一般表現為一個程式的某些資訊狀態上的變化。基於事件驅動的系統一般提供兩類的內建事件(built-in event):一類是底層事件(low-level event)或稱原生事件(native event),在使用者圖形介面(GUI)系統中這類事件直接由滑鼠、鍵盤等硬體裝置觸發;一類是語義事件(semantic event),一般代表使用者的行為邏輯,是若干底層事件的組合。比如滑鼠拖放(drag-and-drop)多表示移動被拖放的物件,由滑鼠按下、滑鼠移動和滑鼠釋放三個底層事件組成。

  還有一類使用者自定義事件(user-defined event)。它們可以是在原有的內建事件的基礎上進行的包裝,也可以是純粹的虛擬事件(virtual event)。除此之外,程式設計者不但能定義事件,還能產生事件。雖然大部分事件是由外界激發的自然事件(natural event),但有時程式設計師需要主動激發一些事件,比如模擬使用者滑鼠點選或鍵盤輸入等,這類事件被稱為合成事件(synthetic event)。這些都進一步豐富完善了事件體系和事件機制,使得事件驅動式程式設計更具滲透性。

  

  上圖為一個典型的事件驅動式模型。事件處理器事先在關注的事件源上註冊,後者不定期地發表事件物件

,經過事件管理器的轉化(translate)、合併(coalesce)、排隊(enqueue)、分派(dispatch)等集中處理後,事件處理器接收到事件並對其進行相應處理。通過事件機制,事件源與事件處理器之間建立了鬆耦合的多對多關係:一個事件源可以有多個處理器,一個處理器可以監聽多個事件源。再換個角度,把事件處理器視為服務方,事件源視為客戶方,便是一個client-server模式。每個服務方與其客戶方之間的會話(session)是非同步的,即在處理完一個客戶的請求後不必等待下一請求,隨時可切換(switch)到對其他客戶的服務。

  在web環境中事件源由DOM充當,事件管理器對於web開發者來說是透明的,由瀏覽器內部管理,事件處理器便是我們繫結在dom事件中的回撥函式。

  Web事件處理流程

  DOM2.0模型將事件處理流程分為三個階段:一、事件捕獲階段,二、事件目標階段,三、事件起泡階段。如圖:

  

  事件捕獲:當某個元素觸發某個事件(如onclick),頂層物件document就會發出一個事件流,隨著DOM樹的節點向目標元素節點流去,直到到達事件真正發生的目標元素。在這個過程中,事件相應的監聽函式是不會被觸發的。

  事件目標:當到達目標元素之後,執行目標元素該事件相應的處理函式。如果沒有繫結監聽函式,那就不執行。

  事件起泡:從目標元素開始,往頂層元素傳播。途中如果有節點綁定了相應的事件處理函式,這些函式都會被一次觸發。如果想阻止事件起泡,可以使用e.stopPropagation()(Firefox)或者e.cancelBubble=true(IE)來組織事件的冒泡傳播。

  然而在此末法時代,瀏覽器兩大派別對於事件方面的處理,常常讓前端程式設計師大傷腦筋,所以任何前端庫首先要對事件機制進行統一。

  dojo中的事件繫結

  dojo事件體系能夠幫我們解決哪些問題?

  1. 解決瀏覽器相容性問題:觸發順序、this關鍵字、規範化的事件物件(屬性、方法)
  2. 可以在一個事件型別上新增多個事件處理函式,可以一次新增多個事件型別的事件處理函式
  3. 統一了事件的封裝、繫結、執行、銷燬機制
  4. 支援自定義事件
  5. 擴充套件組合事件

   dojo中處理瀏覽器事件的程式碼位於dojo/on模組中,在官網中可以檢視該函式的簽名:

  

  其中type可以是一個事件名稱如:“click”

複製程式碼
require(["dojo/on", "dojo/_base/window"], function(on, win){
  var signal = on(win.doc, "click", function(){
    // remove listener after first event
    signal.remove();
    // do something else...
  });
});
複製程式碼

  亦可以是由逗號分隔的多個事件名組成的字串,如:"dblclick,click"

require("dojo/on", function(on){
  on(element, "dblclick, touchend", function(e){
    // handle either event
  });
});

  亦可以是由冒號分隔"selector:eventType"格式進行事件委託使用的字串,如:".myClass:click"

require(["dojo/on", "dojo/_base/window", "dojo/query"], function(on, win){
  on(win.doc, ".myClass:click", clickHandler);
});

  亦可以是一個函式,如:touch.press、on.selector()

require(["dojo/on", "dojo/mouse", "dojo/query!css2"], function(on, mouse){
  on(node, on.selector(".myClass", mouse.enter), myClassHoverHandler);
});

  檢視一下on函式的原始碼

複製程式碼
var on = function(target, type, listener, dontFix){

        if(typeof target.on == "function" && typeof type != "function" && !target.nodeType){
            // delegate to the target's on() method, so it can handle it's own listening if it wants (unless it 
            // is DOM node and we may be dealing with jQuery or Prototype's incompatible addition to the
            // Element prototype 
            return target.on(type, listener);
        }
        // delegate to main listener code
        return on.parse(target, type, listener, addListener, dontFix, this);
    };
複製程式碼   如果target自己擁有on方法則呼叫target自己的on方法,如_WidgetBase類有自己的on方法,再比如jquery物件也會有自己的on方法,此處this關鍵字指向window。      下面來看一下事件解析的過程:
  1. 如果type是方法,則交給type自身去處理;比如touch.press 、on.selector
  2. 多事件的處理;事件可能是通過逗號鍵分隔的字串,所以將其變成字串陣列
  3. 對於事件陣列依次呼叫on.parse
  4. 新增事件監聽器
複製程式碼
on.parse = function(target, type, listener, addListener, dontFix, matchesTarget){
        if(type.call){
            // event handler function
            // on(node, touch.press, touchListener);
            return type.call(matchesTarget, target, listener);
        }

        if(type instanceof Array){
            // allow an array of event names (or event handler functions)
            events = type;
        }else if(type.indexOf(",") > -1){
            // we allow comma delimited event names, so you can register for multiple events at once
            var events = type.split(/\s*,\s*/);
        } 
        if(events){
            var handles = [];
            var i = 0;
            var eventName;
            while(eventName = events[i++]){
                handles.push(on.parse(target, eventName, listener, addListener, dontFix, matchesTarget));
            }
            handles.remove = function(){
                for(var i = 0; i < handles.length; i++){
                    handles[i].remove();
                }
            };
            return handles;
        }
        return addListener(target, type, listener, dontFix, matchesTarget);
    };
複製程式碼   接著看一下事件監聽器的處理過程:
  1. 處理事件委託,dojo中事件委託的書寫格式為:“selector:eventType”,直接交給on.selector處理
  2. 對與touchevent事件的處理,具體分析以後再說
  3. 對於stopImmediatePropagation的修正
  4. 支援addEventListener的瀏覽器,使用瀏覽器自帶的介面進行處理
  5. 對於不支援addEventListener的瀏覽器進行進入fixAttach函式
View Code

  對於上面的分析我們可以得出幾個結論:

  • 對於沒有特殊EventType和普通事件都用addEventListener來新增事件了。
  • 而特殊EventType,則用了另一種方式來新增事件(fixAttach)。
  • 對於事件委託交給了on.selector處理
  來詳細的看一下fixAttach:   1、修正事件監聽器,該過程返回一個閉包,閉包中對event物件進行修正,主要有一下幾方面:
  • target
  • currentTarget
  • relatedTarget
  • stopPropagation
  • preventDefault
  • event的座標位置相容放到了dom-geometry的normalizeEvent中處理
  • keycode與charcode的處理
        呼叫on中傳入的事件監聽器,如果監聽器中掉用過stopImmediatePropagation快取lastEvent,供以後使用   2、對於低版本瀏覽器防止在frames和為連結到DOM樹中元素新增事件時引起的記憶體洩露,這裡自定義一個Event物件,將所有的事件監聽器作為屬性新增到這個Event物件上。   3、不在2條件中的情況使用aspect.after構造一個函式鏈來存放事件監聽器,這就保證了監聽器的呼叫順序與新增順序一致。    View Code

  接下來我們看一下委託的處理:

  

  為document繫結click事件,click事件出發後,判斷event.target是否滿足選擇符“button.myclass”,若滿足則執行clickHandler。為什麼要判斷event.target是否滿足選擇條件,document下可能有a、也可能有span,我們只需要將a的click委託給document,所以要判斷是否滿足選擇條件。委託過程的處理主要有兩個函式來解決:on.selector、on.matches.

  on.selector中返回一個匿名函式,匿名函式中做了幾件事:
  1. 處理matchesTarget在matches方法中使用
  2. 如果eventType含有bubble方法進行特殊處理
  3. 其他普通情況,為代理元素繫結事件回撥

  

  紅框部分就是判斷event.target是否匹配選擇符,如果匹配則觸發事件回撥clickHandler.

  on.matches中做了以下幾件事:
  1. 獲取有效的matchesTarget,matchesTarget是一個擁有matches方法的物件,預設取dojo.query
  2. 對textNode做處理
  3. 檢查event.target的祖先元素是否滿足匹配條件
View Code

  對比dojo與jquery的事件處理過程,可以發現jQuery在事件儲存上更上一籌:

  dojo直接繫結到dom元素上,jQuery並沒有將事件處理函式直接繫結到DOM元素上,而是通過.data儲存在快取.cahce上。

   宣告繫結的時候:

  • 首先為DOM元素分配一個唯一ID,繫結的事件儲存在 .cahce[唯一ID][.expand ][ 'events' ]上,而events是個鍵-值對映物件,鍵就是事件型別,對應的值就是由事件處理函式組成的陣列,最後在DOM元素上繫結(addEventListener/attachEvent)一個事件處理函式eventHandle,這個過程由 jQuery.event.add 實現。

  執行繫結的時候:

  • 當事件觸發時eventHandle被執行,eventHandle再去$.cache中尋找曾經繫結的事件處理函式並執行,這個過程由 jQuery.event. trigger 和 jQuery.event.handle實現。
  • 事件的銷燬則由jQuery.event.remove 實現,remove對快取$.cahce中儲存的事件陣列進行銷燬,當快取中的事件全部銷燬時,呼叫removeEventListener/ detachEvent銷燬繫結在DOM元素上的事件處理函式eventHandle。

  以上就是dojo事件模組的主要內容,如果結合Javascript事件機制相容性解決方案來看的話,更有助於理解dojo/on模組。