JS原生事件

4,262 阅读10分钟


设计:jensen

1. 事件模型

JavaScript事件实现网页与用户之间交互,在各式各样的浏览器中,JavaScript事件模型主要分为3种:原始事件模型(DOM0)、DOM2事件模型、IE事件模型。

1.1 原始事件模型(DOM0级)

原始事件模型是所有浏览器都支持的事件模型,没有事件流,事件一旦触发会马上执行,有两种方式可以实现原始事件:

在html代码中直接指定属性值:

<button type='button' id="test" onclick="doSomeTing()"/>

在js代码中:

document.getElementsById("test").onclick = doSomeTing()

优点:所有浏览器都兼容。

缺点:逻辑与显示没有分离;相同事件的监听函数只能绑定一个,后绑定的会覆盖掉前面的;无法通过事件的冒泡、委托等机制处理事情。因为这些缺点,虽然原始事件类型兼容所有浏览器,但仍不推荐使用。

例子:基于DOM0的事件,对于同一个dom节点而言,只能注册一个,后边注册的同种事件会覆盖之前注册的。例如:

var btn = document.getElementById("test");
btn.onmousemove = function(e){  
   alert("ok");
};
btn["onmousemove"] = function(e){  
   alert("ok1");
};//输出OK1

关于this。事件触发时,this指向触发该事件dom对象上。例如:

var btn = document.getElementById("test");
btn.onmousemove = function(e){  
    alert(this.id);
};//结果输出test

注销事件:

var btn = document.getElementById("test");
btn.onclick = function(e){  
alert("ok");
};
btn.onclick = null;//最后注册的事件要覆盖之前的,最后一次注册事件设置成null,即是注销事件绑定。

1.2 DOM2事件模型

此模型是W3C制定的标准模型,现代浏览器(IE6~8除外)都已经遵循这个规范。W3C制定的事件模型中,一次事件的发生包含三个过程:事件捕获阶段;事件目标阶段;事件冒泡阶段。

事件捕获阶段:当某个元素触发某个事件,顶层对象document就会发出一个事件流,随着DOM树的节点向目标元素节点流去,直到到达事件真正发生的目标元素。在这个过程中,事件相应的监听函数是不会被触发的。

事件目标阶段:当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,就不执行。

事件冒泡阶段:从目标元素开始,往顶层元素传播。途中如果有节点绑定了相应的事件处理函数,这些函数都会被一次触发。

所有的事件类型都会经历事件捕获但是只有部分事件会经历事件冒泡阶段。不支持事件冒泡的,例如:blur,error,focus,load,mouseenter,mouseleave,resize,unload等。

阻止冒泡方法,使用stopPropagation()方法;

标准事件监听器该如何绑定:

element.addEventListener(event, function, useCapture)

其中event指定事件名,注意不要加‘on’前缀,与IE下不同;第二个参数是指定事件触发时执行的函数;第三个参数指定事件是否在捕获或冒泡阶段执行,true 事件句柄在捕获阶段执行,false(默认)事件句柄在冒泡阶段执行。

解除监听器:

removeEventListner(event, function, useCapture);

注意:通过removeEventListner()移除时传入的参数与绑定时使用的参数相同。通过addEventListener()添加的匿名函数无法移除,比如:

var btn = document.getElementById("test");

btn.addEventListener("click", function () {   
    alert(this.id);
}, false);

btn.removeEventListener("click", function () {  
   //无效!
   alert(this.id);
},false);

在上面例子中,removeEventListener无法删除addEventListener所绑定的事件,是因为两个方法并不相等,内存地址不同的,应是下面的方法处理:

 var btn = document.getElementById("test"); 
 var handler = function () {        
     alert(this.id);     
 }; 
 btn.addEventListener("click", handler, false); 
 btn.removeEventListener("click", handler, false);

1.3 IE事件模型

IE事件模型,注册事件方法:

<button id="btn">点击</button>
<script type="text/javascript"> 
   var target = document.getElementById("btn");  

   target.attachEvent('onclick',function(){       
     alert("我是button");  
   });
</script>

与之对应的事件的移除事件方法:detachEvent();

获得event对象,e = window.event(IE中的event对象是个全局属性)。

阻止事件冒泡的方法:event的cancelBubble属性为true。

e.cancelBubble = true;

阻止默认事件发生:event对象的returnValue属性为false即可,

e.returnValue = false;

2. 事件执行顺序

//直接看例子:
<div id="outside" class="outside">   
  <div id="inner" class="inner">
  </div>
</div>

  • 在外层div上注册两个click事件,分别是捕获事件和冒泡事件,代码如下:

var btn = document.getElementById("outside");
//捕获事件
btn.addEventListener("click", function(e){  
  alert("ok1");
}, true);

//冒泡事件
btn.addEventListener("click", function(e){  
  alert("ok");
}, false);

点击内层的div,先弹出ok1,后弹出ok。捕获事件先执行,然后执行冒泡事件。

  • 在目标元素上注册捕获事件和冒泡事件。

var btnInner = document.getElementById("inner");
//冒泡事件
btnInner.addEventListener("click", function(e){  
  alert("ok");
}, false);

//捕获事件
btnInner.addEventListener("click", function(e){  
  alert("ok1");
}, true);

点击内层div,结果是先弹出ok,再弹出ok1。因为我们是在目标元素绑定事件,是捕获事件的终点,是冒泡事件的起点,所以这里哪个先注册,就先执行哪个。

  • 注册多个冒泡事件,执行顺序将按照注册的顺序来,先注册先执行。例如:

var btnInner = document.getElementById("inner");
btnInner.addEventListener("click", function(e){  
  alert("ok");
}, false);

btnInner.addEventListener("click", function(e){  
  alert("ok1");
}, false);

btnInner.addEventListener("click", function(e){  
  alert("ok2");
}, false);

结果依次弹出ok、ok1、ok2。

  • 外层div和内层div同时注册了捕获事件,那么点击内层div时,外层div的事件一定是先触发的,代码如下:

var btn = document.getElementById("outside");
var btnInner = document.getElementById("inner");
btnInner.addEventListener("click", function(e){  
  alert("ok");
}, true);

btn.addEventListener("click", function(e){  
  alert("ok1");
}, true);

结果是先弹出ok1。

同理外层div和内层div都是注册的冒泡事件,点击内层div时,一定是内层div事件先执行。

3. 事件委托

事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

3.1 为什么要用事件委托

当dom元素需要事件处理程序,我们一般都会直接给它绑定事件处理程序,但是如果有很多的dom需要添加事件处理呢?比如我们有100个li,每个li都有相同的click点击事件,可能我们会用for循环的方法,来遍历所有的li,然后给它们添加事件,那这么做会存在什么影响呢?

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因;如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;

每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差,比如上面的100个li,就要占用100个内存空间,如果是1000个,10000个呢,如果用事件委托,那么我们就可以只对它的父级这一个对象进行操作,这样我们就需要一个内存空间就够了,自然性能就会更好。

3.2 事件委托的原理

事件委托是利用事件模型中的冒泡原理来实现的,就是事件从目标节点开始,然后逐步向上传播事件,举个例子:页面上有这么一个节点树,div>ul>li>a;如果给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件。

3.3 事件委托怎么实现

先看下例子,实现功能点击li,弹出123:

<ul id="ul">    
  <li>111</li>    
  <li>222</li>    
  <li>333</li>    
  <li>444</li>
</ul>

常规的方法就是遍历每个元素添加事件,但是这种方法频繁操作dom,影响性能。

window.onload = function(){    
  var oUl = document.getElementById("ul");    
  var aLi = oUl.getElementsByTagName('li');    
  for(var i=0;i<aLi.length;i++){        
    aLi[i].onclick = function(){            
       alert(123);        
    }    
   }
}

下面用事件委托的方式优化下:

window.onload = function(){    
   var oUl = document.getElementById("ul");    
   oUl.onclick = function(){        
     alert(123);    
   }
}

这里在父级ul添加事件处理,当li被点击时,由于冒泡原理,事件就会向上冒泡到父级ul上,因为ul上有点击事件,所以事件就会触发,这里当点击ul的时候也会触发。但是如果只有点击li才会触发呢?

Event对象提供了一个属性叫target,可以返回事件的目标节点,也叫事件源,也就是说,target就可以表示为当前的事件操作的dom。当然,这个是有兼容性的,标准浏览器用ev.target,IE浏览器用event.srcElement,此时只是获取了当前节点的位置,并不知道是什么节点名称,这里我们用nodeName来获取具体是什么标签名,注意这个返回值的是一个大写的。

window.onload = function(){     
   var oUl = document.getElementById("ul");     
   oUl.onclick = function(ev){         
      var ev = ev || window.event;         
      var target = ev.target || ev.srcElement;         
      if(target.nodeName.toLowerCase() == 'li'){              
         alert(123);            
         alert(target.innerHTML);        
      }     
   }
}

现在讲的都是document加载完成的现有dom节点下的操作,那么如果是新增的节点,新增的节点会有事件吗?首先看下常规的添加节点的方法:

<input type="button" name="" id="btn" value="添加" />
<ul id="ul1">    
  <li>111</li>    
  <li>222</li>    
  <li>333</li>    
  <li>444</li>
</ul>

假如现在是要做一个这种效果,移入li变红,移出li变白,然后点击按钮,可以向ul中添加一个li子节点。

window.onload = function(){      
  var oBtn = document.getElementById("btn");      
  var oUl = document.getElementById("ul");      
  var aLi = oUl.getElementsByTagName('li');      
  var num = 4;                
  function mHover () {          
    //鼠标移入变红,移出变白          
    for(var i=0; i<aLi.length;i++){              
       aLi[i].onmouseover = function(){                  
          this.style.background = 'red';                  
       };              
       aLi[i].onmouseout = function(){                  
          this.style.background = 'white';              
       }          
    }      
  }     
  mHover ();     

//添加新节点      
oBtn.onclick = function(){          
    num++;          
    var oLi = document.createElement('li');          
    oLi.innerHTML = 111*num;         
    oUl.appendChild(oLi);         
    mHover ();      
 }; 
}

以上方法功能实现,但实际上又增加了一次dom操作,影响优化性能,试下事件委托的方式。

window.onload = function(){      
  var oBtn = document.getElementById("btn");      
  var oUl = document.getElementById("ul");      
  var aLi = oUl.getElementsByTagName('li');      
  var num = 4;           
  //事件委托,添加的子元素也有事件      
  oUl.onmouseover = function(ev){          
     var ev = ev || window.event;         
     var target = ev.target || ev.srcElement;          
     if(target.nodeName.toLowerCase() == 'li'){              
       target.style.background = "red";        
     }      
  };      

 oUl.onmouseout = function(ev){          
     var ev = ev || window.event;          
     var target = ev.target || ev.srcElement;          
     if(target.nodeName.toLowerCase() == 'li'){                
        target.style.background = "white";          
     }       
 };                  

 //添加新节点      
oBtn.onclick = function(){          
   num++;          
   var oLi = document.createElement('li');          
   oLi.innerHTML = 111*num;          
   oUl.appendChild(oLi);      
};  
}

之前面试的时候遇到过原生事件执行顺序的笔试题,以上三部分总结了事件模型,事件执行顺序,事件委托,基本可以明白此类问题。



                                                                 --END--


未完待续......

最后,祝愿大家身体健康,常洗手,多通风。

欢迎关注GitHub:github.com/wlzhangYes

一点一滴积累,一步一步前进。分享工作中遇到的问题和日常琐事,欢迎关注公众号:南山zwl。


                                    公众号                                                   小程序