dojo事件驅動編程之事件綁定

  什麼是事件驅動?css

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

  何謂事件?通俗地說,它是已經發生的某種使人關注的事情。在軟件中,它通常表現爲一個程序的某些信息狀態上的變化。基於事件驅動的系統通常提供兩類的內建事件(built-in event):一類是底層事件(low-level event)或稱原生事件(native event),在用戶圖形界面(GUI)系統中這類事件直接由鼠標、鍵盤等硬件設備觸發;一類是語義事件(semantic event),通常表明用戶的行爲邏輯,是若干底層事件的組合。好比鼠標拖放(drag-and-drop)多表示移動被拖放的對象,由鼠標按下、鼠標移動和鼠標釋放三個底層事件組成。前端

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

  

  上圖爲一個典型的事件驅動式模型。事件處理器事先在關注的事件源上註冊,後者不按期地發表事件對象,通過事件管理器的轉化(translate)、合併(coalesce)、排隊(enqueue)、分派(dispatch)等集中處理後,事件處理器接收到事件並對其進行相應處理。經過事件機制,事件源與事件處理器之間創建了鬆耦合的多對多關係:一個事件源能夠有多個處理器,一個處理器能夠監聽多個事件源。再換個角度,把事件處理器視爲服務方,事件源視爲客戶方,即是一個client-server模式。每一個服務方與其客戶方之間的會話(session)是異步的,即在處理完一個客戶的請求後沒必要等待下一請求,隨時可切換(switch)到對其餘客戶的服務。jquery

  在web環境中事件源由DOM充當,事件管理器對於web開發者來講是透明的,由瀏覽器內部管理,事件處理器即是咱們綁定在dom事件中的回調函數。程序員

 

  Web事件處理流程web

  DOM2.0模型將事件處理流程分爲三個階段:1、事件捕獲階段,2、事件目標階段,3、事件起泡階段。如圖:編程

  

  

  事件捕獲:當某個元素觸發某個事件(如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函數
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");
    }
View Code

  對於上面的分析咱們能夠得出幾個結論:

  • 對於沒有特殊EventType和普通事件都用addEventListener來添加事件了。
  • 而特殊EventType,則用了另外一種方式來添加事件(fixAttach)。
  • 對於事件委託交給了on.selector處理

  

  來詳細的看一下fixAttach:
  一、修正事件監聽器,該過程返回一個閉包,閉包中對event對象進行修正,主要有一下幾方面:
  • target
  • currentTarget
  • relatedTarget
  • stopPropagation
  • preventDefault
  • event的座標位置兼容放到了dom-geometry的normalizeEvent中處理
  • keycode與charcode的處理
        調用on中傳入的事件監聽器,若是監聽器中掉用過stopImmediatePropagation緩存lastEvent,供之後使用
 
  二、對於低版本瀏覽器防止在frames和爲連接到DOM樹中元素添加事件時引發的內存泄露,這裏自定義一個Event對象,將全部的事件監聽器做爲屬性添加到這個Event對象上。
  三、不在2條件中的狀況使用aspect.after構造一個函數鏈來存放事件監聽器,這就保證了監聽器的調用順序與添加順序一致。
  
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);
        };
View Code

  關於aspect.after的具體工做原理,請看個人這篇文章:Javascript事件機制兼容性解決方案

  

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

  

  爲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的祖先元素是否知足匹配條件
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;
    }
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模塊。

  若是您以爲這篇文章對您有幫助,請不吝點擊一下右下方的推薦,謝謝!

  參考文章:

  冒號課堂§3.4:事件驅動 

  jQuery 2.0.3 源碼分析 事件體系結構

相關文章
相關標籤/搜索