jQuery源码分析(二十一): 事件体系

前端语法/样式/布局 2016-12-16

起步

事件是js和用户交互的核心。js的事件模型也发生变化。

原生的js事件模型

DOM1事件模型

在DOM0时代,对于var ele = document.getElementById("test")它的写法可以是:

ele.onclick = function(e) {};

也可以是:

ele["onclick"] = function(e) {};

DOM0事件模型,所有的浏览器都支持。事件被触发时,会默认传入一个参数 e ,表示事件对象,通过e,我们可以获取很多有用的信息,比如点击的坐标、具体触发该事件的dom元素等等。对于同一dom节点而言,一个事件只能注册一个,后边注册的会把前面的注册覆盖。

DOM2事件模型,

DOM2事件模型相对于DOM0,如下两点:

  1. DOM2支持同一dom元素注册多个同种事件。
  2. DOM2新增了捕获和冒泡的概念。

DOM2事件通过 addEventListenerremoveEventListener 管理,当然,标准 。IE8及其以下版本浏览器,自娱自乐,搞出了对应的attachEventdetachEvent 。IE都被放弃了,有关IE的内容就忽略吧。

addEventListener

addEventListener有三个参数,分别为:"事件名称", "事件回调", "捕获/冒泡"

ele.addEventListener("click", function(e){
    alert("ok");
}, false);

事件名称省略了 on ,最后一个参数是布尔型,true代表捕获事件,false代表冒泡事件:

282221104503701.gif

意思就是说,某个元素触发了某个事件,最先得到通知的是window,然后是document,依次而入,直到真正触发事件的那个元素(目标元素)为止,这个过程就是捕获。接下来,事件会从目标元素开始起泡,再依次而出,直到window对象为止,这个过程就是冒泡。

DOM0与DOM2混用

混合使用完全没问题,DOM0模型和DOM2模型各自遵循自己的规则,互不影响。 整体上来说,依然是哪个先注册,哪个先执行,其他就没什么了。

jQuery事件绑定

前面是基础知识,jQuery对事件的绑定分别有几个API:

.bind()
.delegate()
.on()

旧版本还有 .live() jquery1.9以后好像就抛弃了它。jq需要考虑:

第一:大量的事件绑定,性能消耗,而且还需要解绑(IE会泄漏)
第二:绑定的元素必须要存在
第三: 后期生成HTML会没有事件绑定,需要重新绑定
第四: 语法过于繁杂

优化的办法,采用事件委托

事件委托

利用DOM2的事件流特性,事件都会上下或者向上传播,事件捕捉和事件冒泡。

具体来说,事件委托就是事件目标自身不处理事件,而是把处理任务委托给其父元素或者祖先元素,甚至根元素(document);

比方说给某个列表 <li> 添加绑定就不用对每个 li 一个一个绑定,可以统一到它的父元素来处理,只需绑定一次。

不管用的是(click / bind / delegate)之中哪个方法,最终都是 jQuery 底层都是调用 on 方法来完成最终的事件绑定。

jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
    "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
    "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {

    // Handle event binding
    jQuery.fn[ name ] = function( data, fn ) {
        return arguments.length > 0 ?
            this.on( name, null, data, fn ) :
            this.trigger( name );
    };
});

有参数时,用on方法进行绑定;没有参数时,则触发该事件。

其他绑定。

jQuery.fn.extend({
    hover: function( fnOver, fnOut ) {
        return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
    },

    bind: function( types, data, fn ) {
        return this.on( types, null, data, fn );
    },
    unbind: function( types, fn ) {
        return this.off( types, null, fn );
    },

    delegate: function( selector, types, data, fn ) {
        return this.on( types, selector, data, fn );
    },
    undelegate: function( selector, types, fn ) {
        // ( namespace ) or ( selector, types [, fn] )
        return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
    }
});

几种绑定

.bind()

.bind()方法用于直接附加一个事件处理程序到每个匹配元素上,处理程序附加到 jQuery 对象中当前选中的元素,所以在 .bind() 绑定事件的时候这些元素必须已经存在,很明显就是直接调用没利用委托机制。

.delegate()

为了突破单一 .bind() 方法的局限性,实现事件委托,undelegate: function( selector, types, fn ) {} 这个是可以自己指定委托对象的。

$('#element').delegate('a', 'click', function() { 
    alert("!!!");
});

jQuery 扫描文档查找(#element),并使用 click 事件和a这一CSS选择器作为参数把 alert 函数绑定到(#element)上。

任何时候只要有事件 冒泡$('#element') 上,它就查看该事件是否是 click 事件,以及该事件的目标元素是否与CCS选择器相匹配。如果两种检查的结果都为真的话,它就执行函数。

.on

.bind(), .live(), .delegate()都是通过 .on() 来实现的,.unbind(), .die(), .undelegate() 也是一样的都是通过 .off() 来实现的,提供了一种统一绑定事件的方法。

综上,如果绑定很多相同的事件,.delegate() 会提供很好的方法来提高效率。.bind() 的代价是非常大的,它会把相同的一个事件处理程序hook到所有匹配的DOM元素上。

并非所有的事件都能冒泡,如 load, change, submit, focus, blur

体系结构

jQuery事件的流程图,在绑定阶段与执行阶段:

46124d4ac60d46b1b0df2075ce0be6e4.png

以上是 jQuery 事件的整个结构流程图,右边是流程的一个简单实现,主要是用于理解,源码当然不是这么简单的,考虑代码量太多,有些机制是没有实现的,比如委托与原生事件的区分:

1. 通过 on 绑定事件,分析传递的数据,加工变成 add 能够识别的数据
2. 通过 add 把数据整理放到数据缓存中保存,通过 addEventListener 绑定事件
3. 触发事件执行 addEventListener 回调 dispatch 方法
4. 修正事件对象存在的问题,通过 fix 生成一个可写的事件对象
5. 引入 handlers 把委托和原生事件(例如"click")绑定区分对待
6. 执行数据缓存的事件回调,传入内部产生的事件对象

因为事件对象是重写生成的,所以可以内部获取到状态值,比如是否冒泡,是否阻止默认行为。 通过事件的委托机制我们可以让原本不支持冒泡的元素也同样模拟出来,比如 blur,focus。

绑定设计

在绑定的时候做了包装处理,在执行的时候有过滤器处理。事件对象都归结为 jQuery.event

on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
    // 省略参数处理的代码
    return this.each( function() {
        jQuery.event.add( this, types, fn, data, selector );
    });
}

尽管jq有自己的事件对象,但最终终究还是得通过addEventListener 将事件绑定到元素上:

20161216155117.png

  • elem: 目标元素
  • type: 事件类型,如’click’
  • eventHandle: 事件句柄,也就是事件回调处理的内容了
  • false: 冒泡

eventHandle源码

回到内部绑定的事件句柄 eventHandle ,可想而知 eventHandle 不仅仅只是只是充当一个回调函数的角色,而是一个实现了 EventListener 接口的对象。

if ( !(eventHandle = elemData.handle) ) {
    eventHandle = elemData.handle = function( e ) {
        // Discard the second event of a jQuery.event.trigger() and
        // when an event is called after a page has unloaded
        return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?
            jQuery.event.dispatch.apply( elem, arguments ) : undefined;
    };
}

可见在 eventHandle 中并没有直接处理回调函数,而是映射到 jQuery.event.dispatch 分派事件处理函数了仅仅只是传入 eventHandle.elem,arguments , 就是 body 元素 与事件对象那么这里有个问题,事件回调的句柄并没有传递过去,后面的代码如何关联?本章的一些地方可能要结合后面的 dispatch 处理才能清理,但是我们还是先看看做了那些处理。

16a08497afdc467387f84cff408a3907.png

on的绑定机制

第一步:获取数据缓存

//获取数据缓存
elemData = data_priv.get( elem );

在$.cahce缓存中获取存储的事件句柄对象,如果没就新建elemData

第二步:创建编号

if ( !handler.guid ) {
    handler.guid = jQuery.guid++;
}

为每一个事件的句柄给一个标示,添加ID的目的是 用来寻找或者删除handler,因为这个是缓存在缓存对象上的,没有直接跟元素节点发生关联。

第三步:给缓存增加事件处理句柄

if ( !(events = elemData.events) ) {
    events = elemData.events = {};
}
if ( !(eventHandle = elemData.handle) ) {
    eventHandle = elemData.handle = function( e ) {
        // Discard the second event of a jQuery.event.trigger() and
        // when an event is called after a page has unloaded
        return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?
            jQuery.event.dispatch.apply( elem, arguments ) : undefined;
    };
}

events,eventHandle 都是 elemData 缓存对象内部的,可见在elemData中有两个重要的属性:events,是jQuery内部维护的事件列队;handle,是实际绑定到elem中的事件处理函数

第四步:填充事件名与事件句柄

// 事件可能是通过空格键分隔的字符串,所以将其变成字符串数组
types = ( types || "" ).match( rnotwhite ) || [ "" ];

// 例如:'.a .b .c'.match(/\S+/g) → [".a", ".b", ".c"]
// 事件的个数
t = types.length;
while ( t-- ) {
    // 尝试取出事件的命名空间
    // 如"mouseover.a.b" → ["mouseover.a.b", "mouseover", "a.b"]
    tmp = rtypenamespace.exec( types[t] ) || [];
    // 取出事件类型,如mouseover
    type = origType = tmp[1];
    // 取出事件命名空间,如a.b,并根据"."分隔成数组
    namespaces = ( tmp[2] || "" ).split( "." ).sort();

    // There *must* be a type, no attaching namespace-only handlers
    if ( !type ) {
        continue;
    }

    // If event changes its type, use the special event handlers for the changed type
    // 事件是否会改变当前状态,如果会则使用特殊事件
    special = jQuery.event.special[ type ] || {};

    // If selector defined, determine special event api type, otherwise given type
    // 根据是否已定义selector,决定使用哪个特殊事件api,如果没有非特殊事件,则用type
    type = ( selector ? special.delegateType : special.bindType ) || type;

    // Update special based on newly reset type
    // type状态发生改变,重新定义特殊事件
    special = jQuery.event.special[ type ] || {};

    // handleObj is passed to all event handlers
    // 这里把handleObj叫做事件处理对象,扩展一些来着handleObjIn的属性
    handleObj = jQuery.extend({
        type: type,
        origType: origType,
        data: data,
        handler: handler,
        guid: handler.guid,
        selector: selector,
        needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
        namespace: namespaces.join(".")
    }, handleObjIn );

    // Init the event handler queue if we're the first
    // 初始化事件处理列队,如果是第一次使用,将执行语句
    if ( !(handlers = events[ type ]) ) {
        handlers = events[ type ] = [];
        handlers.delegateCount = 0;

        // Only use addEventListener if the special events handler returns false
        // 如果获取特殊事件监听方法失败,则使用addEventListener进行添加事件
        if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
            if ( elem.addEventListener ) {
                elem.addEventListener( type, eventHandle, false );
            }
        }
    }
    // 特殊事件使用add处理
    if ( special.add ) {
        special.add.call( elem, handleObj );
        // 设置事件处理函数的ID
        if ( !handleObj.handler.guid ) {
            handleObj.handler.guid = handler.guid;
        }
    }

    // Add to the element's handler list, delegates in front
    // 将事件处理对象推入处理列表,姑且定义为事件处理对象包
    if ( selector ) {
        handlers.splice( handlers.delegateCount++, 0, handleObj );
    } else {
        handlers.push( handleObj );
    }

    // Keep track of which events have ever been used, for event optimization
    // 表示事件曾经使用过,用于事件优化
    jQuery.event.global[ type ] = true;
}

多事件处理 如果是多事件分组的情况 $(...).bind("mouseover mouseout", fn); 事件可能是通过空格键分隔的字符串,所以将其变成字符串数组

增加命名空间处理 事件名称可以添加指定的event namespaces(命名空间) 来简化删除或触发事件。例如,"click.myPlugin.simple"为 click 事件同时定义了两个命名空间 myPluginsimple。通过上述方法绑定的 click 事件处理,可以用.off("click.myPlugin").off("click.simple")删除绑定到相应元素的Click事件处理程序,而不会干扰其他绑定在该元素上的“click(点击)” 事件。命名空间类似CSS类,因为它们是不分层次的;只需要有一个名字相匹配即可。以下划线开头的名字空间是供 jQuery 使用的。

Special Event特殊事件机制

.on() 的代码中用到了 jQuery.event.special 方法,special 是一个在处理特殊的事件相当灵活,可以指定绑定和解开钩子以及定制事件的默认行为,用这个 API 的时候可以创建自定义的事件但不仅仅是执行绑定事件处理程序时,引发这些“特殊”事件可以修改事件对象传递给事件处理程序,引发其他完全不同的事件,或者执行复杂的 setupteardown 代码当事件处理程序绑定到或未绑定元素。

某些事件类型的有特殊行为和属性,换句话说就是某些事件不是大众化的事件不能一概处理。

比如 load 事件拥有特殊的 noBubble 属性,可以防止该事件的冒泡而引发一些错误,所以需要单独针的处理.

special: {
    load: {},
    focus: {},
    blur: {},
    click: {},
    beforeunload: {}
},

jQuery.event.dispatch.apply( eventHandle.elem, arguments ) 方法中没有传递回调对象是因为回调的句柄被关联到了 elemData,也就是内部数据缓存中了,不难得出 jQuery 的事件绑定机制:jQuery 对每一个 elem 中的每一种事件,只会绑定一次事件处理函数(绑定这个elemData.handle),而这个 elemData.handle 实际只做一件事,就是把 event 丢到 jQuery 内部的事件分发程序。

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

重写事件对象

jQuery对事件的对象的兼容问题单独抽象出一个 fix 类,用来重写这个事件对象,jQuery.event.fix() 来解决跨浏览器的兼容性问题,统一接口。

除该核心方法外,我们要根据事件的类型,统一接口的获取,所以 jQuery 引入了 (jQuery.event) props、 fixHooks、keyHooks、mouseHooks 等数据模块。

  • props 存储了原生事件对象 event 的通用属性
  • keyHook.props 存储键盘事件的特有属性
  • mouseHooks.props 存储鼠标事件的特有属性。
  • keyHooks.filtermouseHooks.filter 两个方法分别用于修改键盘和鼠标事件的属性兼容性问题,用于统一接口。

比如 event.which 通过 event.charCode 或 event.keyCode 或 event.button 来标准化。

最后 fixHooks 对象用于缓存不同事件所属的事件类别,比如:

fixHooks['click'] === jQuery.event.mouseHooks;
fixHooks['keydown'] === jQuery.event.keyHooks;
fixHooks['focusin'] === {};

从源码处获取对事件对象的操作,通过调用 jQuery.Event 重写事件对象,将浏览器原生 Event 的属性赋值到新创建的 jQuery.Event 对象中去。

重写事件对象初始化是 fixHooks: {} ,添加扩展为:

fixHook = this.fixHooks[ type ];

if ( !fixHook ) {
    this.fixHooks[ type ] = fixHook =
        rmouseEvent.test( type ) ? this.mouseHooks :
        rkeyEvent.test( type ) ? this.keyHooks :
        {};
}

有一些属性是共用的,都存在,所以单独拿出来就好了。

props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),

然后把私有的与公共的拼接一下。

copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;

然后混入到这个新的对象上。

jQuery 自己写了一个基于 native event 的 Event 对象,并且把 copy 数组中对应的属性从 native event 中复制到自己的 Event 对象中。

while ( i-- ) {
    prop = copy[ i ];
    event[ prop ] = originalEvent[ prop ];
}

最后 jQuery 还不忘放一个钩子,调用 fixHook.fitler 方法用以纠正一些特定的 event 属性。例如 mouse event 中的 pageX,pageY,keyboard event中的 which,进一步修正事件对象属性的兼容问题。

fixHook.filter? fixHook.filter( event, originalEvent ) : event

总的来说 jQuery.event.fix 做的事情:

1.将原生的事件对象 event 修正为一个新的可写 event 对象,并对该 event 的属性以及方法统一接口
2.该方法在内部调用了 jQuery.Event(event) 构造函数

自定义事件

jQuery的事件自定义事件还是通过on绑定的,然后再通过trigger来触发这个事件:

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

//触发hello事件
element.trigger("hello");

trigger需要处理的问题:

  1. 模拟事件对象,用户模拟处理停止事件冒泡(因为不是通过浏览器系统触发的,而是自动触发的,所以这个事件对象要如何处理?)
  2. 区分事件类型,触发标准的浏览器事件 和 自定义事件名绑定的处理程序。

拟冒泡机制

  1. 当事件是 click 类型,自然是本身支持冒泡这样的行为,通过 stopPropagation 阻止即可
  2. 当然一些事件,如 focusin 和 blur 本身不冒泡,但 jQuery 为了跨浏览器一致性, jQuery 需要在这些事件上模拟了冒泡行为,jQuery 要如何处理?
  3. 那么如果是自定义的aaa的事件名,又如何处理冒泡?

事件模拟

js如要手动触发点击事件,$.(ele).click() 可以,它其实是.trigger('click') 的简写,

trigger: function( type, data ) {
    return this.each(function() {
        jQuery.event.trigger( type, data, this );
    });
},

trigger 与 dispatch 方法的区别 简单的来说,jQuery 的事件应用从更抽象的一层去理解它的元素层次划分其实是非常清晰的,首先每一个元素都可以绑定事件与冒泡,那么这个针对每个层的单独元素的处理是划分给了 dispatch 方法。在 dispatch 方法中我们通过 targetcurrentTarget(绑定事件的元素)生成一条冒泡线,依次往父层元素遍历取出每一个层级元素对应的数据相应的执行,由于在这个模拟冒泡的操作过程中,jQuery 模拟出的事件对应被所有的这些操作共享,所以在任何一个元素的事件处理中调用了停止冒泡,那么这个循环就停止了,也就达到了 stopPropagation 的目的,这里我们要注意事件的冒泡是在绑定事件元素内部发生的。

原生事件提供了一个最重要参数 - 事件对象,trigger 是模拟触发,所以我们需要模拟一个这样的数据对象,其次 trigger 也要支持冒泡,但是这里有一个区别 dispatch 的地方,trigger 冒泡的 target 的对象是确定的,所以 target 就是自己本身,所以冒泡的路径其实是一个从自己本身到 window 的一条外部路线。

总结

所以整个 trigger 的核心,还是围绕着数据缓存在处理的,通过 on 机制在 jQuery.event.add 的时候预处理好了。trigger 的处理就是模拟冒泡的一个调度,具体的触发还是交给 jQuery.event.dispatch 方法了,通过 trigger 很好的模拟了浏览器事件流程,但是美中不足的是对象的事件混淆其中,这就造成了触发对象事件的时候最后会调用对象的相应方法。

jQuery为了实现兼容统一,可谓煞费苦心了,把事件冒泡与捕获都统一模拟了一遍


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!