Fork me on GitHub

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

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

 本文重点:自定义事件

“通过事件机制,可以将类设计为独立的模块,通过事件对外通信,提高了程序的开发效率。”

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

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

方法调用方和被调用方被紧密耦合在一起;因为发送消息和接收消息是在一个动作内完成,所以无法做到消息的异步发送和接收;

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

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

在消息通信的灵活性方面比较有优势,但是也带来了一定的复杂度。但是复杂度一般可以由框架封装,消息的发送方和接收方仍然可以做到比较简单;

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

 


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

了解自定义事件的概念

  • 类似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很好的模拟了浏览器事件流程,但是美中不足的是对象的事件混淆其中 这就造成了 触发对象事件的时候 最后会调用对象的相应方法

posted on 2013-12-09 09:12 【艾伦】 阅读(...) 评论(...) 编辑 收藏