DOM二级事件

        我们原来学过DOM,DOM是解决文档里元素关系的一套模式,其实那只是DOM的第一个版本解决的问题。在DOM的第二个版本里,解决的问题就不仅仅是文档里元素之间的关系里,还把DOM元素的事件问题重新给了一套解决方案,这套方案就叫“DOM二级事件”。
DOM二级事件解决了原来的同一事件绑定多个处理方法时,后面的绑定会覆盖前面的绑定的问题,如:
ele.οnclick=fn1
ele.οnclick=fn2;
这样处理的结果就是,ele的onclick事件上,fn2方法把fn1方法给覆盖了,这样不容易实现在同一事件上绑定多个方法。
W3C给出的方法是这样的:
ele.addEventListener(“click”,fn,false);
IE6/7/8给出的方案是:
ele.attachEvent(“onclick”,fn);


解决标准浏览器和低版本的IE方案如下:事件绑定
function bind(ele,type,handler){
if(ele.addEventListener){//标准浏览器专用
ele.addEventListener(type,handler,false)
}else if(ele.attachEvent){//IE专用
ele.attachEvent("on"+type,handler);

}
移除事件绑定:
function unbind(ele,type,handler){
if(ele.removeEventListener){//标准流程器专用
ele.removeEventListener(type,handler,false)
}else if(ele.detachEvent){//IE专用
ele.detachEvent("on"+type,handler);
}
}
注意:虽然IE、火狐、chrome等浏览器都给出了DOM2级事件的处理方法,但IE的方法却存在着很多问题,并且非常严重。一是被绑定的方法在事件触发执行时,this关键字竟然是window,二是IE中被绑定到事件上的方法的执行顺序是混乱的。
在W3C的标准是在同一事件上,先绑定的方法先执行,并且不能重复绑定同一个方法在同一个事件上。但IE6/7/8中,如果绑定的方法少于9个,执行的顺序是相反的,超过9个,执行是混乱的。
这些IE中的这些问题都比较严重,我们必须解决好。
事件的兼容性问题总结(常见的)
·  事件对象本身:标准浏览器是事件发生时自动给方法传一个实参,这个实参就是事件对象。IE是全局的window.event
·  阻止事件传播:e.stopPropagation这个方法,IE是e.cancelBubble=true这个属性
·  阻止默认行为:e.preventDefault()方法,IE是e.returnValue=false;
·  事件源:e.target.IE是e.srcElement
·  e.pageX,e.pageY,IE不支持这两个属性
·  DOM二级的事件绑定:ele.addEventListener,IE是ele.attachEvent
·  IE的attachEvent绑定的方法上:1、this不是当前的元素,2执行顺序是混乱的


DOM二级事件事件兼容性问题解决之一:解决this关键字
注意:以下代码中的handler是个形参,它可以表示不同的方法,所以不要把handler认为是某一个具体的方法。如果你bind(ele,”click”,fn1),则fn1是handler,如果bind(ele,”click”,fn2),则fn2是handler。新手一定要理解好把握好。
关键思路:关键是两个问题,一是理解好call的用途作用,二是理解好在一个程序里写的代码,是为了解决另外一个程序中的问题的这种思路。
我们修改一下bind这个方法
如果只是解决被绑定的方法的this指向,这倒好办,只需在IE的专用的代码里,帮如下修改即可
else if(ele.detachEvent){//IE专用
function fnTemp=function(){handler.call(ele)};//使用call方法,强制使handler方法在运行时this指向被绑定的ele这个DOM元素
ele.detachEvent("on"+type,fnTemp);//再绑定时,就不是直接绑定handler这个方法了,而是绑定经过“化装”的fnTemp这个方法
}


或直接这样写:
else if(ele.detachEvent){//IE专用
ele.detachEvent("on"+type,function(){handler.call(ele)});
}
这样确实在事件触发时,handler运行,并且让handler的this指向了被绑定的元素ele,但由于我们已经不是直接绑定的handler方法了,而是经过call“变形”后的fnTemp方法,那在移除绑定的时候,我们就没办法移除handler方法。
那我们怎么能找到这个化装之后fnTemp,并将其移除呢?这件事必须要在绑定事件的时候就要考虑好:在bind方法里,把fnTemp方法保存下来,并且还要能用某种方式识别出这个fnTemp方法是由那一个handler“变形”而来的。
这儿是个不好理解的难点,做这件事,我们需要两步:
【这儿两次都用到了对象的自定义属性,不好理解,望认真思考】
一、把fnTemp保存下来,不能用全局变量保存,因为容易被污染;保存在bind的某个变量里,局部变量在unbind这个作用域里也访问不到。在不同的作用域里,还能访问到一个非全局变量的值,那用什么呢?最方便的方式就是把fnTemp保存在ele这个DOM元素的属性上,因为这个ele是两个函数都要操作的引用类型的变量,那么我们在ele上定义一个自定义属性,那这个属性在bind和unbind两个作用域里都能访问的到。具体实现方式如下:


}else if(ele.attachEvent){//IE专用代码
if(!ele["aBind"+type]){//如果不存在这个属性,则创建一个
ele["aBind"+type]=[];//使用数组来保存被绑定到不同事件上的那些方法(相当的事件上,可能会被绑定很多个handler)
//这个属性是以aBind为前缀,以type为区分符的。Type是事件类型,这是一个非常重要的技巧,
}
var tempFn=function (){handler.call(ele);}//变形这个方法,让这个方法运行的时候this指向被绑定的元素
ele["aBind"+type].push(tempFn);//把变形后的tempFn保存到这个数组里
ele.attachEvent("on"+type,tempFn);

补充:理解好ele["aBind"+type]=[]这个属性定义时,”aBind”这个字符串是个区别符的意思。先来看如果没有这个”aBind”会怎么样?那就定义成了ele[type]=[],如果type是”click”,则会出现ele.click=[],而ele本身就有click这个方法属性,我们是没办法修改这个原生属性的,这样定义就失效了。所以才给”click”前面加个”aBind”做为前缀,以避免或减少和原生的属性冲突。这种加前缀的技巧还是很常见的。
二、给tempFn再加一个自定义属性,用来标识当前这个tempFn是由handler“变形”而来的
var tempFn=function (){handler.call(ele);}//变形这个方法,让这个方法运行的时候this指向被绑定的元素
tempFn.photo=handler;//我们通过tempFn的photo这个属性,就可以分辨出tempFn这个方法是由那个handler变形而来的。Photo这个属性只是在这儿定义,而使用它是在unbind函数里。


三、还要强调一个DOM2级事件绑定编程的原则,即:一个函数不能被重复绑定在同一个事件上。比如:不能把fn1这个函数重复绑定给ele的click事件:
ele.addEventListener(“click”,fn1,false);
ele.addEventListener(“click”,fn1,false);//绑定两次或多次,但是事件触发时,后面的绑定是无效的。
当然,低版的IE浏览器没有遵循这个原则,所以这儿还要解决一下这个问题,加一个判断即可(在ele)
for(var i=0;i<ele["aBind"+type].length;i++){
if(ele["aBind"+type][i].photo==handler){//如果数组里已经存在了经过化装的handler方法,则退出执行
return;//保证一个方法只能被绑定到某事件上一次
}
}


Bind方法完整的代码如下:
function bind(ele,type,handler){
if(ele.addEventListener){
ele.addEventListener(type,handler,false)
}else if(ele.attachEvent){
if(!ele["aBind"+type]){//如果不存在这个属性,则创建一个
ele["aBind"+type]=[];//这个属性是以aBind为前缀,以type为区分符的
}
var tempFn=function (){handler.call(ele);}//变形这个方法,让这个方法运行的时候this指向被绑定的元素
tempFn.photo=handler//photo标识型的属性,用做和handler关联
for(var i=0;i<ele["aBind"+type].length;i++){
if(ele["aBind"+type][i].photo==handler){
return;//保证一个方法只能被绑定到某事件上一次
}
}
ele["aBind"+type].push(tempFn);
ele.attachEvent("on"+type,tempFn);
};
}
我们再来看unbind。该做的准备工作,都已经在bind里完成了。Unbind负责把绑定在事件上的方法移除,但现在已经不是移除handler这个方法了,而是移除经过化装后的这个方法,这个方法被保存在ele的"aBind"+type这个属性上。
我们知道ele["aBind"+type]这是个数组,先把它取到,赋给一个短变量a:var a=ele["aBind"+type]。操作a这个短变量比操作ele["aBind"+type]这个属性更方便。
然后遍历这个数组,逐个比较那个是经过化装的handler方法,当然比较的依据是photo这个属性,所以是:
1.for(var i=0;i<a.length;i++){//遍历
2. if(a[i].photo==handler){//通过photo属性做比较。在bind里定义的photo属性
3. ele.detachEvent("on"+type,a[i]);//把这个方法从事件移除
4. a.splice(i,1);//并且一定要把这个方法从数组里移除了,要不然下一次就不能再绑定了。
5. return ;//因为每一次绑定都是唯一的(这是原则,上边讲了),所以移除后直接结束这个函数的运行就可以了
}
}
解释第四行:用a.splice会造成“数组塌陷”,所以在正式的代码里,用a[i]=null来解决的。看下面代码的第10行。大家要注意上课时看老师的BUG测试演示。
完整的unbind代码如下:
1.function unbind(ele,type,handler){
2. if(ele.removeEventListener){//标准浏览器
3. ele.removeEventListener(type,handler,false);
4. }else if(ele.detachEvent){//IE浏览器
5. var a=ele["aBind"+type];//在bind里约定好的那个自定义
6. if(a){//如果存在这个数组
7. for(var i=0;i<a.length;i++){
8. if(a[i].photo==handler){//这里用了在bind里定义的photo属性
9. ele.detachEvent("on"+type,a[i]);
10. a[i]=null;//改进之后的,避免动态移除的时候会出问题
11. return ;
12. }
13. }
14. }
15. }
16.}




DOM二级事件事件兼容性问题解决之二:执行顺序问题
首先要明确的是在IE6/7/8中,如果一个事件上绑定多个方法,它的执行顺序是相反的或混乱的,这也是IE中另一个很严重的问题。
解决这个问题的原则,其实相当于把浏览器的事件机制给重写了一遍。
就是是把IE浏览器自己的那套事件机制抛弃了,然后自己构造一个可以按顺序执行的程序池(或叫程序清单也行),然后再编写一个run方法,让某个事件发生的时候,让run方法依次去遍历执行程序池里的那些方法。
比如,我们要给ele的click事件绑定fn1,fn2,fn3,fn4,fn5等若干个方法,我们不直接写成bind(ele,”click”,fn1)这样的方式,而是先创建一个数组,这个数组保存在ele的一个自定义属性上,以”aEvent”这个字符串为前缀,以事件类型为区分符(这个技巧在解决this指向的时候用过)。每绑定一个方法,我们就push进去一个。
伪代码如下:
var type=”click”
ele[”aEvent”+type]=[];
ele[”aEvent”+type].push(fn1);
ele[”aEvent”+type].push(fn2);
ele[”aEvent”+type].push(fn3);
ele[”aEvent”+type].push(fn4);
ele[”aEvent”+type].push(fn5);


那当事件发生的时候怎么去执行保存在这个ele[”aEvent”+type]的方法呢?
先定义一run方法,由这个run方法来负责去数组里遍历执行保存好的那些方法。
1.function run(){
2. var a=this["aEvent"+event.type];//这里的this是ele,因为run函数被绑定给了ele。
3. if(a){//为了确定代码不出意外,先判断一下数组是否存在
4. for(var i=0;i<a.length;i++){//遍历
5. a[i].call(this, event);//用call方法去调用执行,确保了数组里的方法的this是指向被绑定元素的。这是个不好理解的点。event是事件对象
6. }
7. }
8.}


然后真正绑定事件的时候,用上面写好的bind函数绑定这个run函数:
bind(ele,”click”,run);
这样,当click事件发生的时候,真正由事件触发而执行的函数其实是run(这里用的是浏览器的事件机制),而run再去数组里遍历执行保存下来的那些函数(这里就避开了浏览器的执行机制)。因为数组的循环执行是有序的,所以这个保存在数组里的方法肯定也是按顺序执行的。
只要理解了这儿,总体的思路就没有问题了,下面就是把代码写的更有通用性。
相对完整的代码如下:
先要写一个on方法,负责创建程序池,并且把需要绑定在事件上的方法保存在这个池子里:
function on(ele,type,handler){//订阅:产生一张清单
if(!ele["aevent"+type]){
ele["aevent"+type]=[];
}
var a=ele["aevent"+type];
for(var i=0;i<a.length;i++){
if(a[i]==handler)return;//避免重复绑定
}
a.push(handler);
bind(ele,type,run);//真正的绑定,虽然on方法会多次运行,但不会多次被绑定这个run方法,这是在bind函数里约定好的原则
}
function run(e){//这个负责通知:遍历那张清单并且执行
e=e||window.event;
var a=this["aevent"+e.type];//在on里产生的那个清单
//下面是解决兼容性问题的代码,这是个整体的事件解决方案
if(!e.stopPropagation){
e.stopPropagation=function(){e.cancelBubble=true;}
}
if(!e.preventDefault){
e.preventDefault=function(){e.returnValue=false}
}
if(typeof e.pageX=="undefined"){
e.pageX=(document.documentElement.scrollLeft||document.body.scrollLeft)+e.clientX;
e.pageY=(document.documentElement.scrollTop||document.body.scrollTop)+e.clientY;
}
if(typeof e.target =="undefined"){
e.target=e.srcElement;
}

if(a){
for(var i=0;i<a.length;i++){//遍历
a[i].call(this, e);//e是事件对象
}
}
}
拖拽的最基本原始代码
拖拽的基本原理:
拖拽是三种事件的组合
当鼠标按下时准备拖拽,当鼠标按着并且鼠标移动时进行拖拽,当鼠标松开时,结束拖拽。盒子是随着鼠标移动而移动的,鼠标移动多远它就移动多远。
鼠标移动的距离是=鼠标现在的位置-鼠标按下去的时的位置。
盒子移动的公式是:盒子现在的位置=盒子原来的位置(mousedown时的)+鼠标移动的距离


在学习这个案例的时候,一个是要把握好对this的理解和应用,另一个就是把握好对自定义属性的应用。
以下的代码,每个函数都是低耦合的,每一个函数里都不会出现具体的某一个被拖拽元素的变量名,而是灵活使用this关键字来获得被拖拽的元素。this的好处在这儿能很明显的体显出来。
把获得到的值保存在当前被拖拽的元素的自定义属性上,避免了全局变量容易被污染的问题。
var ele=document.getElementById('div1');
ele.οnmοusedοwn=down;
function down(e){//准备拖拽
this.x=this.offsetLeft;//先要把盒子的原始位置保存下来,保存到自定义属性x上
this.y=this.offsetTop;//把盒子的垂直坐标保存在this.y上
this.mx=e.clientX;
this.my=e.clientY;
this.οnmοusemοve=move;
this.οnmοuseup=up;
}
function move(e){
this.style.left=e.clientX-this.mx+this.x+"px";//当鼠标移动时,重新计算盒子的坐标
this.style.top=e.clientY-this.my+this.y+"px";
}
function up(){//当鼠标按键抬起的时候,结束拖拽
this.οnmοusemοve=null;//把事件属性清空
this.οnmοuseup=null;
}
经过优化和改进之后的拖拽代码
ele.οnmοusedοwn=down;
/*
因为在下面的拖拽里经常需要让事件绑定的方法在运行的时候,this关键字指向被拖拽的元素,所以把这段经常被重复利用的逻辑封装成一个小方法。
目的就是让fn方法在运行的时候,功能不变,但this指向指定的对象obj。
*/
function processThis(obj,fn){//这个函数一定要理解好
return function(e){fn.call(obj,e);} //返回这个方法。事件在绑定的时候,其实是绑定的这个方法,而运行的时候,其实还是运行fn这个方法
}
function down(e){
e=e||window.event;
this.x=this.offsetLeft;
this.y=this.offsetTop;
this.mx=e.clientX;
this.my=e.clientY;
if(this.setCapture){//
this.setCapture();//专门处理mousemove事件的方法,使DOM元素不能丢失鼠标。目前IE和FireFox支持此方法
this.οnmοusemοve=move;
this.οnmοuseup=up;
}else{//chrome浏览器,把给document绑定mousemove事件.只有文档这个大容器不会丢掉鼠标。
//var that=this;//这是开始用的技巧,注意体会对this的处理
//this.MOVE=function(e){move.call(that,e)}
//this.UP=function(e){up.call(that,e)}
//document.οnmοusemοve=this.MOVE;
//document.οnmοuseup=this.UP;
document.οnmοusemοve=processThis(this,move);//用了上边定义的这个方法
document.οnmοuseup=processThis(this,up)
}
}
function move(e){
e=e||window.event;
this.style.left=e.clientX-this.mx+this.x+"px";
this.style.top=e.clientY-this.my+this.y+"px";
}
function up(){
if(this.releaseCapture){
this.οnmοusemοve=null;
this.οnmοuseup=null;
this.releaseCapture();
}else{
document.οnmοusemοve=null;
document.οnmοuseup=null;
}
}




附录:写在HTML标签里的事件是如何被浏览器解析并执行的
比如HTML代码如下:
<div id="div1" οnclick=”fn()” onClick="fn.call(this,event)" onClick="fn(event)" >
这里面写的三种格式,是为了对比
</div>
写在HTML代码里的事件绑定,相当于把事件后边的字符串当参数,传给 new Function("fn()"),相当于生成了这样一个方法:function(){fn();}
div1.οnclick=function(){fn();}
FireFox里生成的匿名方法是这样的
div1.οnclick=function(event){fn(event);}
这个function(event){}匿名方法才是直接被绑定到click事件上的方法,它形式参数名叫event。而fn是在匿名方法里被调用(invoke),如果fn需要事件对象,则必须传实参:就是event.
只有一个方法,直接被绑定给事件才不需要传实参
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值