解密jQuery事件核心 对象之间通过直接方法调用来交互 了解自定义事件的概念 案例 jQuery.trigger 与 document.dispatchEvent 区分 trigger的几种常见用法 jQuery自定义事件原理 trigger需要处理的问题 源码解读 总结

接上文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 >
    <li data-tab="users">Users</li>
    <li data-tab="groups">Groups</li>
</ul>
<div >
    <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是不是利用这个原理呢?


 

按照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的事件名,又如何处理冒泡?


源码解读

附上源码

/**
        模拟事件触发,为了让事件模型在各浏览器上表现一致 (并不推荐使用)
        * @param {Object} event 事件对象 (原生Event事件对象将被转化为jQuery.Event对象)
        * @param {Object} data 自定义传入到事件处理函数的数据
        * @param {Object} elem HTML Element元素
        * @param {Boolen} onlyHandlers 是否不冒泡 true 表示不冒泡  false表示冒泡        
        */
        trigger: function (event, data, elem, onlyHandlers) {
            var handle, ontype, cur,
                bubbleType, special, tmp, i,
                eventPath = [elem || document],// 需要触发事件的所有元素队列
                type = core_hasOwn.call(event, "type") ? event.type : event,// 指定事件类型
                namespaces = core_hasOwn.call(event, "namespace") ? event.namespace.split(".") : []; // 事件是否有命名空间,有则分割成数组
 
            cur = tmp = elem = elem || document;
 
            // Don't do events on text and comment nodes
            // 对于text和comment节点不进行事件处理
            if (elem.nodeType === 3 || elem.nodeType === 8) {
                return;
            }
 
            // focus/blur morphs to focusin/out; ensure we're not firing them right now
            // 仅对focus/blur事件变种成focusin/out进行处理
            // 如果浏览器原生支持focusin/out,则确保当前不触发他们
            if (rfocusMorph.test(type + jQuery.event.triggered)) {
                return;
            }
            // 如果type有命名空间
            if (type.indexOf(".") >= 0) {
                // Namespaced trigger; create a regexp to match event type in handle()
                // 则重新组装事件
                namespaces = type.split(".");
                type = namespaces.shift();
                namespaces.sort();
            }
            // 检测是否需要改成ontype形式 即"onclick"
            ontype = type.indexOf(":") < 0 && "on" + type;
 
            // Caller can pass in a jQuery.Event object, Object, or just an event type string            
            // jQuery.expando:检测事件对象是否由jQuery.Event生成的实例,否则用jQuery.Event改造
            event = event[jQuery.expando] ?
                event :
                new jQuery.Event(type, typeof event === "object" && event);
            // 对event预处理
            event.isTrigger = true; //开关,表示已经使用了trigger (触发器)
            event.namespace = namespaces.join(".");
            event.namespace_re = event.namespace ?
                new RegExp("(^|\.)" + namespaces.join("\.(?:.*\.|)") + "(\.|$)") :
                null;
 
            // Clean up the event in case it is being reused
            // 清除事件返回数据,以重新使用
            event.result = undefined;
            // 如果事件没有触发元素,则用elem代替
            if (!event.target) {
                event.target = elem;
            }
 
            // Clone any incoming data and prepend the event, creating the handler arg list
            // 如果data为空,则传入处理函数的是event,否则由data和event组成
            data = data == null ?
                [event] :
                jQuery.makeArray(data, [event]);
 
            // Allow special events to draw outside the lines
            // 尝试通过特殊事件进行处理,必要时候退出函数
            special = jQuery.event.special[type] || {};
            if (!onlyHandlers && special.trigger && special.trigger.apply(elem, data) === false) {
                return;
            }
 
            // Determine event propagation path in advance, per W3C events spec (#9951)
            // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
            // 如果需要冒泡,特殊事件不需要阻止冒泡,且elem不是window对象
            // onlyHandlers为true 表示不冒泡
            if (!onlyHandlers && !special.noBubble && !jQuery.isWindow(elem)) {
 
                // 冒泡时是否需要转成别的事件(用于事件模拟)
                bubbleType = special.delegateType || type;
 
                // 如果不是变形来的foucusin/out事件
                if (!rfocusMorph.test(bubbleType + type)) {
                    // 则定义当前元素师父节点
                    cur = cur.parentNode;
                }
                // 遍历自身及所有父节点
                for (; cur; cur = cur.parentNode) {
                    eventPath.push(cur);  // 推入需要触发事件的所有元素队列
                    tmp = cur; // 存一下循环中最后一个cur
                }
 
                // Only add window if we got to document (e.g., not plain obj or detached DOM)
                // 如果循环中最后一个cur是document,那么事件是需要最后触发到window对象上的
                // 将window对象推入元素队列
                if (tmp === (elem.ownerDocument || document)) {
                    eventPath.push(tmp.defaultView || tmp.parentWindow || window);
                }
            }
 
            // Fire handlers on the event path
            // 触发所有该事件对应元素的事件处理器
            i = 0;
            // 遍历所有元素,并确保事件不需要阻止冒泡
            while ((cur = eventPath[i++]) && !event.isPropagationStopped()) {
 
                // 先确定事件绑定类型是delegateType还是bindType
                event.type = i > 1 ?
                    bubbleType :
                    special.bindType || type;
 
                // jQuery handler
                // 检测缓存中该元素对应事件中包含事件处理器,
                // 有则取出主处理器(jQuery handle)来控制所有分事件处理器
                handle = (jQuery._data(cur, "events") || {})[event.type] && jQuery._data(cur, "handle");
                // 如果主处理器(jQuery handle)存在
                if (handle) {
                    // 触发处理器
                    handle.apply(cur, data);
                }
 
                // Native handler
                // 取出原生事件处理器elem.ontype (比如click事件就是elem.onclick)              
                handle = ontype && cur[ontype];
                // 如果原生事件处理器存在,检测需不需要阻止事件在浏览器上的默认动作
                if (handle && jQuery.acceptData(cur) && handle.apply && handle.apply(cur, data) === false) {
                    event.preventDefault();
                }
            }
            // 保存事件类型,因为这时候事件可能变了
            event.type = type;
 
            // If nobody prevented the default action, do it now
            // 如果不需要阻止默认动作,立即执行
            if (!onlyHandlers && !event.isDefaultPrevented()) {
                // 尝试通过特殊事件触发默认动作
                if ((!special._default || special._default.apply(elem.ownerDocument, data) === false) &&
                    !(type === "click" && jQuery.nodeName(elem, "a")) && jQuery.acceptData(elem)) {
 
                    // Call a native DOM method on the target with the same name name as the event.
                    // Can't use an .isFunction() check here because IE6/7 fails that test.
                    // Don't do default actions on window, that's where global variables be (#6170)
 
                    // 调用一个原生的DOM方法具有相同名称的名称作为事件的目标。
                    // 例如对于事件click,elem.click()是触发该事件
                    // 并确保不对window对象阻止默认事件
                    if (ontype && elem[type] && !jQuery.isWindow(elem)) {
 
                        // Don't re-trigger an onFOO event when we call its FOO() method
                        // 防止我们触发FOO()来触发其默认动作时,onFOO事件又触发了
                        tmp = elem[ontype];
                        // 清除掉该事件监听
                        if (tmp) {
                            elem[ontype] = null;
                        }
 
                        // Prevent re-triggering of the same event, since we already bubbled it above
 
                        // 当我们已经将事件向上起泡时,防止相同事件再次触发
                        jQuery.event.triggered = type;
                        try {
                            // 触发事件
                            elem[type]();
                        } catch (e) {
                            // IE<9 dies on focus/blur to hidden element (#1486,#12518)
                            // only reproducible on winXP IE8 native, not IE9 in IE8 mode
                        }
                        // 完成清除标记
                        jQuery.event.triggered = undefined;
                        // 事件触发完了,可以把监听重新绑定回去
                        if (tmp) {
                            elem[ontype] = tmp;
                        }
                    }
                }
            }
 
            return event.result;
        },

初看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 );
            }
        }

其实大致的手法都差不多了,无非就是遍历所有的元素节点了,排个队列出来

解密jQuery事件核心
对象之间通过直接方法调用来交互
了解自定义事件的概念
案例
jQuery.trigger 与 document.dispatchEvent 区分
trigger的几种常见用法
jQuery自定义事件原理
trigger需要处理的问题
源码解读
总结

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