jQuery源码学习(12)-事件绑定(1)
1、javaScript传统的事件处理
给某一个元素绑定了一个点击事件,传入一个回调句柄处理:
element.addEventListener('click',doSomething,false);
但是如果页面上有几百个元素需要绑定,那需要绑定几百次,这样就可以知道传统绑定方法存在的问题:
- 大量的事件绑定,性能消耗,而且还需要解绑(IE会泄漏);
- 绑定的元素必须要存在;
- 后期生成的HTML会没有事件绑定,需要重新绑定;
- 语法过于繁杂。
关于jQuery提出的事件绑定的一些方法:bind()、live()、on()、delegate()。对应的解除事件绑定的函数分别是:unbind()、die()、off()、undelegate()。其中,live和delegate方法利用了事件委托机制,很好的解决了大量事件绑定的问题,所以先介绍事件委托的原理。
2、事件委托
DOM有个事件流的特性,也就是说我们在页面上触发节点的时候事件都会上下或者向上传播,事件捕捉和事件冒泡。
DOM2.0模型将事件处理流程分为三个阶段:一、事件捕获阶段,二、事件目标阶段,三、事件起泡阶段。
模型如图所示:
事件传送可以分为3个阶段。
(1).在事件捕捉(Capturing)阶段,事件将沿着DOM树向下转送,目标节点的每一个祖先节点,直至目标节点。例如,若用户单击了一个超链接,则该单击事件将从document节点转送到html元素,body元素以及包含该链接的p元素。在此过程中,浏览器都会检测针对该事件的捕捉事件监听器,并且运行这件事件监听器。
(2)在目标(target)阶段,浏览器在查找到已经指定给目标事件的事件监听器之后,就会运行 该事件监听器。目标节点就是触发事件的DOM节点。例如,如果用户单击一个超链接,那么该链接就是目标节点(此时的目标节点实际上是超链接内的文本节点)。
(3).在冒泡(Bubbling)阶段,事件将沿着DOM树向上转送,再次逐个访问目标元素的祖先节点到document节点。该过程中的每一步。浏览器都将检测那些不是捕捉事件监听器的事件监听器,并执行它们。
利用事件传播(这里是冒泡)这个机制,就可以实现事件委托。
具体来说,事件委托就是事件目标自身不处理事件,而是把处理任务委托给其父元素或者祖先元素,甚至根元素(document)举例子:
<ul id="myLinks">
<li id="goSomeWhere">Go somewhere</li>
<li id="doSomeThing">Do someThing</li>
<li id="sayHi">Say Hi</li>
</ul>
以上代码包含三个点击后会执行操作的列表项。按照传统的JS做法,我们需要像下面这样为他们添加3个事件处理程序:
var item1=document.getElementById("goSomeWhere");
var item2=document.getElementById("doSomeThing");
var item3=document.getElementById("sayHi");
EvevtUtil.addHandler(item1,"click",function(event){
location.href = "http://www.xx.com";
});
EvevtUtil.addHandler(item2,"click",function(event){
document.title = "I change the document";
});
EvevtUtil.addHandler(item3,"click",function(event){
alert("hi");
});
但是,使用事件委托,只需在DOM树中尽量高的层次上添加一个事件处理程序:
var list = document.getElementById("myLinks");
EventUtil.addHandler(list,"click",function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
switch(target.id){
case: "doSomeThing"
document.title="I change the document";
break;
case: "goSomeWhere"
location.href="http://www.xx.com";
break;
case: "sayHi"
alert("Hi");
break;
}
});
使用事件委托只为<ul>元素添加了一个onclick事件处理程序,事件目标是被单击的列表项,他们会冒泡至父节点通过检测ID属性来执行对应的操作。3、方法解析
一、bind(type,[data],function(eventObject))
bind()的作用就是在选择到的元素上绑定特定事件类型的事件处理程序,参数含义如下:
type:事件类型,如click、change、mouseover等;
data:传入事件处理函数的参数,通过event.data取到,可选;
function:事件处理函数,可传入event对象,但是这里的event是jQuery封装的event对象,与原生的event有区别。
bind的源码:
bind: function(types,data,fn){
return this.on(types,null,data,fn);
}
//使用方式
$("#myol li").bind('click',getHtml);
bind的特点就是直接附加一个事件处理程序到元素上,有一个绑一个,在页面的元素不会动态添加的时候使用它没什么,但是如果在列表中动态增加一个列表元素li,点击它是没有反应的,必须再bind一次。即,在bind绑定事件的时候,这些元素必须已经存在。为了解决这个问题,可以使用live方法。
二、live(type,[data],fn) (已被弃用)
live参数与bind一样,源码如下:
live: function(types,data,fn){
jQuery(this.context).on(types,this.selector,data,fn);
return this;
}
//live方法并没有将事件处理函数绑定到自己(this)身上,而是绑定到了this.context上了,即元素的限定范围。一般元素的范围都是document。
所以live函数将委托的事件处理程序附加到一个页面的document
元素,从而简化了在页面上动态添加的内容上事件处理的使用。
例如:
$('a').live('click',function(){alert("!!")});
JQuery把alert函数绑定到$(document)元素上,并使用’click’和’a’作为参数。任何时候只要有事件冒泡到document节点上,它就查看该事件是否是一个click事件,以及该事件的目标元素与’a’这一CSS选择器是否匹配,如果都是的话,则执行函数。但是使用live方法还存在以下问题:
- 在调用
.live()
方法之前,jQuery 会先获取与指定的选择器匹配的元素,这一点对于大型文档来说是很花费时间的。 - 不支持链式写法。例如,
$("a").find(".offsite, .external").live( ... );
这样的写法是不合法的,并不能像期待的那样起作用。 - 由于所有的
.live()
事件被添加到document
元素上,所以在事件被处理之前,可能会通过最长最慢的那条路径之后才能被触发。 - 在移动 iOS (iPhone, iPad 和 iPod Touch) 上,对于大多数元素而言,
click
事件不会冒泡到文档 body 上,并且如果不满足如下情况之一,就不能和.live()
方法一起使用:- 使用原生的可被点击的元素,例如,
a
或button
,因为这两个元素可以冒泡到document
。 - 在
document.body
内的元素使用.on()
或.delegate()
进行绑定,因为移动 iOS 只有在 body 内才能进行冒泡。 - 需要 click 冒泡到元素上才能应用的 CSS 样式
cursor:pointer
(或者是父元素包含document.documentElement
)。但是依需注意的是,这样会禁止元素上的复制/粘贴功能,并且当点击元素时,会导致该元素被高亮显示。
- 使用原生的可被点击的元素,例如,
- 在事件处理中调用
event.stopPropagation()
来阻止事件处理被添加到 document 之后的节点中,是效率很低的。因为事件已经被传播到document
上。 -
.live()
方法与其它事件方法的相互影响是会令人感到惊讶的。例如,$(document).unbind("click")
会移除所有通过.live()
添加的 click 事件!
三、delegate()
为了解决live存在的上述问题,jQuery引入了一个新方法delegate(),其把处理程序绑定到具体的元素而非document这一根上。源码:
delegate: function(selector,types,data,fn){
return this.on(types,selector,data,fn);
}
参数多了一个selector,用来指定触发事件的目标元素。
例如:
$('#element').delegate('a','click',function(){
alert("!!!");
});
使用click事件和'a'这一css选择器作为参数把alert函数绑定到了元素'#element'上。任何时候只要有事件冒泡到$(‘#element)上,它就查看该事件是否是click事件,以及该事件的目标元素是否与CCS选择器相匹配。如果两种检查的结果都为真的话,它就执行函数。四、.on( events [, selector ] [, data ], handler(eventObject) )
events:事件名
selector : 一个选择器字符串,用于过滤出被选中的元素中能触发事件的后代元素
data :当一个事件被触发时,要传递给事件处理函数的
handler:事件被触发时,执行的函数
所有delegate(),live(),bind()方法内部都是调用的on方法。
undelegate(),unlive(),unbind()方法内部都是调用的off方法。
on方法源码解析:
//on方法实质只完成一些参数调整的工作,而实际负责事件绑定的是其内部jQuery.event.add方法。
on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
var type, origFn;
// Types can be a map of types/handlers
if ( typeof types === "object" ) {
// ( types-Object, selector, data )
if ( typeof selector !== "string" ) {
// ( types-Object, data )
data = data || selector;
selector = undefined;
}
// 遍历types对象,针对每一个属性绑定on()方法
// 将types[type]作为fn传入
for ( type in types ) {
this.on( type, selector, data, types[ type ], one );
}
return this;
}
// 参数修正
// jQuery这种参数修正的方法很好
// 可以兼容多种参数形式
// 可见在灵活调用的背后做了很多处理
if ( data == null && fn == null ) {
// ( types, fn )
fn = selector;
data = selector = undefined;
} else if ( fn == null ) {
if ( typeof selector === "string" ) {
// ( types, selector, fn )
fn = data;
data = undefined;
} else {
// ( types, data, fn )
fn = data;
data = selector;
selector = undefined;
}
}
if ( fn === false ) {
// fn传入false时,阻止该事件的默认行为
// function returnFalse() {return false;}
fn = returnFalse;
} else if ( !fn ) {
return this;
}
// one()调用on()
if ( one === 1 ) {
origFn = fn;
fn = function( event ) {
// Can use an empty set, since event contains the info
// 用一个空jQuery对象,这样可以使用.off方法,
// 并且event带有remove事件需要的信息
jQuery().off( event );
return origFn.apply( this, arguments );
};
// Use same guid so caller can remove using origFn
// 事件删除依赖于guid
fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
}
// 这里调用jQuery的each方法遍历调用on()方法的jQuery对象
// 如$('li').on(...)则遍历每一个li传入add()
// 推荐使用$(document).on()或者集合元素的父元素
return this.each( function() {
jQuery.event.add( this, types, fn, data, selector );
});
},
例如:
var body = $('body')
body.on('click','p',function(){
console.log(this)
})
用on方法给body上绑定一个click事件,冒泡到p元素的时候才出发回调函数
这里大家需要明确一点:每次在body上点击其实都会触发事件,但是只目标为p元素的情况下才会触发回调handler
关于上述四种方法的总结:
在下列情况下,应该使用.live()或.delegate(),而不能使用.bind():
- 为DOM中的很多元素绑定相同事件;
- 为DOM中尚不存在的元素绑定事件;
用.bind()的代价是非常大的,它会把相同的一个事件处理程序hook到所有匹配的DOM元素上
不要再用.live()了,它已经不再被推荐了,而且还有许多问题
.delegate()会提供很好的方法来提高效率,同时我们可以添加一事件处理方法到动态添加的元素上
我们可以用.on()来代替上述的3种方法
不足点也是有的:
- 并非所有的事件都能冒泡,如load, change, submit, focus, blur
- 加大管理复杂。
- 不好模拟用户触发事件
4、事件体系结构
4.1 整个事件的API有:
4.2 事件结构
所有的函数添加事件都会进入jQuery.event.add函数。该函数有两个主要功能:添加事件、附加很多事件相关信息。下章讲解源码。
用实例来说明jQuery的事件结构:
<div id="#center"></div>
<script>
function dohander(){console.log("dohander")};
function dot(){console.log("dot");}
$(document).on("click",'#center',dohander)
.on("click",'#center',dot)
.on("click",dot);
</script>
经过添加处理环节,事件添加到了元素上,而且节点对应的缓存数据也添加了相应的数据。结构如下:
elemData = jQuery._data( elem );
elemData = {
events: {
click: {//Array[3]
0: {
data: undefined/{...},
guid: 2, //处理函数的id
handler: function dohander(){…},
namespace: "",
needsContext: false,
origType: "click",
selector: "#center",//选择器,用来区分不同事件源
type: "click"
}
1: {
data: undefined/{...},
guid: 3,
handler: function dot(){…},
namespace: "",
needsContext: false,
origType: "click",
selector: "#center",
type: "click"
}
2: {
data: undefined,
guid: 3,
handler: function dot(){…},
namespace: "",
needsContext: false,
origType: "click",
selector: undefined,
type: "click"
}
delegateCount: 2,//委托事件数量,有selector的才是委托事件
length: 3
}
}
handle: function ( e ) {…}/*事件处理主入口*/{
elem: document//属于handle对象的特征
}
}
缓存结构特点:每一个函数添加guid;使用events对象存放响应事件列表,有一个总的事件处理入口handle等。
4.3 bind、delegate、on在上面已经介绍过了,下面介绍其他API。
one():
通过one()
函数绑定的事件处理函数都是一次性的,只有首次触发事件时会执行该事件处理函数。触发之后,jQuery就会移除当前事件绑定。
比如$("#chua").one("click",fn);为#chua节点绑定一次性的click事件
$(document).one("click","#chua",fn);将#chua的click事件委托给document处理。
源码:
one: function(types, selector, data, fn) {
return this.on(types, selector, data, fn, 1);
},
内部也是通过调用on方法来实现。
trigger()和triggerHandler():
trigger触发jQuery对象所匹配的每一个元素对应type类型的事件。比如$("#chua").trigger("click");
triggeHandler只触发jQuery对象所匹配的元素中的第一个元素对应的type类型的事件,且不会触发事件的默认行为。
源码:
trigger: function(type, data) {
return this.each(function() {
jQuery.event.trigger(type, data, this);
});
},
triggerHandler: function(type, data) {
var elem = this[0];
if (elem) {
return jQuery.event.trigger(type, data, elem, true);
}
}
两者通过调用jQuery.event.trigger()函数实现,稍后解析。
unbind():
unbind: function( types, fn ) {
return this.off( types, null, fn );
},
undelegate():
undelegate: function( selector, types, fn ) {
// ( namespace ) or ( selector, types [, fn] )
return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
}
它们均调用了off函数:
off: function(types, selector, fn) {
var handleObj, type;
//传入的参数是事件且绑定了处理函数
if (types && types.preventDefault && types.handleObj) {
// ( event ) dispatched jQuery.Event
handleObj = types.handleObj;
//types.delegateTarget是事件托管对象
jQuery(types.delegateTarget).off(
//组合jQuery识别的type
handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
handleObj.selector,
handleObj.handler
);
return this;
}
if (typeof types === "object") {
// ( types-object [, selector] )
for (type in types) {
this.off(type, selector, types[type]);
}
return this;
}
if (selector === false || typeof selector === "function") {
// ( types [, fn] )
fn = selector;
selector = undefined;
}
if (fn === false) {
fn = returnFalse;
}
return this.each(function() {
off函数又调用了jQuery.event.remove函数,源码分析如下:
remove: function(elem, types, handler, selector, mappedTypes) {
var j, origCount, tmp,
events, t, handleObj,
special, handlers, type, namespaces, origType,
//获取该元素的jQuery内部数据
elemData = data_priv.hasData(elem) && data_priv.get(elem);
//如果内部数据不存在,或者内部数据没有events域则直接返回
if (!elemData || !(events = elemData.events)) {
return;
}
//第一步:分解传入的要删除的事件类型types,遍历类型,如果要删除的事件没有事件名,
//只有命名空间则表示删除该命名空间下所有绑定事件
// Once for each type.namespace in types; type may be omitted
//分解types为type.namespace为单位元素的数组
types = (types || "").match(core_rnotwhite) || [""];
t = types.length;
//对所有的事件类型进行遍历
while (t--) {
//rtypenamespace = /^([^.]*)(?:\.(.+)|)$/;
//如打印[click.test,click,test]
tmp = rtypenamespace.exec(types[t]) || [];
type = origType = tmp[1];
//对付命名空间存在多个的情况,如: .aaa.bbb.ccc
namespaces = (tmp[2] || "").split(".").sort();
// Unbind all events (on this namespace, if provided) for the element
//如果teype不存在,那么移除所有的事件!也就是当前元素的events域中间的所有的数据!
if (!type) {
for (type in events) {
jQuery.event.remove(elem, type + types[t], handler, selector, true);
}
continue;//循环继续
}
//第二步: 遍历类型过程中,删除匹配的事件,代理计数修正
//获取该类型事件的special进行特殊处理
special = jQuery.event.special[type] || {};
//如果存在selector那么就是代理对象,否则就是绑定事件到elem上面!
type = (selector ? special.delegateType : special.bindType) || type;
//获取回调函数集合
handlers = events[type] || [];
//创建该命名空间下的一个正则表达式,例如:重新组合为:xx.aaa.bbb.ccc
tmp = tmp[2] && new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)");
// Remove matching events
origCount = j = handlers.length;
while (j--) {
handleObj = handlers[j];//获取该类型事件的所有的handleObj事件
//判断该对象的origType,如果和handleObj一样表示该类事件全部要移除!
//但是必须移除的对象是origType,guid,namespace,selector都相同才可以!
if ((mappedTypes || origType === handleObj.origType) &&
(!handler || handler.guid === handleObj.guid) &&
(!tmp || tmp.test(handleObj.namespace)) &&
(!selector || selector === handleObj.selector || selector === "**" && handleObj.selector)) {
handlers.splice(j, 1);//删除handlers[j];
if (handleObj.selector) {
//如果是selector存在表示代理对象,那么把delegateCount递减!
handlers.delegateCount--;
}
if (special.remove) {
//如果special有remove方法,那么直接调用special的remove方法!
special.remove.call(elem, handleObj);
}
}
}
//第三步:如果节点上指定类型的事件处理器已经为空,则将events上的该类型的事件处理对象移除
//例如 var js_obj = document.createElement("div"); js_obj.onclick = function(){ …}
/*上面的js_obj是一个DOM元素的引用,DOM元素它长期在网页当中,不会消失,
而这个DOM元素的一属性onclick,又是内部的函数引用(闭包),
而这个匿名函数又和js_obj之间有隐藏的关联(作用域链)所以形成了一个,循环引用*/
//如果当前事件的回调函数集合已经为空,
if (origCount && !handlers.length) {
//同时该speical没有tearDown或者tearDown是false,那么用removeEvent方法!
if (!special.teardown || special.teardown.call(elem, namespaces, elemData.handle) === false) {
jQuery.removeEvent(elem, type, elemData.handle);
}
delete events[type];//移除当前回调函数集合!
}
}
// Remove the expando if it's no longer used
//如果events对象已经是空了,那么直接连handle也移除,因为events不存在那么handle已经没有存在的意义了!
//所以移除handle同时连events域也同时移除!
if (jQuery.isEmptyObject(events)) {
delete elemData.handle;
data_priv.remove(elem, "events");
}
},
4.4 jQuery事件流程
那么JQuery为了更好的对事件的支持内部又做了哪些额外的优化操作?
兼容性问题处理:
浏览器的事件兼容性是一个令人头疼的问题。IE的event在是在全局的window下, 而mozilla的event是事件源参数传入到回调函数中。还有很多的事件处理方式也一样
JQuery提供了一个 event的兼容类方案
jQuery.event.fix 对游览器的差异性进行包装处理
例如:
- 事件对象的获取兼容,IE的event在是在全局的window,标准的是event是事件源参数传入到回调函数中
- 目标对象的获取兼容,IE中采用srcElement,标准是target
- relatedTarget只是对于mouseout、mouseover有用。在IE中分成了to和from两个Target变量,在mozilla中 没有分开。为了保证兼容,采用relatedTarget统一起来
- event的坐标位置兼容
- 等等
事件的存储优化:
jQuery并没有将事件处理函数直接绑定到DOM元素上,而是通过.data存储在缓存.data存储在缓存.cahce上,这里就是之前分析的贯穿整个体系的缓存系统了
声明绑定的时候:
- 首先为DOM元素分配一个唯一ID,绑定的事件存储在.cahce[唯一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。
事件处理器:
jQuery.event.handlers
针对事件委托和原生事件(例如"click")绑定 区分对待
事件委托从队列头部推入,而普通事件绑定从尾部推入,通过记录delegateCount来划分,委托(delegate)绑定和普通绑定。
其余一些兼容事件的Hooks
fixHooks,keyHooks,mouseHooks
总的来说对于JQuery的事件绑定
在绑定的时候做了包装处理
在执行的时候有过滤器处理。
下章再看具体流程分解。