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 源码分析 事件体系结构

相关文章
相关标签/搜索