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)。這些都進一步豐富完善了事件體系和事件機制,使得事件驅動式程式設計更具滲透性。
上圖為一個典型的事件驅動式模型。事件處理器事先在關注的事件源上註冊,後者不定期地發表事件物件
在web環境中事件源由DOM充當,事件管理器對於web開發者來說是透明的,由瀏覽器內部管理,事件處理器便是我們繫結在dom事件中的回撥函式。
Web事件處理流程
DOM2.0模型將事件處理流程分為三個階段:一、事件捕獲階段,二、事件目標階段,三、事件起泡階段。如圖:
事件捕獲:當某個元素觸發某個事件(如onclick),頂層物件document就會發出一個事件流,隨著DOM樹的節點向目標元素節點流去,直到到達事件真正發生的目標元素。在這個過程中,事件相應的監聽函式是不會被觸發的。
事件目標:當到達目標元素之後,執行目標元素該事件相應的處理函式。如果沒有繫結監聽函式,那就不執行。
事件起泡:從目標元素開始,往頂層元素傳播。途中如果有節點綁定了相應的事件處理函式,這些函式都會被一次觸發。如果想阻止事件起泡,可以使用e.stopPropagation()(Firefox)或者e.cancelBubble=true(IE)來組織事件的冒泡傳播。
然而在此末法時代,瀏覽器兩大派別對於事件方面的處理,常常讓前端程式設計師大傷腦筋,所以任何前端庫首先要對事件機制進行統一。
dojo中的事件繫結
dojo事件體系能夠幫我們解決哪些問題?
- 解決瀏覽器相容性問題:觸發順序、this關鍵字、規範化的事件物件(屬性、方法)
- 可以在一個事件型別上新增多個事件處理函式,可以一次新增多個事件型別的事件處理函式
- 統一了事件的封裝、繫結、執行、銷燬機制
- 支援自定義事件
- 擴充套件組合事件
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。 下面來看一下事件解析的過程:
- 如果type是方法,則交給type自身去處理;比如touch.press 、on.selector
- 多事件的處理;事件可能是通過逗號鍵分隔的字串,所以將其變成字串陣列
- 對於事件陣列依次呼叫on.parse
- 新增事件監聽器
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); };接著看一下事件監聽器的處理過程:
- 處理事件委託,dojo中事件委託的書寫格式為:“selector:eventType”,直接交給on.selector處理
- 對與touchevent事件的處理,具體分析以後再說
- 對於stopImmediatePropagation的修正
- 支援addEventListener的瀏覽器,使用瀏覽器自帶的介面進行處理
- 對於不支援addEventListener的瀏覽器進行進入fixAttach函式
對於上面的分析我們可以得出幾個結論:
- 對於沒有特殊EventType和普通事件都用addEventListener來新增事件了。
- 而特殊EventType,則用了另一種方式來新增事件(fixAttach)。
- 對於事件委託交給了on.selector處理
- target
- currentTarget
- relatedTarget
- stopPropagation
- preventDefault
- event的座標位置相容放到了dom-geometry的normalizeEvent中處理
- keycode與charcode的處理
接下來我們看一下委託的處理:
為document繫結click事件,click事件出發後,判斷event.target是否滿足選擇符“button.myclass”,若滿足則執行clickHandler。為什麼要判斷event.target是否滿足選擇條件,document下可能有a、也可能有span,我們只需要將a的click委託給document,所以要判斷是否滿足選擇條件。委託過程的處理主要有兩個函式來解決:on.selector、on.matches.
on.selector中返回一個匿名函式,匿名函式中做了幾件事:- 處理matchesTarget在matches方法中使用
- 如果eventType含有bubble方法進行特殊處理
- 其他普通情況,為代理元素繫結事件回撥
紅框部分就是判斷event.target是否匹配選擇符,如果匹配則觸發事件回撥clickHandler.
on.matches中做了以下幾件事:- 獲取有效的matchesTarget,matchesTarget是一個擁有matches方法的物件,預設取dojo.query
- 對textNode做處理
- 檢查event.target的祖先元素是否滿足匹配條件
對比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模組。