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() 方法一起使用:
    1. 使用原生的可被点击的元素,例如, a 或 button,因为这两个元素可以冒泡到 document
    2. 在 document.body 内的元素使用 .on() 或 .delegate() 进行绑定,因为移动 iOS 只有在 body 内才能进行冒泡。
    3. 需要 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() {
jQuery.event.remove(this, types, fn, selector);//最终都是调用jQuery.event.remove函数来解绑事件。
});},

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 对游览器的差异性进行包装处理

例如:

  1. 事件对象的获取兼容,IE的event在是在全局的window,标准的是event是事件源参数传入到回调函数中
  2. 目标对象的获取兼容,IE中采用srcElement,标准是target
  3. relatedTarget只是对于mouseout、mouseover有用。在IE中分成了to和from两个Target变量,在mozilla中 没有分开。为了保证兼容,采用relatedTarget统一起来
  4. event的坐标位置兼容
  5. 等等

事件的存储优化:

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的事件绑定

在绑定的时候做了包装处理

在执行的时候有过滤器处理。

下章再看具体流程分解。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值