欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

jQuery源码学习(12)-事件绑定(1)

程序员文章站 2022-05-31 11:53:26
...

1、javaScript传统的事件处理

给某一个元素绑定了一个点击事件,传入一个回调句柄处理:

element.addEventListener('click',doSomething,false);

但是如果页面上有几百个元素需要绑定,那需要绑定几百次,这样就可以知道传统绑定方法存在的问题:

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

关于jQuery提出的事件绑定的一些方法:bind()、live()、on()、delegate()。对应的解除事件绑定的函数分别是:unbind()、die()、off()、undelegate()。其中,live和delegate方法利用了事件委托机制,很好的解决了大量事件绑定的问题,所以先介绍事件委托的原理。

2、事件委托

DOM有个事件流的特性,也就是说我们在页面上触发节点的时候事件都会上下或者向上传播,事件捕捉和事件冒泡。

DOM2.0模型将事件处理流程分为三个阶段:一、事件捕获阶段,二、事件目标阶段,三、事件起泡阶段。

模型如图所示:

jQuery源码学习(12)-事件绑定(1)

事件传送可以分为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有:

jQuery源码学习(12)-事件绑定(1)

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源码学习(12)-事件绑定(1)

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

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

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

下章再看具体流程分解。

相关标签: 事件绑定