zl程序教程

您现在的位置是:首页 >  前端

当前栏目

jQuery源码分析之Event事件分析

jQuery事件源码 分析 Event
2023-06-13 09:14:19 时间

对于事件的操作无非是addEvent,fireEvent,removeEvent这三个事件方法。一般lib都会对浏览器的提供的函数做一些扩展,解决兼容性内存泄漏等问题。第三个问题就是如何得到domReady的状态。
  6.1event的包裹

  浏览器的事件兼容性是一个令人头疼的问题。IE的event在是在全局的window下,而mozilla的event是事件源参数传入到回调函数中。还有很多的事件处理方式也一样。

  Jquery提供了一个event的包裹,这个相对于其它的lib提供的有点简单,但是足够使用。

复制代码代码如下:

//对事件进行包裹。
  fix:function(event){
   if(event[expando]==true)returnevent;//表明事件已经包裹过
   //保存原始event,同时clone一个。
   varoriginalEvent=event;               ①
   event={ originalEvent:originalEvent};
   for(vari=this.props.length,prop;i;){
     prop=this.props[--i];
     event[prop]=originalEvent[prop];
   }   
   event[expando]=true;   
   //加上preventDefaultandstopPropagation,在clone不会运行
   event.preventDefault=function(){         ②
     //在原始事件上运行
     if(originalEvent.preventDefault)
       originalEvent.preventDefault();
     originalEvent.returnValue=false;
   };
   event.stopPropagation=function(){
     //在原始事件上运行
     if(originalEvent.stopPropagation)
       originalEvent.stopPropagation();
     originalEvent.cancelBubble=true;
   };
   //修正timeStamp
   event.timeStamp=event.timeStamp||now();
   //修正target
   if(!event.target)                   ③
     event.target=event.srcElement||document;    
   if(event.target.nodeType==3)//文本节点是父节点。
     event.target=event.target.parentNode;
   //relatedTarget
   if(!event.relatedTarget&&event.fromElement)   ④
     event.relatedTarget=event.fromElement==event.target
         ?event.toElement:event.fromElement;
   //CalculatepageX/YifmissingandclientX/Yavailable
   if(event.pageX==null&&event.clientX!=null){ ⑥
     vardoc=document.documentElement,body=document.body;
    event.pageX=event.clientX
       +(doc&&doc.scrollLeft||body&&body.scrollLeft||0)
         -(doc.clientLeft||0);
     event.pageY=event.clientY
       +(doc&&doc.scrollTop||body&&body.scrollTop||0)
         -(doc.clientTop||0);
   }
  
   //Addwhichforkeyevents
  if(!event.which &&((event.charCode||event.charCode===0)⑦
          ?event.charCode :event.keyCode))
     event.which=event.charCode||event.keyCode;
  
  //AddmetaKeytonon-Macbrowsers
   if(!event.metaKey&&event.ctrlKey)            ⑧
     event.metaKey=event.ctrlKey;
  //Addwhichforclick:1==left;2==middle;3==right
  //Note:buttonisnotnormalized,sodon"tuseit
   if(!event.which&&event.button)             ⑨
     event.which=(event.button&1?1:(event.button&2
         ?3:(event.button&4?2:0)));
   returnevent;
},


  上面的代码①处保留原始事件的引用,同时clone原始事件。在这个clone的事件上进行包裹。②处在原始事件上运行preventDefault和stopPropagation两个方法达到是否阻止默认的事件动作发生和是否停止冒泡事件事件向上传递。

  ③处是修正target个,IE中采用srcElement,同时对于文本节点事件,应该把target传到其父节点。

  ④处relatedTarget只是对于mouseout、mouseover有用。在IE中分成了to和from两个Target变量,在mozilla中没有分开。为了保证兼容,采用relatedTarget统一起来。

  ⑥处是进行event的坐标位置。这个是相对于page。如果页面可以scroll,则要在其client上加上scroll。在IE中还应该减去默认的2px的body的边框。

  ⑦处是把键盘事件的按键统一到event.which的属性上。Ext中的实现ev.charCode||ev.keyCode||0;⑨则是把鼠标事件的按键统一把event.which上。charCode、ev.keyCode一个是字符的按键,一个不是字符的按键。⑨处采用&的方式来进行兼容性的处理。Ext通过下面三行解决兼容问题。

 varbtnMap=Ext.isIE?{1:0,4:1,2:2}:(Ext.isSafari?{1:0,2:1,3:2}:{0:0,1:1,2:2});this.button=e.button?btnMap[e.button]:(e.which?e.which-1:-1);

  ①②③④⑤⑥⑦⑧⑨⑩

  6.2事件的处理

  Jquery提供了一些来进行regist,remove,fire事件的方法。

  6.2.1Register

  对于注册事件,jquery提供了bind、one、toggle、hover四种注册事件的方法,bind是最基本的方法。One是注册只运行一次的方法,toggle注册交替运行的方法。Hover是注册鼠标浮过的方法。
复制代码代码如下:

bind:function(type,data,fn){
  returntype=="unload"?this.one(type,data,fn):this
   .each(function(){//fn||data,fn&&data实现了data参数可有可无
         jQuery.event.add(this,type,fn||data,fn&&data);
       });},



  Bind中对于unload的事件,只能运行一次,其它的就采用默认的注册方式。

//为每一个匹配元素的特定事件(像click)绑定一个一次性的事件处理函数。
//在每个对象上,这个事件处理函数只会被执行一次。其他规则与bind()函数相同。
//这个事件处理函数会接收到一个事件对象,可以通过它来阻止(浏览器)默认的行为。
//如果既想取消默认的行为,又想阻止事件起泡,这个事件处理函数必须返回false。
复制代码代码如下:
  one:function(type,data,fn){
   varone=jQuery.event.proxy(fn||data,function(event){
     jQuery(this).unbind(event,one);
     return(fn||data).apply(this,arguments);/this->当前的元素
       });
     returnthis.each(function(){
       jQuery.event.add(this,type,one,fn&&data);
     });
  },

  One与bind基本上差不多,不同的在调用jQuery.event.add时,把注册的事件处理的函数做了一个小小的调整。One调用了jQuery.event.proxy进行了代理传入的事件处理函数。在事件触发调用这个代理的函数时,先把事件从cache中删除,再执行注册的事件函数。这里就是闭包的应用,通过闭包得到fn注册的事件函数的引用。

//一个模仿悬停事件(鼠标移动到一个对象上面及移出这个对象)的方法。
//这是一个自定义的方法,它为频繁使用的任务提供了一种“保持在其中”的状态。
//当鼠标移动到一个匹配的元素上面时,会触发指定的第一个函数。当鼠标移出这个元素时,
/会触发指定的第二个函数。而且,会伴随着对鼠标是否仍然处在特定元素中的检测(例如,处在div中的图像),
 //如果是,则会继续保持“悬停”状态,而不触发移出事件(修正了使用mouseout事件的一个常见错误)。
   hover:function(fnOver,fnOut){
     returnthis.bind("mouseenter",fnOver).bind("mouseleave",fnOut);
  },



  Hover则是建立在bind的基础之上。

//每次点击后依次调用函数。
toggle:function(fn){  
varargs=arguments,i=1;
while(i<args.length)//每个函数分配GUID
   jQuery.event.proxy(fn,args[i++]);//修改后的还在args中
returnthis.click(jQuery.event.proxy(fn,function(event){//分配GUID    this.lastToggle=(this.lastToggle||0)%i;//上一个函数      event.preventDefault();//阻止缺省动作
   //执行参数中的第几个函数,apply可以采用array-like的参数
   returnargs[this.lastToggle++].apply(this,arguments)||false;
  }));
  },

  Toggle中参数可以是多个fn。先把它们代码生成UUID。之后调用click的方法来注册再次进行代理的callback。这个函数在事件触发时运行,它先计算上一次是执行了参数中的那个函数。之后阻止缺省动作。之后找到下一个函数运行。

//为jquery对象增加常用的事件方法
jQuery.each(
  ("blur,focus,load,resize,scroll,unload,click,dblclick,"
  +"mousedown,mouseup,mousemove,mouseover,mouseout,change,select,"
+"submit,keydown,keypress,keyup,error").split(","),
function(i,name){jQuery.fn[name]=function(fn){
         returnfn?this.bind(name,fn):this.trigger(name);
       };});

  Jquery增加了一个常用的事件处理方法,包含上面调用的click。这里可以看出这里还是调用bind进行注册。当然这里还可以通过程序实现去触发事件。



  上面的众多方法都是注册事件,其最终都落在jQuery.event.add();来完成注册的功能。如果我们采用Dom0或DOM1的事件方法,我们会采用elem.onclick=function(){}来为元素的某一种事件来注册处理函数。这个最大的缺点就是每个一个事件只是一个处理函数。在dom1的方式中有改进,我们可以采用elem.addEventListener(type,handle,false)为元素的事件注册多个处理函数。

  这样的处理方式还不是很完美,如果我们只这个事件运行一次就有点麻烦了。我们要在事件的处理函数中最后进行elem.removeEventListener来取消事件的监听。这样做可能会有事务上的问题。如果第一个事件处理函数在没有取消事件监听之前,就再次触发了怎么办?

  还有采用浏览器的方式,它不支持自定义事件的注册和处理,还不能为多个事件注册同一个处理函数。
复制代码代码如下:
jQuery.event={//add事件到一个元素上。
add:function(elem,types,handler,data){
if(elem.nodeType==3||elem.nodeType==8)return;//空白节点或注释
  //IE不能传入window,先复制一下。
if(jQuery.browser.msie&&elem.setInterval)elem=window;
//为handler分配一个全局唯一的Id
if(!handler.guid) handler.guid=this.guid++;
//把data附到handler.data中
if(data!=undefined){                      ①
varfn=handler;
handler=this.proxy(fn,function(){returnfn.apply(this,arguments);});
handler.data=data;
  }
//初始化元素的events。如果没有取到events中值,就初始化data:{}   ②
varevents=jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),
//如果没有取到handle中值,就初始化data:function(){....}    ③
handle=jQuery.data(elem,"handle")||jQuery.data(elem,"handle",
function(){//处理一个触发器的第二个事件和当page已经unload之后调用一个事件。
   if(typeofjQuery!="undefined"&&!jQuery.event.triggered)
     returnjQuery.event.handle.apply(//callee.elem=handle.elem
         arguments.callee.elem,arguments);
     });
//增加elem做为handle属性,防止IE由于没有本地Event而内存泄露。
handle.elem=elem;
//处理采用空格分隔多个事件名,如jQuery(...).bind("mouseovermouseout",fn);
jQuery.each(types.split(/s+/),function(index,type){   ④
  //命名空间的事件,一般不会用到。
varparts=type.split(".");type=parts[0];handler.type=parts[1];
  //捆绑到本元素type事件的所有处理函数
varhandlers=events[type];                    ⑤
if(!handlers){//没有找到处理函数列表就初始化事件队列
   handlers=events[type]={};
  //如果type不是ready,或ready的setup执行返回false        ⑥
if(!jQuery.event.special[type]||jQuery.event.special[type].setup
   .call(elem,data)===false){//调用系统的事件函数来注册事件
if(elem.addEventListener)elem.addEventListener(type,handle,false);
elseif(elem.attachEvent)elem.attachEvent("on"+type,handle);
     }
}
//把处理器的id和handler形式属性对的形式保存在handlers列表中,
//也存在events[type][handler.guid]中。
handlers[handler.guid]=handler;                 ⑦
//全局缓存这个事件的使用标识
jQuery.event.global[type]=true;
});
  
  elem=null;//防止IE内存泄露。
  },
  guid:1,
  global:{},



  jQuery.event.add通过jQuery.data把事件相关的事件名和处理函数有机有序地组合起存放在jQuery.cache中与该元素对应的空间里。我们就一个例子分析一下add的过程中:假如我们招待下面jQuery(e1).bind("mouseovermouseout",fn0);jQuery(e1).bind("mouseovermouseout",fn1)的语句。

  在jQuery(e1).bind("mouseovermouseout",fn0);时,②③都不可能从cache取到数,先初始化。此时的cache:{e1_uuid:{events:{},handle:fn}}。接着在⑤会为mouseovermouseout名初始化。此时的cache:{e1_uuid:{events:{mouseover:{},mouseout:{}},handle:fn}}。在⑥处向浏览器的事件中注册处理函数。接着⑦会把处理函数到事件名中。此时的cache:{e1_uuid:{events:{mouseover:{fn0_uuid:fn0},mouseout:{fn0_uuid:fn0}},handle:fn}}。这里可以看出为采用proxy为函数生成uuid的作用了。

  在jQuery(e1).bind("mouseovermouseout",fn1)时,②③都从cache取到数据{e1_uuid:{events:{mouseover:{fn0_uuid:fn0},mouseout:{fn0_uuid:fn0}},接着在⑤取到mouseover:{fn0_uuid:fn0},mouseout:{fn0_uuid:fn0}的引用。接着⑦会把处理函数注册到事件名中。此时的cache:{e1_uuid:{events:{mouseover:{fn0_uuid:fn0,fn1_uuid:fn1,},mouseout:{fn0_uuid:fn0,fn1_uuid:fn1}},handle:fn}}。

  jQuery.event.add很重要的任务就是把注册的事件函数有序地存放起来。以便remove和fire事件的函数能找到。

//{elem_uuid_1:{events:{mouseover:{fn_uuid:fn1,fn_uuid1:fn2},
      //mouseout:{fn_uuid:fn1,fn_uuid1:fn2}},handle:fn}}

  6.2.2trigger



  注册了事件,如onclick。那么当用户点击这个元素时,就会自动触发这个事件的已经注册的事件处理函数。但是我们有的时候要采用程序来模拟事件的触发就得采用强迫触发某个事件。在IE中我们可以采用.fireEvent()来实现。如:<formonsubmit="a()">中,如果button的form.submit()的方式提交表单,是不会主动触发onsumbit事件的,如果必须的话,就要在submit前$(“:form”)[0].fireEvent("onsubmit”,),这样就会触发该事件。

  在mozilla中有三个步骤:  var evt = document.createEvent("HTMLEvents");

  evt.initEvent("change",true,true);  t.dispatchEvent( evt);

  在prototype是采用这样的方式来实现的。那么jquery中呢,它的实现方式有一点不一样。
复制代码代码如下:
trigger:function(type,data,fn){
returnthis.each(function(){
   jQuery.event.trigger(type,data,this,true,fn);
     });},

  Trigger有三个参数,data参数是为了注册的事件函数提供了实传。如果data[0]中preventDefault存在,data[0]就可以做为用户自定义的包裹事件的空间。Fn是可以为事件提供一个即时即用的事件处理方法。也就是在没有注册事件的情况下也可以通过传入处理函数来处理事件。如果已经注册了,那就是在原来的事件处理函数之后执行。

  //这个方法将会触发指定的事件类型上所有绑定的处理函数。但不会执行浏览器默认动作.
triggerHandler:function(type,data,fn){
returnthis[0]&&jQuery.event.trigger(type,data,this[0],false,fn);
  },



  triggerHandle通过把jQuery.event.trigger的donative参数设为false,来阻止执行浏览器默处理方法。它与trigger不现的一点,还在于它只是处理jquery对象的第一个元素。

  上面两个方法都调用了jQuery.event.trigger来完成任务:
复制代码代码如下:
trigger:function(type,data,elem,donative,extra){
  data=jQuery.makeArray(data);//data可以为{xx:yy}
  //支持getData!这样的形式,exclusive=true表现会对add的注册的
  //事件的所有函数进行命名空间的分种类的来执行。
if(type.indexOf("!")>=0){            ①
   type=type.slice(0,-1);varexclusive=true;
   }
if(!elem){//处理全局的fire事件           ②
  if(this.global[type])
   jQuery.each(jQuery.cache,function(){
    //从cache中找到所有注册该事件的元素,触发改事件的处理函数
     if(this.events&&this.events[type])
       jQuery.event.trigger(type,data,this.handle.elem);
     });
  }else{//处理单个元素事件的fire事件          ③
  if(elem.nodeType==3||elem.nodeType==8) returnundefined;
   varval,ret,fn=jQuery.isFunction(elem[type]||null),
  //如果data参数传进入的不是浏览器的event对象的话,event变量为true.
  //如果data参数本身是娄组,那么第一个元素不是浏览器的event对象时为true.
  //对于event为true。即没有event传进入,先构建一个伪造的event对象存在data[0]。
  event=!data[0]||!data[0].preventDefault;
  //在没有传入event对象的情况下,构建伪造event对象。
  if(event){//存到数组中的第一个             ④
   data.unshift({type:type,target:elem,
      preventDefault:function(){},stopPropagation:
function(){},timeStamp:now() });
   data[0][expando]=true;//不需要修正伪造的event对象
   }
  data[0].type=type;//防止事件名出错
  //表现会进行事件注册函数的分类(命名空间)执行。不是所有的。
  if(exclusive)data[0].exclusive=true;
  
  //与prototype等传统的处理方式不一样,没有采用fireEvent来
  //来fire通过注册到浏览器事件中的事件处理方法。
  //这里分了三步,先fire通过jQuery.event.add来注册的事件,这个事件
  //有可能是自定义的事件(没有注册到浏览器事件中)。
  //第二步是fire通过elem.onclick方式注册的事件的本地处理函数
  //第三步是fire默认的事件处理方式(在本地的onclick的方式注册
  //不存在的情况下)。  
//这里是触发通过jQuery.event.add来注册的事件,
   varhandle=jQuery.data(elem,"handle");      ⑤
   if(handle)val=handle.apply(elem,data);//这里data分成多个参数
  //处理触发通过elem.onfoo=function()这样的注册本地处理方法,
  //但是是对于links"s.click()不触发,这个不会执行通过addEvent
  //方式注册的事件处理方式。     
  if((!fn||(jQuery.nodeName(elem,"a")&&type=="click"))⑥
   &&elem["on"+type]&&elem["on"+type].apply(elem,data)===false)
   val=false;
//额外的函数参数的开始几个是通过data给定的。这里会把伪造加上的event给去掉。
//它的最后一个参数是一系列的事件处理函数返回的结果,一般为bool值
//这个函数可以根据这个结果来处理一个扫尾的工作。
  if(event)data.shift();
//处理触发extra给定的函数处理。
  if(extra&&jQuery.isFunction(extra)){          ⑦
    ret=extra.apply(elem,val==null?data:data.concat(val));
    //如果这个函数有返回值,那么trigger的返回值就是它的返回值
    //没有的话就是串连的事件处理函数的最后一个返回值。一般为bool
   if(ret!==undefined) val=ret;
  }
  //触发默认本地事件方法,它是在没有如.onclick注册事件
  //加上前面的执行事件处理函数返回值都不为false的情况下,才会执行。
  //它还可以通donative来控制是否执行。
  //如form中可以采用this.submit()来提交form.
  if(fn&&donative!==false&&val!==false     ⑧
       &&!(jQuery.nodeName(elem,"a")&&type=="click")){
   this.triggered=true;
   try{elem[type](); //对于一些hidden的元素,IE会报错
     }catch(e){}
   }
  this.triggered=false;
  }
returnval;
},



  Jquery的fire事件的方法与prototype中实现是完全不一样的。Ext、YUI没有提供强迫触发事件的方法。对于一般的思维,程序来触发浏览器的事件就应该采用fireEvent或dispatchEvent方法来运行。

  但是jquery采用一种不同的方法。对于通过jquery.event.add来注册的事件(不管是自定义的还是注册到浏览器事件),它保存在一个与元素及事件名相对应的cache中。在浏览器的触发中,这个是没有什么作用。但是它是为了通过等程序来强迫触发时,从cache中取到对应的事件处理函数。这个时候就抛开了浏览器的事件。在这里还可以执行一些自定义的事件函数。如⑤处。

  对于通过html的标签中如click或elem.onclick=function(){}形式注册的事件函数。在⑥处它采用执行元素的如onclick形式的回调函数就可以。通过这种dom0的方式只能注册一个函数。

  有的时候,如果没有onclick这样的事件处理函数,浏览器会执行默认的处理函数。如form.submit()。⑧处可以看出对于这样的默认的事件处理,还可以通过参数donative来控制。

  程序手动强迫触发事件,有一点问题就是event是怎么生成,就是没有浏览器生成event传入到函数中。Prototype采用了是新生成的dataavailable的事件。这样的事件也没有什么作用。Jquery也采用fake的方式伪造一个一个事件,如④,它比prototype的事件好处在于它能通过trigger的函数的参数来传入需要的event。Prototype则不能。

  通过上面的分析,隐隐可以看出Jquery是通过模拟浏览器的触发事件的执行过程来构建这个trigger的函数的。先执行dom1方式(addEvent)注册的事件,再执行dom0方式注册的事件,最后看看要不要执行默认的事件处理。

  在⑦处,我们可以看出trigger还可能通过传入回调函数和参数来完成对执行的事件处理函数的结果进行判断处理,形成新结果通过trigger的函数返回。这在有的时候是很有用的。



  除了这些,它还能对于事件的处理函数进行分类(namespace),可以在合适的时候调用事件的不同分类的的处理函数(通过jquery.event.add来注册)。这个分类的处理在handle实现。
复制代码代码如下:
  handle:function(event){
   //返回undefinedorfalse
   varval,ret,namespace,all,handlers;
   //修改了传入的参数,这里是引用。
   event=arguments[0]=jQuery.event.fix(event||window.event);
   //命名空间处理
   namespace=event.type.split(".");
   event.type=namespace[0];
   namespace=namespace[1];
   //all=true表明任何handler,namespace不存在,同时
   //event.exclusive不存在或为假时,all=true.
   all=!namespace&&!event.exclusive;
   //找到元素的events中缓存的事件名的处理函数列表
   handlers=(jQuery.data(this,"events")||{})[event.type];
   for(varjinhandlers){//每个处理函数执行
     varhandler=handlers[j];
     //Filterthefunctionsbyclass
     if(all||handler.type==namespace){
       //传入引用,为了之后删除它们
       event.handler=handler;
       event.data=handler.data;//add的时候加上的
       ret=handler.apply(this,arguments);//执行事件处理函数
       if(val!==false)
        val=ret;//只要有一个处理函数返回false,本函数就返回false.
       if(ret===false){//不执行浏览器默认的动作
         event.preventDefault();
         event.stopPropagation();
       }
     }
   }
   returnval; }