声明:写博客,是对自身知识的一种总结,也是一种分享,但由于作者本人水平有限,难免会有一些地方说得不对,还请大家友善 指出,或致电:tianzhen.chen0509@gmail.com
关注:国内开源jQuery-UI组件库:Operamasks-UI
jQuery版本:v1.7.1
jQuery1.7系列四: 事件
一. 有感而发
处理过前端脚本事件的朋友都清楚,各浏览器在处理DOM上的事件的不一致性让人烦不胜烦。而为了提供一致的访问接口,jQuery作者提供了一套犀利的解决方案,这种思想是值得我们借鉴和学习的。
二.传统事件处理蔽端
多说无益,我们直接从以下例子来看看浏览器间在事件处理方面的一些不一致性。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="jquery-1.7.1.js"></script>
<script>
function f1(event){
//IE上事件对象从window.event获取,其它浏览器一般会以参数传进来
event = event || window.event;
alert(event.pageX);
}
function bind(){
var btn = document.getElementById("btn");
if(document.attachEvent){
btn.attachEvent("onclick" , f1);
btn.attachEvent("onclick" , f1);
}else if(document.addEventListener){
btn.addEventListener("click" , f1 , false);
btn.addEventListener("click" , f1 , false);
}
}
/**测试结果:
**在火狐上,弹出两个窗口,分别打印出 "original click." "50"
**在IE9上,弹出两个窗口,分别打印出 "original click." "undefined"
**在IE8上,弹出三个窗口,分别打印出 "original click." "undefined" "undefined"(两个undefined,你没有看错)
*/
</script>
</head>
<body οnlοad="bind()">
<input id="btn" type="button" value="click me" οnclick="alert('original click.');"/>
</body>
</html>
就上面这个非常简单的例子,我们就可以看出几个浏览器间事件处理会出现不一致的地方
1. 事件的绑定方式不同
2. 获取event对象的方式不同
3. event对象中的数据不完全相同
4. 对重复添加同一处理函数的处理方式不同,有人叠加有人忽略
还有一些例子没有体现出来,比如不可以添加两个处理事件,后添加的会覆盖前面的;阻止事件冒泡的方式和阻止其默认行为;还有什么,等你来挖掘。
既然有如此多的不一致性,jQuery作者当然想办法要进行屏蔽了,而大师的处理方式将是本文章要探讨的内容。
三.jQuery的事件处理方式
3.1 自己实现事件处理
事件绑定方式不同,这个解决不难,只要不同浏览器采用不同绑定方式,像第二部分的例子一样就行了。但如何处理类似不可以添加多个处理函数这种局限?我们自己来试着解决一下:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="jquery-1.7.1.js"></script>
<script>
window.guid = 0;//标记各个不同的DOM节点
var cache = {};//缓存对象,用于存储事件处理器
function bind(elem , type , handler){
//给dom节点添加一个唯一属性,用于标识该 dom节点
if(!elem["guid"]){
elem["guid"] = ++window.guid;
}
var events = cache[elem["guid"]];
if(!events){
//初始化dom节点的缓存数据对象
cache[elem["guid"]] = events = {};
events[type] = events[type] || [];
//同一个dom节点只注册一次事件,那就是eventHandle,然后在eventHandle中调用dispatch进行分发
if(document.attachEvent){
elem.attachEvent("on"+type , eventHandle);
}else if(document.addEventListener){
elem.addEventListener(type , eventHandle , false);
}
}
events[type].push(handler);
}
function dispatch(elem , event){
var events = window.cache[elem["guid"]],
ret;
if(!events){
return ;
}
var handlers = events[event.type];
for(var i=0,len=handlers.length; i<len; i++){
//event作为参数传递过去
ret = handlers[i].call(elem , event);
}
if(ret != undefined){
if ( ret === false ) {
event.preventDefault();
event.stopPropagation();
event.cancleBubble = true;
event.returnValue = false;
}
}
}
function eventHandle(event){
event = event || window.event;
dispatch(this , event);
}
function test(){
var btn = document.getElementById("btn");
bind(btn , "click" , function(){alert("click1");});
//当下边改为bind(btn , "click" , function(){alert("click2");return false});时,
//点击按钮不会显示 "wrapper",因为取消冒泡了
bind(btn , "click" , function(){alert("click2");});
var wrapper = document.getElementById("wrapper");
bind(wrapper , "click" , function(){alert("wrapper");});
}
//点击按钮后,弹出窗口依次显示 : click1 click2 wrapper
</script>
</head>
<body οnlοad="test()">
<div id="wrapper">
<input id="btn" type="button" value="click me"/>
</div>
</body>
</html>
以上代码虽然不是很多,但却是jQuery处理事件的一个小缩影。我们 统一给dom节点注册了一个事件eventHandle,也就是说不管该dom触发什么类型的事件,一定会执行eventHandle,然后在 eventHandle中再调用dispatch进行分发,也就是根据事件类型在缓存中查找相应的处理器。这样可以得到很多好处,最明显的就是可以添加多 个处理器了,以后用户再也不用怕兼容性问题了,我们全部都隐藏在了内部实现中。当绑定完后,我们可以看下缓存是怎样的。
如果看懂了以上这个 event 的示例,那么jQuery关于事件的处理最核心的东西你已经知道了,接下来我们来看下jQuery的做法。
3.2 jQuery的事件处理
先来看个流程图:
上图是一个处理流程图,jQuery分几步来处理:
1. 给每个dom节点注册一个唯一的处理器(eventHandle)
2. 不管该dom触发了什么事件,都会执行eventHandle, 然后eventHandle调用dispatch进行事件的真正处理
3. dispatch进行事件的修正,用jQuery自定义的事件类型来包装原生的event对象,这样提供统一的访问接口,如event.target在任何浏览器下表示触发事件的源dom节点。
4. 在jQuery公用缓存中获取该dom节点的处理器数据,由于委托机制的存在(稍后再讲),所以先获取此节点的委托处理器列表。
5. 把此节点的委托处理器列表与本节点本身的处理器列表进行合并,形成最终的处理器列表。
6. 执行最终的处理器列表,并即时处理取消冒泡和阻止浏览器默认行为。
看到这里,发现大体思想还是比较简单的吧,那我们再来深入一些细节,首先看看当绑定事件后缓存的存储情况怎样?
Html代码
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="jquery-1.7.1.js"></script>
<script>
$(function($){
$("#btn").bind("click" , function(){alert("f1");})
.bind("click" , function(){alert("f2");});
});
//当点击按钮后弹出窗口依次显示 f1 f2
</script>
</head>
<body>
<div id="wrapper">
<input id="btn" type="button" value="click me"/>
</div>
</body>
</html>
绑定后缓存热图:
通过此图我们可以看到这跟我们自己实现的缓存方式是非常类似的,只是多了一些额外的东西,比如名称空间(稍后会讲),事件委托。
另外提一个,因为一个事件类型可以有多个处理器函数,默认情况下,当你取消了冒泡行为时(event.stopPropagation),这多个处理器还 是会全部执行的,如果你想取消冒泡同时当前dom节点未执行的处理器函数也不执行了,则可以调用event. stopImmediatePropagation。
3.3 jQuery事件委托 (delegate)
首先,我们通过一个例子来看下什么是事件的委托。
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="jquery-1.7.1.js"></script>
<script>
$(function($){
var myTable = $("#mytable");
myTable.delegate("tr" , "dblclick", function(){alert("行双击");});
$("#add").click(function(){
$("<tr><td>123</td></tr>").appendTo(myTable);
});
//不管你何时点击"添加"按钮,都会给表格新增一行
//不管是哪一行,只要在行上双击一行,都会弹出窗口显示 "行双击"
//最神奇的是,我们根本没有显示给tr添加过"行双击"事件,而是通过委托的形式
});
</script>
</head>
<body>
<table id="mytable">
</table>
<input id="add" type="button" value="添加" />
</body>
</html>
委托是种很方便的东西,说得白一点,就是孩子节点的事件处理器放在父节点的事件处理器列表中,让父节点统一来进行触发。如果没有这种机制的话,那么前面的例子就要单独给每个tr添加事件了,浪费了不少的空间,而且有时还要写多代码。
就上面这个例子我们看下委托后的缓存情况:
当我们双击了tr时,触发tr双击事件,然后冒泡到table,table根据delegateCount的值进行检查,因为 delegateCount=1,所以它会对table的事件处理器列表第一个函数(个数由delegateCount决定)进行检查,看它的 selector是否符合当前触发事件的源dom节点,结果发现刚刚匹配,说明此函数也是要执行的,再与后边剩余(总数-delegateCount)的 函数列表进行合并,成为最终的函数处理器列表,这便是事件委托。
最后提一句,在事件的处理上,jQuery1.7提供了两个堪称万能的api,分别是on和off,至于怎么用,就不多说了,文档一看就清楚了。
四. 名称空间
jQuery事件还有一个很新鲜的东西,那就是其独有的名称空间了。但是此名称空间与我们普通想的却是不太一样的,来看看jQuery的处理就知道怎么回事了。
首先介绍一下这个名称空间。我们在绑定事件时可以这样来绑定,$(“#btn”).bind(“click.www.jquery.com”, fn); 看到了吗,在事件类型后边可以加上”.www.jquery.com”这串东西,这整整一串就称为名称空间。(不要自以为www是一级,jquery是一 级,com是一级),在这里是没有多级这种概念的。在保存这个名称空间的时候,jQuery会这样做: 把www.jquery.com进行split,得到[“www”,”jquery”,”com”],然后进行sort,得到[“com” , “jquery” , “www”],然后再进行join得到com.jquery.www,最后保存在缓存当中。而当我们利用trigger想触发某个事件时,比如我们执 行$(“#btn”).trigger(“click”),这时候jQuery是这样处理的:“click”并没有名称空间,只有纯粹的类型,所以触发所 有类型为click的函数处理器(不管名称空间是什么),但有一个特例,也就是”!”号的使用,如果你刚刚写的是 $(“#btn”).trigger(“click!”),那么只会触发类型为click,并且名称空间为空的函数处理器。
另外一种就是我们触发有名称空间的事件了,比如$(“#btn”).trigger(“click.jquery.com”),这时候又怎么处理呢?
click.jquery.com 的名称空间为 jquery.com,split一下,变成[“jquery”,”com”],再sort一下,成[“com” , “jquery”],再join一下,成“com.jquery”,然后依次检查click类型的处理器,看看它的名称空间是否匹 配”com.jquery”,此例的匹配是这样的:/(^|\.)com\.(?:.*\.)?jquery(\.|$) /.test(“com.jquery.www”);左边的//正则是动态产生出来的,而test()中的” com.jquery.www”则是处理器中的名称空间,只要这个匹配成功了,那么该处理器便会得到执行。显然此例子是匹配成功的。关于这个正则就不多说 了,并不是很难看懂。
五. 后语
本文主要是对jQuery事件实现方式的一个解说,并展示了自己实现的一个简单版本的事件处理,希望可以帮助大家对JQuery事件的实现有更好的了解。