解密jQuery事件核心 - 自定义设计(三)

接上文http://www.cnblogs.com/aaronjs/p/3447483.htmlcss

 本文重点:自定义事件html

“经过事件机制,能够将类设计为独立的模块,经过事件对外通讯,提升了程序的开发效率。”node

对象之间经过直接方法调用来交互

1)对象A直接调用对象B的某个方法,实现交互;直接方法调用本质上也是属于一种特殊的发送与接受消息,它把发送消息和接收消息合并为一个动做完成;jquery

方法调用方和被调用方被紧密耦合在一块儿;由于发送消息和接收消息是在一个动做内完成,因此没法作到消息的异步发送和接收;ajax

2)对象A生成消息->将消息通知给一个事件消息处理器(Observable)->消息处理器经过同步或异步的方式将消息传递给接收者;api

这种方式是经过将消息发送和消息接收拆分为两个过程,经过一个中间者来控制消息是同步仍是异步发送;数组

在消息通讯的灵活性方面比较有优点,可是也带来了必定的复杂度。可是复杂度通常能够由框架封装,消息的发送方和接收方仍然能够作到比较简单;浏览器

总的来讲就是一种松耦合的处理,2个对象之间有太多紧密的直接关联,应该要考虑经过消息通讯解耦,从而提升应用程序的可维护性和重用性缓存

 


在JS中,消息的通知是经过事件表达的,当代码库增加到必定的规模,就须要考虑将行为和自定义事件进行解耦。app

了解自定义事件的概念

  • 相似DOM的行为:你在DOM节点(包括document对象)监听并触发自定义事件。这些事件既能够冒泡,也能够被拦截。这正是Prototype、jQuery和MooTools所作的。若是事件不能扩散,就必须在触发事件的对象上进行监听。
  • 命名空间:一些框架须要你为你的事件指定命名空间,一般使用一个点号前缀来把你的事件和原生事件区分开。
  • 自定义额外数据:JavaScript框架容许你在触发自定义事件时,向事件处理器传送额外的数据。jQuery能够向事件处理器传递任意数量的额外参数。
  • 通用事件API:只用Dojo保留了操做原生DOM事件的正常API。而操做自定义事件须要特殊的发布/订阅API。这也意味着Dojo中的自定义事件不具备DOM事件的一些行为(好比冒泡)。 
  • 声明:咱们每每须要在预约义的事件中加入一些特殊的变化(例如,须要Alt键按下才能触发的单击事件),MooTools运行你定义此类自定义事件。此类事件须要预先声明,即使你只是声明他们的名字。任何未声明的自定义事件不会被触发。

理论太抽象,看看jQuery框架中如何使用事件

 


案例

jQuery的事件自定义事件仍是经过on绑定的,而后再经过trigger来触发这个事件

//给element绑定hello事件
element.bind("hello",function(){
    alert("hello world!");
});
       
//触发hello事件
element.trigger("hello");

这段代码这样写彷佛感受不出它的好处,看了下面的例子也许你会明白使用自定义事件的好处了:

   

咱们已一个选项卡的插件为例:

咱们让ul列表来响应点击事件,当用户点击一个列表项时,给这个列表项添加一个名为active的类,同时将其余列表项中的active类移除,

以此同时让刚刚点击的列表对应的内容区域也添加active类。

HTML:

<ul id="tabs">
    <li data-tab="users">Users</li>
    <li data-tab="groups">Groups</li>
</ul>
<div id="tabsContent">
    <div data-tab="users">part1</div>
    <div data-tab="groups">part2</div>
</div>

 

jQuery

$.fn.tabs=function(control){
    var element=$(this);
    control=$(control);
    element.delegate("li","click",function(){
        var tabName=$(this).attr("data-tab");
         //点击li的时候触发change.tabs自定义事件 
        element.trigger("change.tabs",tabName);
    });
         
    //给element绑定一个change.tabs自定义事件
    element.bind("change.tabs",function(e,tabName){
        element.find("li").removeClass("active");
        element.find(">[data-tab='"+ tabName +"']").addClass("active");
    });    
    element.bind("change.tabs",function(e,tabName){
        control.find(">[data-tab]").removeClass("active");
        control.find(">[data-tab='"+ tabName +"']").addClass("active");
    });
    //激活第一个选项卡 
    var firstName=element.find("li:first").attr("data-tab");
    element.trigger("change.tabs",firstName);
                 
    return this;
};

 

从上面的例子咱们能够看到使用自定义事件回调使得选项卡状态切换回调彼此分离,让代码变得整洁易读。

$("ul#tabs").tabs("#tabsContent");

 


jQuery.trigger 与 document.dispatchEvent 区分

浏览器提供自定义事件接口,那么就jQuery是否是利用这个原理呢?


 

第一种状况:DOM-events使用jQuery触发。 触发不会处理经过addEventListener绑定的事件
另外一种:DOM-events触发使用本机createEvent / dispatchEvent方法与用jQuery.bind注册事件侦听器

这是一个问题,若是你与非jQuery代码混合jQuery代码。例如,jQuery移动模拟orientationchange事件基于窗口尺寸和大小事件但它使用jQuery触发orientationchange事件。 触发,所以不调用本机事件侦听器。

 


trigger的几种常见用法

1.经常使用模拟

在jQuery中,可使用trigger()方法完成模拟操做。例如可使用下面的代码来触发id为btn按钮的click事件。

$("#btn").trigger("click");

这样,当页面加载完毕后,就会马上输出想要的效果。

也能够直接用简化写法click(),来达到一样的效果:

$("#btn").click();
 

2.触发自定义事件

trigger()方法不只能触发浏览器支持的具备相同名称的事件,也能够触发自定义名称的事件。

例如为元素绑定一个“myClick”的事件,jQuery代码以下:

$("#btn").bind("myClick", function () {
    $("#test").append("<p>个人自定义事件。</p>");
});

想要触发这个事件,可使用下面的代码来实现:

$("btn").trigger("myClick");
 

3.传递数据

trigger(tpye[,datea])方法有两个参数,第一个参数是要触发的事件类型,第二个单数是要传递给事件处理函数的附加数据,以数组形式传递。一般能够经过传递一个参数给回调函数来区别此次事件是代码触发的仍是用户触发的。

下面的是一个传递数据的例子:

$("#btn").bind("myClick", function (event, message1, message2) { //获取数据
    $("#test").append("p" + message1 + message2 + "</p>");
});
$("#btn").trigger("myClick",["个人自定义","事件"]); //传递两个数据
$(“#btn”).trigger(“myClick”,["个人自定义","事件"]); //传递两个数据

 

4.执行默认操做

triger()方法触发事件后,会执行浏览器默认操做。例如:

$("input").trigger("focus");

以上代码不只会触发为input元素绑定的focus事件,也会使input元素自己获得焦点(浏览器默认操做)。

若是只想触发绑定的focus事件,而不想执行浏览器默认操做,可使用jQuery中另外一个相似的方法-triggerHandler()方法。

$("input").triggerHandler("focus");

该方法会触发input元素上绑定的特定事件,同时取消浏览器对此事件的默认操做,即文本框指触发绑定的focus事件,不会获得焦点。

 


jQuery自定义事件原理

根据API,trigger支持 .trigger()事件会在DOM树上冒泡,在事件处理程序中返回false或调用事件对象中的.stopPropagation() 方法可使事件中止冒泡

看看demo


 

 

按照tigger绑定的方式

$('ele').on('aaa',function(){})
$('ele').on('click',function(){})

第一种是自定义的事件名aaa,第二种是浏览器事件click

根据trigger的API,会处理冒泡这个关键点,

 


trigger须要处理的问题

1.模拟事件对象,用户模拟处理中止事件冒泡

这个很明了,由于不是经过浏览器系统触发的,而是自动触发的,因此这个事件对象要如何处理?

2.区分事件类型,触发标准的浏览器事件 和 自定义事件名绑定的处理程序。

例如:事件名称+命名空间

p4.on('click.aaa.ccc',function(e,vv,c){
       console.log('p4')
   })

    p4.trigger('click.aaa')

因此trigger触发的时

3.模拟冒泡机制

那么浏览器click类型,天然是自己支持冒泡这样的行为,经过stopPropagation阻止便可

固然一些事件,如focusin和 blur自己不冒泡,但 jQuery 为了跨浏览器一致性, jQuery 须要在这些事件上模拟了冒泡行为,jQuery要如何处理?

那么若是是自定义的aaa的事件名,又如何处理冒泡?


源码解读

附上源码

trigger源码

 

初看trigger源码部分,真有点晕,处理的hack太多了,可是仔细规划下,无非就是解决上面提到的几点问题

 

1 命名空间的过滤

if ( type.indexOf(".") >= 0 ) {
            // Namespaced trigger; create a regexp to match event type in handle()
            namespaces = type.split(".");
            type = namespaces.shift();
            namespaces.sort();
        }

按照规范p4.trigger('click.aaa.ccc'),'click.aaa.ccc' 就是事件+命名空间的组合

判断也挺巧妙,indexOf判断有.是索引,即存在命名空间,而后踢掉第一个事件名

 

2 模拟事件对象

event = event[ jQuery.expando ] ?
            event :
            new jQuery.Event( type, typeof event === "object" && event );

在on机制里面就分析了,其实就是jQuery.Event类了

 

4 返回的事件数据合集

data = data == null ?
            [ event ] :
            jQuery.makeArray( data, [ event ] );

因此data就是事件回调返回的[event,data],若是传递了数据就合并到data中

 

5  jQuery.event.special

这个在不少地方用到,这个是用来作模拟事件的,好比提到的模拟聚焦冒泡之类的,下章再讲

 

6 模拟事件冒泡

trigger与triggerHandler的本质区别实如今这里了

if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {

            bubbleType = special.delegateType || type;
            if ( !rfocusMorph.test( bubbleType + type ) ) {
                cur = cur.parentNode;
            }
            for ( ; cur; cur = cur.parentNode ) {
                eventPath.push( cur );
                tmp = cur;
            }

            // Only add window if we got to document (e.g., not plain obj or detached DOM)
            if ( tmp === (elem.ownerDocument || document) ) {
                eventPath.push( tmp.defaultView || tmp.parentWindow || window );
            }
        }

其实大体的手法都差很少了,无非就是遍历全部的元素节点了,排个队列出来

image

若是循环中最后一个cur是document,那么事件是须要最后触发到window对象上的,将window对象推入元素队列

为何最后要加window?


 

7 处理事件

接下来的处理逻辑,无非就是遍历每一个节点,取出对应节点上的事件句柄,并确保事件不须要阻止冒泡

i = 0;
        while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) {

            event.type = i > 1 ?
                bubbleType :
                special.bindType || type;

            // jQuery handler
            handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" );
            if ( handle ) {
                handle.apply( cur, data );
            }

            // Native handler
            handle = ontype && cur[ ontype ];
            if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) {
                event.preventDefault();
            }
        }

固然每一个元素上可能有多个事件,因此先肯定事件绑定类型是delegateType仍是bindType

检测缓存中该元素对应事件中包含事件处理器,有则取出主处理器(jQuery handle)来控制全部分事件处理器

因此最终代码又走到了

handle.apply(cur, data);

其实就是交给了事件派发管理了

jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :

这时候事件就是按照dispatch的触发规则,自行处理了,若是是浏览器事件就会按照dispatch处理冒泡了,自定义的就过滤了

因此jQuery的结构 是一层套一层,必需要从头看起来知道流程

还有一部分代码,须要在特定的环境下才会触发的,遇到的时候在说

 


总结

因此整个trigger的核心,仍是围绕着数据缓存在处理的,经过on机制在jQuery.event.add的时候预处理好了

最终经过jQuery.event.dispatch派发

经过trigger很好的模拟了浏览器事件流程,可是美中不足的是对象的事件混淆其中 这就形成了 触发对象事件的时候 最后会调用对象的相应方法

相关文章
相关标签/搜索