7.客户端JavaScript笔记:事件

本系列内容由ZouStrong整理收录

整理自《JavaScript权威指南(第六版)》,《JavaScript高级程序设计(第三版)》

客户端JavaScript程序采用了异步、事件驱动的编程模型

事件就是文档或浏览器窗口发生的一些特定的交互瞬间(就是浏览器窗口或者页面上发生的那些事情,有些是自动的,例如页面加载完毕;有些是用户触发的,例如一次点击……)

事件监听器(事件处理程序)用于监听事件,以便事件发生时执行相应的操作

关于事件,有几个重要概念

  • 事件流:事件接收的顺序(事件冒泡和事件捕获)
  • 事件处理程序:响应事件的函数(也叫事件监听器)
  • 事件对象:与特定事件相关的对象
  • 事件类型:用来说明发生了什么事件
  • 事件目标:事件发生的目标元素
  • 事件委托:提高性能,并且对于后续JS添加的元素也能有效

事件最早出现在IE3,DOM2级事件规范标准化了一些行为(IE9及其它标准浏览器全都已经实现了“DOM2 级事件”模块的核心部分, IE8及其以下浏览器仍然使用专有事件机制,后详述)

一. 事件流

把手指放在一组同心圆的圆心上,手指指向的不仅仅是一个圆,而是所有的圆,同样的,当用户点击页面某个元素,点击也会发生在其父元素、祖先元素、甚至整个页面上….

事件流就是用来描述页面元素接收事件的顺序

1. 事件冒泡(event bubbling)流

事件冒泡由IE最先提出,即事件最开始由最具体的元素接收,然后逐级向上传播到其外层元素

例如,用户点击页面上唯一的一个div元素,则浏览器会认为我们先点击了div,后点击了body,再点击了html,再点击了document,最后点击了window

注:所有浏览器都支持事件冒泡,但 IE6、IE7、IE8 只冒泡到document,所以window.onclick在IE8及以下浏览器中始终没反应

2. 事件捕获(event capturing)流

事件捕获由Netscape最先提出,与事件冒泡相反,即事件最开始由最不具体的元素接收,然后逐级传播到其具体的元素

事件捕获的用意在于事件到达预定目标之前捕获它

注:IE6、IE7、IE8不支持事件捕获,因此很少使用事件捕获

3. DOM事件流

DOM2级事件规定的事件流包括三个阶段

  • 事件捕获阶段
  • 处于目标阶段
  • 事件冒泡阶段

首先发生的是事件捕获,为截获事件提供了机会;然后是实际的目标接收到事件;最后一个阶段是冒泡阶段

注:IE6、IE7、IE8不支持完整的DOM事件流(因为不支持事件捕获)

二. 事件处理程序(事件监听器)

事件处理程序即事件发生时响应该事件的函数,名字为 ”on+事件名”,并且全部小写

1. HTML级别事件处理程序

某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的HTML特性来指定,特性的值是可以执行的JavaScript代码(因此不能在其中使用未经转义的 HTML 语法字符)

<input type="" onclick="alert('strong');" />
<input type="" onclick="showSome();" />

注:该种方式添加的事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码

这样指定事件处理程序,会自动创建一个封装着元素特性的动态(隐形的)函数,所以可以直接使用this关键字,并且this指代当前元素

<input type="text" onclick="alert(this.type)" />    //"text"

所以,指定事件处理程序为一个函数时,函数中的this并不会指代当前元素(因为外部还有一个创建的隐藏的函数,因此自定义的函数成了内部的函数)

<p onclick="test();">sssss</p>     //"window"
<script>
	function test(){
		alert(this);
	}
</script>

可以将this作为函数参数传入就行了

<p onclick="test(this);">sssss</p>     //p元素对象
<script>
	function test(that){
		alert(that);
	}
</script>

动态(隐形的)函数中有一个局部变量event,通过event变量可以直接访问事件对象

<input type="" onclick="alert(event.type)" />  //"click" 

在这个动态创建的函数内部,可以直接访问document和该元素本身的成员,因为它扩展作用域的方式如下

function(){ 
	with(document){ 
		with(this){ 
		} 
	} 
}

如此,事件处理程序要访问自己的属性就简单多了

<input type="text" onclick="alert(type)" />    //"text"

并且如果当前元素是表单输入元素,则作用域还包含了访问form元素的入口

function(){ 
	with(document){ 
		with(this.form){
			with(this){ 
			} 
		} 
	} 
}

所有浏览器都支持HTML级别事件处理程序,但是缺点也很明显

  • 时差问题,元素一出现,用户就可能触发事件,但事件处理程序可能尚不具备执行条件,便会报错(可以通过封装在try-catch块中来解决)
  • 作用域链问题,作用域链在不同浏览器中会导致不同结果
  • 耦合问题, HTML与JavaScript代码紧密耦合

2. DOM 0级事件处理程序

每个元素(包括window和document)都有自己的事件处理程序属性(如onclick、onload),将这种属性的值设置为一个函数,就可以指定事件处理程序(全部小写)

div.onclick = function(){
};

注:事件处理程序被认为是元素的方法,因此this始终指向当前绑定事件的元素(不考虑闭包的话)

也可以删除通过DOM0级方法指定的事件处理程序

div.onclick = null;
//删除事件处理程序

使用DOM0级添加事件处理程序会覆盖同类型的HTML级别的事件处理程序,因此,这种方式同时也会删除通过HTML指定的事件处理程序

使用DOM 0级事件处理程序,优点很明显

  • 所有浏览器都支持
  • 解除了耦合
  • 明确了作用域链

但是,缺点也有

  • 时差问题,元素一出现,用户就可能触发事件,可能什么都不会执行(但不是报错)
  • 只能为同一个元素指定一个同类的事件处理程序(可通过函数封装解决)

    div.onclick = function(){
    func1();
    func2();
    }

这样的话,this的指向就不明确了,这样解决

div.onclick = function(){
	var that=this;
	func1();
	func2();
}

3. DOM 2级事件处理程序(IE6、7、8不支持 )

每个元素都可以通过 addEventListener() 方法添加事件处理程序,通过 removeEventListener() 方法移除事件处理程序

  • 第一个参数必须,String,事件名(不是事件处理程序名)
  • 第二个参数必须,Function,作为事件处理程序的函数
  • 第三个参数必须,Boolean,事件发生阶段(true表示在捕获阶段,false表示冒泡阶段)

    div.addEventListener("click" , function(){
    },false);

使用DOM2级事件处理程序,this同样指代当前元素

并且,不同于DOM 0级,可以同时为一个元素指定多个事件处理程序,并按照指定顺序依次触发

更神奇的是,通过addEventListener()添加事件不会覆盖使用前两种方式添加的事件,它们都会被执行

通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除,移除时传入的参数与添加处理程序时使用的参数相同,所以通过addEventListener()添加的匿名函数将无法移除

div.addEventListener("click", function(){ 
	alert(this.id); 
}, false); 
//没有用,因为是两个不用的函数,两个独立的对象永不相等
div.removeEventListener("click", function(){ 
	alert(this.id);
}, false); 

应该是这样

var handler = function(){ 
	alert(this.id); 
}; 

div.addEventListener("click", handler, false);
div.removeEventListener("click", handler, false);

4. IE中的事件处理程序(IE11开始已经不再支持)

在IE中每个元素都可以通过 attachEvent() 方法添加事件处理程序,通过 detachEvent()方法移除事件处理程序

  • 第一个参数必须,String,事件处理程序名(on...)
  • 第二个参数必须,Function,表示作为事件处理程序的函数

没有第三个参数,是因为IE早期版本只支持事件冒泡,所以所以通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段

div.attachEvent("onclick",function(){
});

同样,可以同时为一个元素指定多个事件处理程序,但是在IE6、7、8下以相反顺序触发

通过attachEvent ()添加的事件处理程序只能使用detachEvent()来移除,移除时传入的参数与添加处理程序时使用的参数相同,所以通过attachEvent ()添加的匿名函数将无法移除

还有最大的一点不同,前面讲到的几种方法,事件处理程序都是在元素的作用域下面执行,因此this指代元素,但在使用 attachEvent() 方法的情况下,事件处理程序会在全局作用域中运行,因此this 等于 window

小结

事件处理程序的返回值很重要

对于HTML级别事件处理程序和DOM 0级别事件处理程序来说,使用return false可以阻止事件的默认行为(对于DOM2级和IE事件处理程序不行,相见后)

对于beforeunload事件,返回值可以提示用户,是否重载(或离开)当前页面

三. 事件对象

在触发某个事件时,会产生一个事件对象event,该对象包含着所有与当前事件有关的信息 (事件的类型、发生事件的元素.....以及特定事件特有的信息)

要注意,只有在事件处理程序执行期间,event对象才会存在;一旦事件处理程序执行完成,event对象就会被销毁

1. DOM中的事件对象

使用HTML特性添加事件处理程序时,变量event中保存着event对象

<input type="button"  onclick="alert(event.type)" />

使用DOM 0级或者2级添加事件处理程序时,会将event对象事件处理程序的参数传入

div.onclick = function(event){ 
	alert(event.type);                //"click" 
}; 
div.addEventListener("click", function(event){ 
	alert(event.type);               //"click" 
}, false);

event对象包含的属性和方法与特定的事件有关,事件类型不一样,可用的属性和方法也不同,但所有event对象都有下列属性或方法(常用的)

  • event.type:(String)表示事件类型
  • event.bubbles:(Boolean)表示事件是否冒泡
  • event.cancelable:(Boolean)表示是否可以取消事件默认行为
  • event.defaultPrevented:(Boolean)表示是否阻止了默认行为(是否调用了preventDefault())
  • event.target:表示事件发生的真实目标元素
  • event.currentTarget:表示事件事件处理程序绑定的元素
  • event.preventDefault():表示阻止事件默认行为(只有cancelable属性设置为 true 的事件,才可以使用preventDefault()来取消其默认行为)
  • event.stopPropagation():表示阻止事件冒泡或者捕获

在事件处理程序中(不考虑闭包的情况下),this始终指代当前绑定事件的元素(调用函数的对象)(也就是currentTarget),而不一定是事件真实发生的元素,而target才指代事件发生的真实目标元素

如果直接将事件处理程序指定给了目标元素,则 this、 currentTarget 和 target 包含相同的值

2. IE中的事件对象(IE6、7、8)

使用HTML特性添加事件处理程序时,变量event中保存着event对象

在IE6、7、8中,使用DOM 0级添加事件处理程序时,event作为window对象的属性存在

div.onclick = function(){
	var event = window.event;
 		alert(event.type);	
};

使用attachEvent()添加事件处理程序时,event作为事件处理程序的参数传入

div.attachEvent("onclick",function(event){
     alert(event.type);
});

IE6、7、8下,事件对象的属性和方法有不同

  • event.type:(String)表示事件类型
  • event.cancelBubble:true为取消冒泡(与DOM中的stopPropagation()作用类似,但前者只能取消冒泡)
  • event.returnValue:false为取消默认行为(与DOM中的preventDefault()作用相同)
  • event.srcElement:事件发生的真实目标(与DOM中的target相同)

注:IE中,事件处理程序的作用域是根据指定它的方式来确定的,所以不能认为this会始终等于绑定事件的元素

小结

不管怎样,都可以通过这样取得事件对象

fnction handler(event){
	var event = event || window.event;
}

四. 事件类型

Web浏览器中可能发生的事件有很多类型

1. UI事件

  • load事件

当页面完全加载(所有外部文件,包括CSS、JS和图像等完全加载并显示)后在window上触发、当框架完全加载后在框架集上触发、当图像完全加载后在img元素上触发、当嵌入内容完全加载后在object元素上触发

  • unload事件

当页面完全卸载后在window上触发、当框架完全卸载后在框架集上触发、当嵌入内容完全卸载后在object元素上触发

在unload事件处理程序中可以保存用户的状态,但是不能阻止卸载事件的发生

  • beforeunload事件(HTML5规范,所有浏览器都支持)

当页面完全卸载之前在window上触发、当框架完全卸载之前在框架集上触发、当嵌入内容完全卸载之前在object元素上触发

beforeunload事件处理程序可以阻止卸载事件的发生,只要返回一个字符串即可,这样可以给用户确认的机会,以决定是不是要卸载当前页面

为了显示这个弹出对话框,必须将 event.returnValue 的值设置为要显示给用户的字符串(对IE 及 Fiefox 而言),同时作为函数的值返回(对 Safari 和 Chrome 而言)

event.returnValue = message;
return message;

刷新页面也会导致以上两个事件的发生

  • error事件

当发生JavaScript错误时在window上触发、当框架无法加载时在框架集上触发、当图像无法加载时在img元素上触发、当嵌入内容无法加载时在object元素上触发

与一般的处理程序不同,它的事件处理程序接收的不是事件对象,而是三个参数(表示错误信息、错误所在的文档的URL、错误发生的行数)

  • resize事件

当window窗口或者框架的大小变化时在window或者框架上触发

  • scroll事件

当滚动带滚动条的元素中的内容时,在该元素上面触发(body元素包含所加载页面的滚动条)

注:窗口每次变化或者每次滚动都会分别触发上面两个事件,切记不要在处理程序中加入大量需要计算的代码

  • focus事件和blur事件

浏览器窗口获得和失去焦点的时候,也会在window上触发这个事件

小结

一般来说,在window上面发生的任何事件都可以在body元素中通过相应的特性来指定(因为在HTML中无法访问window 元素),所有浏览器都能很好地支持这种方式

<body onload="alert('Loaded!')">

2. 表单事件

  • submit事件

当用户点击提交按钮时(也只有这样),form元素上就会触发submit事件

  • reset事件

当用户点击重置按钮时(也只有这样),form元素上就会触发reset事件

  • change事件

当用户改变表单元素的值时就会触发change事件,例如从下拉框中选择一个选项后就会触发;对于输入域来说,不是每次输入都会触发change事件,当且仅当用户改变了元素的值,并且元素失去焦点的时候才会触发;对于单选和复选按钮的话,click和change事件都会触发,并且后者更加有用(注:如果用户单击了其它单选按钮,而导致这个单选按钮状态变化,后者不触发change事件)

  • focus事件

当元素获得焦点时触发,不会冒泡(不只是用在表单上)

  • blur事件

当元素失去焦点时触发,不会冒泡(不只是用在表单上)

焦点事件会在页面元素获得或失去焦点时触发,利用这些事件并与document.hasFocus()方法及document.activeElement属性配合,可以知晓用户在页面上的行踪

  • input事件(IE8及其以下浏览器不支持)

不同于change事件,每次用户输入都会触发input事件(由于浏览器兼容情况有限,因此常常被键盘事件keypress、keydown、keyup等代替)

  • select事件

当用户选择文本框(input或textarea)中的字符后,在该元素上触发

3.鼠标与滚轮事件

鼠标事件最为常用,也最重要

  • click事件

当用户点击鼠标左键或者按下回车键时触发(通过回车键触发对于易访问性很重要)

  • contextmenu事件(HTML5规范,所有浏览器都支持)

右击鼠标,出现上下文菜单时触发,可以通过取消默认事件来自定义右键弹出菜单

  • dblclick事件

当用户双击鼠标左键时触发

  • mousedown事件

当用户按下任意鼠标按键时触发

  • mouseup事件

当用户释放任意鼠标按键时触发

  • mousemove事件

当用户在元素内部移动鼠标时反复触发

  • mouseover事件

当用户将鼠标移入元素时触发(冒泡,所以移入后代元素也算)

  • mouseout事件

当用户将鼠标移出元素时触发(冒泡,所以移出后代元素也算)

  • mouseenter事件

当用户将鼠标移入元素时触发(不冒泡,所以移入后代元素不触发)

  • mouseleave事件

当用户将鼠标移出元素时触发(不冒泡,所以移出后代元素不触发)

  • mousewheel事件

当用户滑动鼠标滚轮时触发(火狐不支持,它支持DOMMouseScroll事件,并且只能通过DOM 2级方式添加)

小结

除了mouseenter和mouseleave事件,所有鼠标事件都会冒泡

用户点击一次鼠标,事件发生的顺序如下:

  • mousedown
  • mouseup
  • click

只有在同一个元素上相继触发mousedown 和mouseup 事件,才会触发click 事件;类似地,只有触发两次click事件,才会触发一次dblclick事件;而mousedown和mouseup则
不受其他事件的影响

移动端“鼠标”事件

移动端(ios,Android)有单独的事件机制,但是鼠标事件大部分也是有效的,但要注意

  • 不支持dblclick事件,双击浏览器窗口会放大画面,而且没有办法改变该行为
  • 轻击可单击元素会触发 mousemove 事件。如果此操作会导致内容变化,将不再有其他事件发生;如果屏幕没有因此变化,那么会依次发生 mousedown、 mouseup 和 click 事件。轻击不可单击的元素不会触发任何事件。可单击的元素是指那些单击可产生默认操作的元素(如链接),或者那些已经被指定了 onclick 事件处理程序的元素
  • mousemove 事件也会触发 mouseover 和 mouseout 事件
  • 两个手指放在屏幕上且页面随手指移动而滚动时会触发 mousewheel和scroll事件

鼠标事件的event对象

event.button属性

表示事件发生时,按下的鼠标键是哪个(0表示主鼠标按钮, 1表示中间的鼠标按钮(鼠标滚轮按钮),2表示次鼠标按钮)

event.screenX属性 、event.screenY属性

表示事件发生时鼠标指针在屏幕中的水平和垂直坐标

event.clientX属性、event.clientY属性

表示事件发生时鼠标指针在浏览器窗口中的水平和垂直坐标

event.pageX属性、event.pageY属性(IE6、7、8不支持)

表示事件发生时鼠标指针在页面中的水平和垂直坐标(包含了页面滚动的距离,页面不滚动时,值与clientX和clientY相同)——IE6、7、8不支持pageX和pageY,可以使用clientX或Y,再加上滚动距离来模拟

event.shiftKey属性

表示鼠标事件发生时,是否按下了shift键

event.ctrlKey属性

表示鼠标事件发生时,是否按下了ctrl键

event.altKey属性

表示鼠标事件发生时,是否按下了alt键

event.metaKey属性(IE6、7、8不支持)

表示鼠标事件发生时,是否按下了window(微软)或者cmd键(苹果)

注:这些属性中包含的都是布尔值,如果相应的键被按下了,则值为true,否则值为false。当某个鼠标事件发生时,通过检测这几个属性就可以确定用户是否同时按下了其中的键

在mouseover和mouseout事件中,还有

event.relatedTarget属性(IE6、7、8不支持)

表示事件发生时的相关元素,对mouseover事件而言,相关元素就是那个失去光标的元素;对mouseout事件而言,相关元素则是获得光标的元素

IE6、7、8不支持,但是在mouseover事件触发时,fromElement属性中保存了相关元素;在mouseout事件触发时,toElement属性中保存着相关元素

event.detail属性(IE6、7、8不支持)

event对象中还提供了detail属性,用于给出有关事件的更多信息。对于鼠标事件来说, detail中包含了一个数值,表示在给定位置上发生了多少次单击,detail 属性从 1 开始计数,每次单击发生后都会递增。如果鼠标移动了位置,则 detail 会被重置为 0

在mousewheel事件中,还有

event.wheelDelta属性

当用户向前滚动鼠标滚轮时,wheelDelta是120的倍数(通常就是120);当用户向后滚动鼠标滚轮时,wheelDelta是-120的倍数(通常就是-120)

注:可用来放大缩小元素,但要注意阻止默认事件(即页面滚动)

Firefox支持名为DOMMouseScroll的滚轮事件,而有关鼠标滚轮的信息则保存在detail属性中,当向前滚动鼠标滚轮时,这个属性的值是-3的倍数,当向后滚动鼠标滚轮时,这个属性的值是3的倍数

4. 键盘与文本事件

用户在使用键盘时会触发键盘事件

  • keydown事件

当用户按下键盘上的任意键时触发(如果按住不放的话,会重复触发)

  • keypress事件

当用户按下键盘上的字符键时触发(如果按住不放的话,会重复触发)

  • keyup事件

当用户松开键盘上的按键时触发

  • textInput事件(IE6、7、8不支持)

当用户在可编辑区按下键盘上的字符键时触发(在文本插入文本框之前触发)

小结

用户按一次键盘上的字符键时,首先会触发keydown事件,然后紧跟着是keypress事件,最后会触发keyup事件;如果用户按一次键盘上的非字符键,首先会触发keydown事件,然后会触发keyup事件

textInput事件的用意是在将文本显示给用户之前更容易拦截文本,它奇葩的名字(大写)决定了该事件只能通过DOM2级事件方式添加

键盘事件的event对象

键盘事件与鼠标事件一样,键盘事件的事件对象中也有shiftKey、ctrlKey、altKey和metaKey属性(IE8以下不支持metaKey),此外还有一个

event.keyCode属性

按下或松开按键时,返回该键对应的键码

对于textInput事件,还有

event.data属性

表示用户输入的字符

移动端“键盘”事件

移动端(ios,Android)在使用屏幕键盘时会触发键盘事件

5. 设备事件、触摸与手势事件

详见移动端笔记

四. 事件模拟

事件通常由用户操作或浏览器自身来触发,也可以使用JavaScript在任意时刻来模拟触发特定的事件(此时的事件就如同浏览器创建的事件一样,该冒泡还会冒泡...而且能够触发指定的事件处理程序)

...暂未涉及...

五. 内存和性能

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的性能

  • 首先,每个函数都是对象,都会占用内存
  • 其次,必须事先指定所有事件处理程序,而导致DOM访问次数增加,影响性能

1. 事件委托(事件代理)

解决“事件处理程序过多”的方案之一就是事件委托

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

例如,click事件会一直冒泡到window层次(IE8以下是document层次),所以,我们可以为整个页面指定一个onclick事件处理程序,然后通过检测事件发生的目标来完成操作(例如检测target的id值),而不必给每个可单击的元素分别添加事件处理程序,因为前者消耗更低,因为只取得了一个DOM元素,只添加了一个事件处理程序

2. 移除事件处理程序

解决“事件处理程序过多”的方案之二就是及时移除不需要的事件处理程序

第一种情况就是从文档中移除带有事件处理程序的元素时(如果带有事件处理程序的元素被 innerHTML 删除了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收)

<div id="myDiv"> 
	<input type="button"  id="myBtn"> 
</div> 
<script> 
	var btn = document.getElementById("myBtn"); 
	btn.onclick = function(){ 
		document.getElementById("myDiv").innerHTML = "strong";
	}; 
</script> 

此时,事件处理程序仍然留在内存中,而不会被回收,应该手动移除

btn.onclick = function(){ 
	btn.onclick = null; //移除事件处理程序
	document.getElementById("myDiv").innerHTML = "strong"; 
}; 

注:在事件处理程序中删除按钮也能阻止事件冒泡,因为目标元素在文档中是事件冒泡的前提,采用事件委托也有助于解决这个问题。如果事先知道将来有可能使用innerHTML替换掉页面中的某一部分,那么就可以不直接把事件处理程序添加到该部分的元素中。而通过把事件处理程序指定给较高层次的元素,同样能够处理该区域中的事件

第二种情况就是卸载页面的时候,如果在页面被卸载之前没有清理干净事件处理程序,那它们就会滞留在内存中。每次加载完页面再卸载页面时(可能是在两个页面间来回切换,也可以是单击了“刷新”按钮),内存中滞留的对象数目就会增加,因为事件处理程序占用的内存并没有被释放

一般来说,最好的做法是在页面卸载之前,先通过onunload事件处理程序移除所有事件处理程序。在此,事件委托技术再次表现出它的优势——需要跟踪的事件处理程序越少,移除它们就越容易

注:只要是通过onload事件处理程序添加的东西,最后都要通过onunload事件处理程序将它们移除

你可能感兴趣的:(JavaScript)