1 标准浏览器中的DOM2级事件绑定
- addEventListener绑定
- 代码:
oDiv.addEventListener("click",fn,false)
- 代码:
- 兼容性:标准浏览器(包括IE9,10浏览器)支持,IE8及其以下浏览器不支持;
- 特点:
- 执行顺序:按“绑定”的先后顺序进行执行;
- 绑定后触发执行函数中的this指向当前元素;
- 如果同一个元素,同一个行为绑定多次同一个方法,那么实际上只执行一次;
- removeEventListener解绑
2 IE浏览器中的DOM2级事件绑定
- attachEvent绑定
- 代码:
oDiv.attachEvent("onclick",fn)
- 代码:
- 兼容性:IE10及其以下浏览器支持,其他浏览器不支持;
- 特点:
- 执行顺序:不按着绑定顺序执行,为乱序;
- 绑定后触发执行函数中的this指向window;
- 如果同一个元素,同一个行为绑定多次同一个方法,执行多次;
- detachEvent解绑
3 解决IE浏览器中绑定事件attachEvent的问题
- 解决this指向和重复绑定问题
- 思路:
- 解决this指向就是给detachEvent方法中绑定一个匿名函数,匿名函数通过call改变fn的this指向,并且通过匿名函数来解决立即执行问题,但是会出现问题:在解除绑定时,不能解除匿名函数;
- 新建一个变量,将匿名函数赋值给它,然后在解除绑定时就可以解除绑定,但是又会出现问题:绑定多个函数时,变量会被重复赋值,所以在解除绑定时,无法拿到指定的函数
- 所以需要新建一个数组,作为自己的事件池,将每次新建的变量,插入到数组中,解除绑定时,就从数组中拿指定元素解除即可,但是又会出现问题:因为匿名函数都一样,无法确定谁是谁,所以可以给每个匿名函数添加一个静态属性name,将绑定的函数名赋值给它,这样就可以通过判断数组中每一项的静态属性就可以找到对应的函数,然后解绑;
- 解决重复绑定问题:在传入fn后,先在自己的事件池数组中判断是否已经存在此函数地址,通过静态属性判断,如果存在,阻止程序执行,这样就避免重复绑定问题;
- 系统事件池与自己的事件池必须一一对应,自己的事件池相当于一个屏障,在自己的事件池中过滤,然后赋值给系统事件池;需注意的是在解除绑定时,要先解除,后删除自己事件池中的对应项,如果不删除,会在重复绑定判断时,阻止执行,即无法再次绑定此函数;
- 代码:
function on(ele,type,fn) { if(ele.addEventListener){//标准浏览器 ele.addEventListener(type,fn,false); }else{//IE浏览器 var tmpFn=function () { fn.call(ele);//此时匿名函数中的this为window,不再是元素,因为attachEvent绑定的事件触发时,里面的this为window; }; tmpFn.name=fn;//给tmpFn函数添加静态属性 if(!ele.tempFn){ ele.tempFn=[]; } //避免重复绑定,需要对tmpFn进行过滤,如果在ele.tempFn中已经存在了,就阻断程序; for(var i=0; i<ele.tempFn.length; i++){ if(ele.tempFn[i].name==fn) return; } ele.tempFn.push(tmpFn); ele.attachEvent("on"+type,tmpFn);//attachEvent的弊端就是会重复绑定,重复执行; } } function off(ele,type,fn) { var ary=ele.tempFn;//等号是赋值的,将ele.tempFn的地址赋给ary,所以在改变ary时,其实是在改变地址中的东西,ele.tempFn也会跟着改变; if(ele.removeEventListener){ ele.removeEventListener(type,fn) }else{ for(var i=0;i<ary.length;i++){ if(ary[i].name==fn){ ele.detachEvent("on"+type,ary[i]); ary.splice(i,1);//解除方法后,在自己的事件池中也要删除,二者一一对应; break;//阻断循环 } } } }
3 事件库封装
- 目的:在标准浏览器下使用addEventListener绑定事件,不会出问题;在IE浏览器下,用attachEvent绑定事件会出问题,存在以上三个问题,所以需要封装一个事件库,解决这三个问题;
- 核心思想:在元素上创建一个自定义属性,初始值为一个空数组,作为自己创建的事件流,里面主要存储绑定的不同函数;
- 思路:主要分为三步,绑定,执行,解绑
- 绑定:on(ele,type,fn)-实质为创建自己的事件池
- 区分浏览器
- 给ele身上创建一个私有属性,赋值为空数组,将传入的不同的函数fn,用push插入到数组中;
- 将执行函数run绑定在系统事件池上,只绑定一次,要保证触发事件行为时,保证run函数中的this为元素;
- 解决第一个重复绑定问题:判断传入的fn,是否在自己的事件池中存在,如果存在,则阻止程序执行,不插入到数组,就不会重复执行;
- 执行:run()-实质为执行事件池数组中的每一项函数
- 分析:在标准浏览器下,绑定事件后执行函数,函数中的this为元素,而且会向函数中传入一个实参,为事件对象;
- 模仿:获取事件对象,遍历事件池数组,执行每一项,执行每一项时,此时函数中的this为数组对象,多以需要用call改变函数里面的this指向;
- 在执行数组每一项时,要判断其数据类型,如果不为函数,则为null,则删除,但是要注意i的塌陷问题,null是在off解绑时产生的;
- 解绑:off(ele,type,fn)-实质为将事件池数组中与被解绑函数对应的一项赋值为null;
- 遍历事件池数组,判断与fn对应的一项,将其赋值为null,设置break,为了阻止for循环继续执行,性能优化;
- 绑定:on(ele,type,fn)-实质为创建自己的事件池
- 知识点:
- IE浏览器下,绑定事件attachEvent方法的三大缺点:this,重复执行,乱序执行;
- attachEvent绑定时,方法为匿名函数时,匿名函数中的this为window;
- IE浏览器与标准浏览器中事件对象上属性的兼容设置
//事件源 e.target=e.srcElement; //pageX,pageY e.pageX=(document.documentElement.scrollLeft || document.body.scrollLeft)+e.clientX; e.pageY=(document.documentElement.scrollTop || document.body.scrollTop)+e.clientY; //阻止默认事件 e.preventDefault=function () { e.returnValue=false; }; //阻止冒泡 e.stopPropagation=function () { e.cancelBubble=true; };
- 代码:
var a=this["myEvent"+e.type]
- 1)等号作用是赋值,因为
this["myEvent"+e.type]
为一个数组,所以是将数组对应的地址赋值给a,即,数组和a同指向一个空间地址,地址中的内容为键值对数组元素;当改变a时,事件池数组也会同步改变; - 2)
e.tyepe
目的是想获取on中传入的type实参,所以用事件对象上的type属性来获取事件行为;
- 1)等号作用是赋值,因为
- 在off函数中,将事件池数组中与被解绑函数对应的一项赋值为null,而不是删除此项,因为如果删除此项,会在run函数中,数组遍历执行时,会出错;
- 分析:如果用on绑定了6个函数,得到的事件池数组为[fn1,fn2,fn3,fn4,fn5,fn6],但是在fn3函数体中,代码为解绑fn1,fn2,所以在run函数中遍历事件池数组中,当i值为2时,开始执行fn3函数,函数执行完会解绑fn1,fn2,所以此时的事件池数组为[fn3,fn4,fn5,fn6],然后i值变为3,开始找事件池数组索引为3的一项,即fn6,执行fn6,此时就会出错,fn4,fn5函数没有执行;所以不能删除此项,将其赋值为null,就不会出现数组长度变化,按照正常情况进行遍历执行函数,然后再次绑定事件时,到达遍历执行时,会将fn1,fn2转变为的null,删除掉,不会再执行;
- 三个函数中的变量a,不是同一个变量,均为各自函数的私有变量,但是三者的地址均为同一个地址,也是事件池数组地址,改变其中任何一个a,都是改变地址中的数组元素,三个函数中的a都会变化;
- 删除数组一项后,注意i的塌陷问题,设置
i--
来防止数组塌陷; - 在off函数中,将满足情况的一项赋值为null,然后设置break,结束for循环,旨在性能优化;
- 代码:
- JS代码:
function on(ele,type,fn) { if(ele.addEventListener){//标准浏览器 ele.addEventListener(type,fn,false); }else{//IE浏览器 if(!ele["myEvent"+type]){ ele["myEvent"+type]=[]; //将run绑定到系统的事件流上,只绑定一次,避免重复绑定 ele.attachEvent("on"+type,function () {//此时匿名函数中的this为window; run.call(ele);//保证run中的this为元素; }) } var a=ele["myEvent"+type]; //去重,当绑定重复的方法时,阻断程序,不往自己的事件流中放; if(a.length){//如果长度为0,转化为布尔值,为假 for(var i=0; i<a.length; i++){ if(a[i]==fn) return; } } a.push(fn);//将所有不重复的方法,添加到a数组中,即放在自己的事件流上; } } //run的目的是,执行函数 function run() { var e=window.event;//此时获取事件对象是为了传参和获取事件对象上的type属性,获取事件行为; //下面代码是,设置兼容处理,为以后使用时,在所有浏览器中都兼容; e.target=e.srcElement; e.pageX=(document.documentElement.scrollLeft || document.body.scrollLeft)+e.clientX; e.pageY=(document.documentElement.scrollTop || document.body.scrollTop)+e.clientY; //阻止默认事件 e.preventDefault=function () { e.returnValue=false; }; //阻止冒泡 e.stopPropagation=function () { e.cancelBubble=true; }; var a=this["myEvent"+e.type]; if(a.length){ //遍历数组a,让每一项执行,注意在函数执行时,要保证函数中的this为元素,而且传入一个事件对象实参; for(var i=0; i<a.length; i++){ //此时判断数组中元素的类型,为null,是解绑时产生的,所以需要删除掉,但是要注意i的塌陷; if(typeof a[i]==="function"){ a[i].call(this,e);//此时未改变时,this为a数组; }else{ a.splice(i,1);//删掉null; i--;//避免塌陷; } } } } //off的目的是解绑:就是在自己创建的事件流上查找与需解绑的fn相同的,将其赋值为null,不能删除,如果删除的话,会出错; function off(ele,type,fn) { if(ele.removeEventListener){ ele.removeEventListener(type,fn,false) }else{//IE浏览器 var a=ele["myEvent"+type]; if(a.length){ for(var i=0; i<a.length; i++){ if(a[i]===fn){ //将解绑的函数赋值为null a[i]=null; break;//停止for循环,性能优化; } } } } }
- 执行代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>事件库封装</title> <style> div{ width: 200px; height: 200px; background-color: red; } </style> </head> <body> <div id="div1"></div> <script src="03事件库封装.js"></script> <script> var oDiv=document.getElementById("div1"); function fn1() {console.log(this===oDiv)} function fn2(e) {console.log(e.clientX,e.clientY)} function fn5() {console.log(5)} function fn4() {console.log(4)} function fn3(){ off(oDiv,"click",fn1); } on(oDiv,"click",fn1); on(oDiv,"click",fn2); on(oDiv,"click",fn3); on(oDiv,"click",fn4); on(oDiv,"click",fn5); on(oDiv,"click",fn5); //结果为:ture 12,23 4 5 5; </script> </body> </html>
4 多级菜单实例
- 页面结构:
- ul下多个li,每个有内容的li下面那么就存在一个p元素和一个ul元素,p元素是为了点击事件触发的,ul是做显示和隐藏的,p中的em设置背景图,来设置加号,当隐藏时,改变背景图的定位,来呈现减号标志;
- 思路:
- 通过事件委托将点击事件,绑定在最高级容器中,通过获取事件源来进行相应操作;
- 判断事件源为p元素或是em元素时,判断p元素的父级下的儿子的状态
- 如果为block,则将此父级下的子孙级ul进行隐藏处理,将子孙级em改变背景图定位,标志为加号;
- 如果为none,则将此父级下的儿子级ul进行显示处理,将点击的p下的儿子em改变背景图定位,标志为减号;
- 知识点:
- 页面结构设置中,em通过定位来脱离文档流,通过top值让其居中,然后p添加paddingLeft值,然后给em腾出位置;
- 事件委托思想;
- jQuery中children()指的是获取儿子级的元素,find()指的是获取子孙级的元素;
- 原生JS与jQuery之间的转换;
- 代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>多级菜单</title> <style> *{ margin: 0; padding: 0; list-style: none; -webkit-user-select: none; } body{ padding: 10px; } #wrap{ width: 500px; background: -webkit-linear-gradient(top,lightseagreen,lightcoral,lightcoral,lightslategray); padding: 10px; font-size: 18px; } #wrap li{ line-height: 30px; position: relative; } #wrap li p{ padding-left: 28px; } #wrap li p:hover{ cursor: pointer; } #wrap li em{ position: absolute; top: 3px; left: 0; width: 24px; height: 24px; border-radius: 6px; background: url("img/1.jpg") no-repeat; background-position: -5px -19px; /*background-position: -33px -19px;*/ } .two,.three,.four{ display: none; } .two{ margin-left: 50px; } .three{ margin-left: 50px; } .four{ margin-left: 30px; } </style> </head> <body> <ul id="wrap"> <li><p><em></em>第一级第1个</p> <ul class="two"> <li>第二级第1个</li> <li><p><em></em>第二级第2个</p> <ul class="three"> <li>第三级第1个</li> <li><p><em></em>第三级第2个</p> <ul class="four"> <li>第四级第1个</li> <li>第四级第2个</li> <li>第四级第3个</li> <li>第四级第4个</li> <li>第四级第5个</li> </ul> </li> <li>第三级第3个</li> <li><p><em></em>第三级第4个</p> <ul class="four"> <li>第四级第1个</li> <li>第四级第2个</li> <li>第四级第3个</li> <li>第四级第4个</li> <li>第四级第5个</li> </ul> </li> </ul> </li> <li>第二级第3个</li> <li><p><em></em>第二级第4个</p> <ul class="three"> <li>第三级第1个</li> <li><p><em></em>第三级第2个</p> <ul class="four"> <li>第四级第1个</li> <li>第四级第2个</li> <li>第四级第3个</li> <li>第四级第4个</li> <li>第四级第5个</li> </ul> </li> <li>第三级第3个</li> <li><p><em></em>第三级第4个</p> <ul class="four"> <li>第四级第1个</li> <li>第四级第2个</li> <li>第四级第3个</li> <li>第四级第4个</li> <li>第四级第5个</li> </ul> </li> </ul> </li> </ul> </li> <li>第一级第2个</li> <li><p><em></em>第一级第3个</p> <ul class="two"> <li>第二级第1个</li> <li><p><em></em>第二级第2个</p> <ul class="three"> <li>第三级第1个</li> <li><p><em></em>第三级第2个</p> <ul class="four"> <li>第四级第1个</li> <li>第四级第2个</li> <li>第四级第3个</li> <li>第四级第4个</li> <li>第四级第5个</li> </ul> </li> <li>第三级第3个</li> <li><p><em></em>第三级第4个</p> <ul class="four"> <li>第四级第1个</li> <li>第四级第2个</li> <li>第四级第3个</li> <li>第四级第4个</li> <li>第四级第5个</li> </ul> </li> </ul> </li> <li>第二级第3个</li> <li><p><em></em>第二级第4个</p> <ul class="three"> <li>第三级第1个</li> <li><p><em></em>第三级第2个</p> <ul class="four"> <li>第四级第1个</li> <li>第四级第2个</li> <li>第四级第3个</li> <li>第四级第4个</li> <li>第四级第5个</li> </ul> </li> <li>第三级第3个</li> <li><p><em></em>第三级第4个</p> <ul class="four"> <li>第四级第1个</li> <li>第四级第2个</li> <li>第四级第3个</li> <li>第四级第4个</li> <li>第四级第5个</li> </ul> </li> </ul> </li> </ul> </li> <li>第一级第4个</li> </ul> <script src="jquery.js"></script> <script> //事件委托给wrap添加点击事件 $("#wrap").click(function (e) { e=e||window; e.target=e.target||e.srcElement; //通过获取事件源来判断是点击的哪个元素 if(e.target.tagName.toLowerCase()==="p" || e.target.tagName.toLowerCase()==="em"){ //判断当前点击的p的时候,让其父级下的子孙ul都隐藏 var op=e.target; //如果事件源为em时,op要赋值为其父级 if(op.tagName.toLowerCase()==="em"){ op=$(op).parent().get(0); } var $parent=$(op).parent(); //获取儿子级ul和em var $asonUl=$parent.children("ul"); //获取儿子级的em,要在op下获取子级,在$parent下是孙子级 var $asonEm=$(op).children("em"); //获取子孙级ul和em var $aUl=$parent.find("ul"); var $aEm=$parent.find("em"); if($aUl[0].style.display=="block"){ //$aUl[0].style.display获取的是行内样式,而在style中设置不是行内样式,所以第一次获取时,获取的不是none,而是空; //隐藏时,让点击的p的父级下的子孙ul都隐藏,让子孙em都改变背景 $aUl.css("display","none"); $aEm.css("backgroundPosition","-5px -19px"); }else{ //显示时,让点击的p的父级下的儿子ul显示,孙子不显示,让儿子em改变背景,让孙子不改变 $asonUl.css("display","block"); $asonEm.css("backgroundPosition","-33px -19px"); } } }) </script> </body> </html>