什么是事件驅動?
事件驅動編程是以事件為第一驅動的編程模型,模塊被動等待通知(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事件體系能夠幫我們解決哪些問題?
- 解決瀏覽器兼容性問題:觸發順序、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); };

- 如果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函數

function addListener(target, type, listener, dontFix, matchesTarget){ // event delegation: var selector = type.match(/(.*):(.*)/); // if we have a selector:event, the last one is interpreted as an event, and we use event delegation if(selector){ type = selector[2]; selector = selector[1]; // create the extension event for selectors and directly call it return on.selector(selector, type).call(matchesTarget, target, listener); } // test to see if it a touch event right now, so we don't have to do it every time it fires if(has("touch")){ if(touchEvents.test(type)){ // touch event, fix it listener = fixTouchListener(listener); } if(!has("event-orientationchange") && (type == "orientationchange")){ //"orientationchange" not supported <= Android 2.1, //but works through "resize" on window type = "resize"; target = window; listener = fixTouchListener(listener); } } if(addStopImmediate){ // add stopImmediatePropagation if it doesn't exist listener = addStopImmediate(listener); } // normal path, the target is |this| if(target.addEventListener){ // the target has addEventListener, which should be used if available (might or might not be a node, non-nodes can implement this method as well) // check for capture conversions var capture = type in captures, adjustedType = capture ? captures[type] : type; target.addEventListener(adjustedType, listener, capture); // create and return the signal return { remove: function(){ target.removeEventListener(adjustedType, listener, capture); } }; } type = "on" + type; if(fixAttach && target.attachEvent){ return fixAttach(target, type, listener); } throw new Error("Target must be an event emitter"); }
對於上面的分析我們可以得出幾個結論:
- 對於沒有特殊EventType和普通事件都用addEventListener來添加事件了。
- 而特殊EventType,則用了另一種方式來添加事件(fixAttach)。
- 對於事件委托交給了on.selector處理
- target
- currentTarget
- relatedTarget
- stopPropagation
- preventDefault
- event的坐標位置兼容放到了dom-geometry的normalizeEvent中處理
- keycode與charcode的處理

var fixAttach = function(target, type, listener){ listener = fixListener(listener); if(((target.ownerDocument ? target.ownerDocument.parentWindow : target.parentWindow || target.window || window) != top || has("jscript") < 5.8) && !has("config-_allow_leaks")){ // IE will leak memory on certain handlers in frames (IE8 and earlier) and in unattached DOM nodes for JScript 5.7 and below. // Here we use global redirection to solve the memory leaks if(typeof _dojoIEListeners_ == "undefined"){ _dojoIEListeners_ = []; } var emitter = target[type]; if(!emitter || !emitter.listeners){ var oldListener = emitter; emitter = Function('event', 'var callee = arguments.callee; for(var i = 0; i<callee.listeners.length; i++){var listener = _dojoIEListeners_[callee.listeners[i]]; if(listener){listener.call(this,event);}}'); emitter.listeners = []; target[type] = emitter; emitter.global = this; if(oldListener){ emitter.listeners.push(_dojoIEListeners_.push(oldListener) - 1); } } var handle; emitter.listeners.push(handle = (emitter.global._dojoIEListeners_.push(listener) - 1)); return new IESignal(handle); } return aspect.after(target, type, listener, true); };
關於aspect.after的具體工作原理,請看我的這篇文章:Javascript事件機制兼容性解決方案
接下來我們看一下委托的處理:
為document綁定click事件,click事件出發后,判斷event.target是否滿足選擇符“button.myclass”,若滿足則執行clickHandler。為什么要判斷event.target是否滿足選擇條件,document下可能有a、也可能有span,我們只需要將a的click委托給document,所以要判斷是否滿足選擇條件。委托過程的處理主要有兩個函數來解決:on.selector、on.matches.
- 處理matchesTarget在matches方法中使用
- 如果eventType含有bubble方法進行特殊處理
- 其他普通情況,為代理元素綁定事件回調
紅框部分就是判斷event.target是否匹配選擇符,如果匹配則觸發事件回調clickHandler.
- 獲取有效的matchesTarget,matchesTarget是一個擁有matches方法的對象,默認取dojo.query
- 對textNode做處理
- 檢查event.target的祖先元素是否滿足匹配條件

on.matches = function(node, selector, context, children, matchesTarget) { // summary: // Check if a node match the current selector within the constraint of a context // node: DOMNode // The node that originate the event // selector: String // The selector to check against // context: DOMNode // The context to search in. // children: Boolean // Indicates if children elements of the selector should be allowed. This defaults to // true // matchesTarget: Object|dojo/query? // An object with a property "matches" as a function. Default is dojo/query. // Matching DOMNodes will be done against this function // The function must return a Boolean. // It will have 3 arguments: "node", "selector" and "context" // True is expected if "node" is matching the current "selector" in the passed "context" // returns: DOMNode? // The matching node, if any. Else you get false // see if we have a valid matchesTarget or default to dojo/query matchesTarget = matchesTarget && matchesTarget.matches ? matchesTarget : dojo.query; children = children !== false; // there is a selector, so make sure it matches if(node.nodeType != 1){ // text node will fail in native match selector node = node.parentNode; } while(!matchesTarget.matches(node, selector, context)){ if(node == context || children === false || !(node = node.parentNode) || node.nodeType != 1){ // intentional assignment return false; } } return node; }
對比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模塊。
如果您覺得這篇文章對您有幫助,請不吝點擊一下右下方的推薦,謝謝!
參考文章: