这篇随笔,深恶痛绝,敲到快结束的时候,凌晨00:19,突然闪退,也不知道是Mac的原因还是chrome的原因,重新打开的时候,以为自动保存有效果,心想没关系,结果他么的只保存了四分之一,WTF?!!!!还得重新继续敲,所以提醒各位笔者,永远不要相信所谓的时时保存,敲一点记得保存一点!坑!!!
一,概念梳理
1,事件:就是文档或浏览器窗口中发生的一些特定的交互瞬间;
2,事件流:描述的是从页面中接收事件的顺序;IE团队提出的是事件冒泡流,Netscape团队提出的是事件捕获流;
3,事件冒泡:事件开始时由最具体的元素(文档中嵌套最深的节点)接收,然后逐级向上传播到较为不具体的节点(window对象);
4,事件捕获:不太具体的节点应该更早接受到事件,而最具体的节点应该最后接收到事件,事件捕获的用意在于在事件到达预订目标之前就捕获它。尽管DOM2级事件规范要求事件应该从document对象开始传播,但这些浏览器都是从window对象开始捕获事件的。由于老版本的浏览器不支持,所以很少有人用事件捕获,有特殊需要时再用。
5,DOM事件流:DOM2级事件规定事件流包含三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。(IE8及更早的版本不支持DOM事件流)
二,事件处理程序
1,定义及命名
事件:用户或浏览器自身执行某种动作,名字:click、load、mouseover等这类,不带on;
事件处理程序(或事件侦听器):响应某个事件的函数,名字都以on开头,on+事件名。
2,指定方式
1⃣️HTML事件处理程序:
就是把onclick="fn()"写在HTML标签里,当做一个属性,如<input type="text" onclick="fn()" />
2⃣️DOM0级事件处理程序:
就是将一个函数赋值给一个事件处理程序属性,如:
var btn = document.getElementById('mybtn'); btn.onclick = function (){ alert('Clicked') };
btn.onclick = null; // 删除事件处理程序
⚠️:DOM0级所有浏览器的所有版本都支持,包括IE
3⃣️DOM2级事件处理程序:
用于处理指定和删除事件处理程序的操作,addEventListener()和removeEventListener(),接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值,如果为true,表示在事件捕获阶段调用事件处理程序,如果是false,则在冒泡阶段调用。
移除事件处理程序:
var btn = document.getElementById('myBtn'); btn.removeEventListener('click', function(){ alert('Clicked') }, false);
这样是没效果的,因为第二个参数传递的不是同一个事件处理程序函数,所以必须引用同一个:
var handler = function (){ alert('Clicked') }; btn.addEventListener('click', handler, false); .... // 这里省略中间代码 btn.removeEventListener('click', handler, false);
这样才有效果,所以如果传的是匿名函数,那没办法移除
⚠️:DOM2级里面有事件捕获,所以只有IE9以及其他浏览器的各个版本支持,IE8及以前不支持这个,IE8及之前版本只支持事件冒泡
4⃣️IE事件处理程序:
两个类似的方法attachEvent()、detachEvent(),只传两个参数:事件名(这里是不加on的)、事件处理程序函数,由于IE8及之前版本只支持事件冒泡,所以默认冒泡阶段添加,就不需要第三个参数。
在IE中,使用DOM0级方法和attachEvent()的区别是事件处理程序的作用域不同,前者是当前元素的作用域,后者是window全局,所以this指代的也不同。
和addEventListener方法类似,attachEvent也能为同一个元素添加多个事件处理程序,但是区别是后者执行顺序颠倒过来的,后添加的先执行,前者是正常顺序。
5⃣️跨浏览器的事件处理程序:
addHandler和removeHandler,三个参数:要操作的元素、事件名、事件处理程序函数
var EventUtil = { addHandler: function(element, type, handler){ if (element.addEventListener) { element.addEventListener(type, handler, false); } else if (element.attachEvent) { element.attachEvent('on'+type, handler); } else { element['on'+type] = handler; } }, removeHandler: function(element, type, handler){ if (element.removeEventListener) { element.removeEventListener(type, handler, false); } else if (element.detachEvent) { element.detachEvent('on'+type, handler); } else { element['on'+type] = null; } } }
⚠️:为什么要先检测DOM2级,因为主流浏览器都支持DOM2,如果DOM2不支持,说明是IE8及以前的,那么就用IE事件处理程序,如果再不行,就用DOM0级,不过一般不会用到DOM0级
三,事件对象
1⃣️DOM中的事件对象
兼容DOM的浏览器会将一个event对象传入到事件处理程序中,无论使用DOM0级还是DOM2级的方法:
btn.addEventListener('click', function(event){ ... }, false); btn.onclick = function(event){ ... }
event对象的一些属性和方法:
target:事件的目标
currentTarget:其事件处理程序当前正在处理事件的那个元素
preventDefault():取消事件的默认行为
stopPropagation():阻止事件进一步的冒泡或捕获
========
这里补充一下上面target和currentTarget的区别:
target在事件流的目标阶段,而currentTarget则在事件流的捕获阶段、目标阶段或冒泡阶段
event.currentTarget指向事件所绑定的元素,而event.target始终指向事件发生时的元素。
下面一个例子来说明:
var btn = document.getElementById('myBtn'); btn.onclick = function(event){ alert(event.currentTarget === this); // true alert(event.target === this); // true }; document.body.onclick = function(event){ alert(event.currentTarget === document.body); // true alert(document.body === this); // true alert(event.target === document.getElementById('myBtn')); // true };
========
2⃣️IE中的事件对象
DOM0级方法,event作为window对象的一个属性,而attachEvent则传参使用
btn.onclick = function(){ var event = window.event; }; btn.attachEvent('onclick', function(event){ var event = event; });
event对象同样有一些属性和方法
srcElement:事件的目标,等同于DOM中的target,事件流的目标阶段
cancelBubble:默认false,将其设置为true表示取消事件冒泡
returnValue:默认true,将其设置为false表示取消事件默认行为
在IE8及之前版本中不能使用event.stopPropagation()和event.preventDefault()
3⃣️跨浏览器的事件对象
接着刚刚写过的EventUtil对象
var EventUtil = { addHandler: ...., removeHandler: ...., getEvent: function(event){ return event ? event : window.event }, getTarget: function(event){ return event.target || event.srcElement }, preventDefault: function(event){ if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } }, stopPropagation: function(event){ if (event.stopPropagation) { event.stopPropagation(); } else { event.cancelBubble = true; } } };
用法:
btn.onclick = function(event){ event = EventUtil.getEvent(event) };
四,事件类型
DOM3级事件规定了下列几类事件:
①UI事件:指的是那些不一定与用户操作有关的事件。
load:1,当页面完全加载后在window上触发;2,当所有框架加载完毕后在框架上触发;当图像加载完毕时在<img>元素上面触发,当嵌入的内容加载完毕时在<object>元素上面触发。
select:当用户选择文本框(input或textarea)中的一或多个字符时触发。
resize:当窗口或框架大小变化时在window或框架上触发。
scroll:当用户滚动带滚动条的元素中的内容时,在该元素上触发。
针对不同的元素,添加事件处理程序,用我们上面的EventUtil对象的addHandler方法,这里要注意的是传的第一个参数element,第二个是事件名(不加on),比如:
Event.addHandler(window, 'load', function(){ .... }); var img = new Image(); Event.addHandler(img, 'load', function(){ ... }); img.src = 'smile.gif';
②焦点事件
blur和focus:元素在失去或获得焦点时触发,所有浏览器都支持,并且没有冒泡。
focusin和focusout类似,只不过它们不支持Firefox,同时都冒泡,这里不做讨论。
③鼠标和滚轮事件
最常用的一类事件(这里挑重要的说):
click:。。。,这里要补充一点,除了鼠标,回车键也能触发click事件!!
dblclick:。。。
mousedown:。。。
mousemove:。。。
mouseup:。。。
mouseenter:光标从元素外首次移动到元素范围内触发,移动到后代元素上不触发,这个事件不冒泡,并且chrome和Safari不支持,并且chrome和Safari不支持
mouseleave:方向相反,其他同上,并且chrome和Safari不支持,并且chrome和Safari不支持,并且chrome和Safari不支持
mouseout:从一个元素移动到另一个元素,可能是外部元素,也可能是子元素
mouseover:元素外移动到元素内,首次进入时会触发
=====
click事件受mousedown和mouseup影响,如果其中一个被取消了,click事件不会触发
鼠标从按下去到松开,一系列的触发事件顺序为:
mousedown
mouseup
click
同样的如果是双击事件,触发顺序为
mousedown
mouseup
click
mousedown
mouseup
click
dblclick
=====
1,客户区坐标位置:是鼠标指针所在的垂直位置和水平位置,是相对于浏览器视口,不包含滚动条位置,其实就是click事件的event对象的clientX和clientY,还是用之前定义的EventUtil对象,这个还是比较常用的,因为可以计算鼠标移动的距离等
var btn = document.getElementById('myBtn'); EventUtil.addHandler(btn, 'click', function(event){ var event = EventUtil.getEvent(event); alert('clientX: '+event.clientX); alert('clientY: '+event.clientY); })
2, 页面坐标位置:是包含滚动条的位置,其实就是click事件的event对象的pageX和pageY,如果页面没有滚动,pageX和pageY就等于clientX和clientY,如果有滚动条,就要在clientX和clientY的基础上再加一个scrollTop和scrollLeft,scrollTop和scrollLeft可能body捞不到,所以保险起见也用documentElement,(documentElement返回的是dom的根节点,也就是html节点)
var btn = document.getElementById('myBtn'); EventUtil.addHandler(btn, 'click', function(event){ var event = EventUtil.getEvent(event); var pageX = event.pageX, pageY = event.pageY; if (pageX === undefined) { pageX = event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft); pageY = event.clientY + (document.body.scrollTop || document.documentElement.scrollTop); } alert('pageX: ' + pageX + ' , pageY: ' + pageY) });
3,屏幕坐标位置:是相对于整个电脑屏幕的位置,也是event对象的screenX和screenY,用法就跟clientX一样
4,点击鼠标时检测是否按下了特殊键,这个不多说,用得比较少,详见372页
5,。。。不想说
6,鼠标按钮:mousedown和mouseup事件的event对象存在一个button属性,DOM的button属性有三个值,0、1、2,代表的是鼠标左键,鼠标滚轮,鼠标右键,而IE8及之前的版本的button属性有8个值,0~8,1、2、4和DOM的button属性0、1、2一样,其他5个就是一些同时按下多个鼠标键,没什么太大作用,因为正常人不会同时按下;检测是否支持DOM的button属性,用hasFeature()方法来检测,所以可以为EventUtil对象添加getButton()方法:
var EventUtil = {
。。。。。。 getButton: function(event){ if (document.implementation.hasFeature('MouseEvents', '2.0')) { return event.button; } else { switch (event.button) { case 0: case 1: case 3: case 5: case 7: return 0; case 2: case 6: return 2; case 4: return 1; } } } }
8,鼠标滚轮事件:mousewheel,事件event对象有一个wheelDelta属性,是120的倍数,向前是正倍数,向后是负倍数,具体兼容问题详见377
9,针对iPhone和ipad的Safari开发注意点:不要用dblclick。。。。
④键盘与文本事件
键盘事件主要遵循的是DOM0级,DOM3级为键盘事件制定了规范(之前几个事件都是DOM2就提出了,但是也是DOM3进行了规范):
keydown:按下任意键时触发,按住不放会重复触发;
keypress:按下字符键时触发,按住不放会重复触发(能够影响文本的键就是字符键,退格del也是)
keyup:释放键时触发
如果用户按下一个字符键,首先触发keydown,再是keypress
1,键码keyCode:只有keydown和keyup事件的event对象才有keyCode属性,记住几个特殊的:
回车:13,退出:27,删除:46,数字键盘0-9: 96~105
2,。。。其他都是一些不常用的,详见383,比如游戏手柄上面的按键等
⑤复合事件:DOM3新增,用于处理IME(输入法编辑器)的输入序列,大部分浏览器不支持,这个不说
⑥变动事件:能在DOM中某一部分发生变化时给出提示。主要是添加或删除DOM节点的时候会触发的事件,这个不细说
⑦HTML5事件:这个也不细说,大概浏览了一些,都不怎么用
⑧设备事件:可以让开发人员确定用户怎样使用设备,主要是智能手机和平板,这个说一说
1,orientationchange事件(Safari)
移动Safari的window.orientation属性中可能包含3个值:0表示肖像模式,90表示向左旋转的横向模式,-90表示向右旋转的横向模式,暂不支持向下的,只要用户改变设备的查看模式,就会触发orientationchange事件,此时的event不包含任何信息,只有通过window.orientation才能访问到。下面是典型案例代码:
EventUtil.addHandler(window, 'load', function(event){ alert('Current orientation is ' + window.orientation); EventUtil.addHandler(window, 'orientationchange', function(event){ alert('Current orientation is ' + window.orientation); }) });
当页面加载后先检测此时的查看模式,当查看模式发生改变时,再检测是往哪个方向改变了;
所有的ios设备都支持这个事件;
2,MozOrientation事件
当设备的加速计检测到设备方向改变时,就会触发这个事件。在window对象上触发,此时event对象包含三个属性:x、y、z,数值都在-1~1之间。x的正半轴方向是向左,y的正半轴方向是向用户靠近的方向,z是检测垂直加速度,1是静止,0是失重,设备移动时会减小。
带加速计的设备才支持该事件,包括Macbook,Lenovo Thinkpad,Windows Mobile和安卓设备,这个是实验API,以后可能会变
3,deviceorientation事件
和Mozorientation事件类似,也是在window对象上,这个事件主要是为了告诉用户设备在空间中方向朝哪里,而不是朝哪里移动,具体event中5个属性详见397,类似于alpha,beta,gamma。
支持该事件的有Safari,chrome,安卓版的webkit
4,devicemotion事件
告诉开发人员设备什么时候移动,而不仅仅是设备方向如何改变,比如可以测各个方向上的加速度,详见398
⑨触摸与手势事件
1,触摸事件
touchstart:。。。
touchmove:手指滑动时连续触发,可以用preventDefault()来阻止滚动
touchend:。。。
上面事件都会冒泡,每个触摸事件的event对象中都提供了鼠标事件中常用的属性:如clientX, clientY, screenX, screenY ,但是这些DOM属性不会去用。下列三个用于跟踪触摸的属性:
touches:表示当前跟踪的触摸操作的Touch对象的数组
targetTouches:特定于事件目标的Touch对象的数组
changeTouches:表示自上次触摸以来发生了什么改变的Touch对象数组
每一个Touch对象都包含下列属性:clientX,clientY,pageX,pageY,screenX,screenY,target(触摸的DOM节点目标)
EventUtil.addHandler(document, 'touchstart', function(event){ var event = EventUtil.getEvent(event); alert('Touch started (' + event.touches[0].clientX + ',' + event.touches[0].clientY + ')'); }); EventUtil.addHandler(document, 'touchend', function(event){ var event = EventUtil.getEvent(event); alert('Touch end (' + event.changedTouches[0].clientX + ',' + event.changedTouches[0].clientY + ')'); });
touchstart的事件里面只有touches数组,而touchmove和touchend事件里面没有touches数组,所以访问位置只能通过changedTouches数组。
2,手势事件
主要是Safari中,安卓里面无效
gesturestart:当一个手指已经按在屏幕上另一个手指又触摸的时候会触发
gesturechange:当触摸屏幕时任何一个手指位置发生改变时会触发
gestureend:当任何一个手指从屏幕上面移开时会触发
每个手势事件的event对象下也包含着鼠标事件中的一些属性,此外有两个重要的属性:rotation和scale
rotation表示手指变化引起的旋转角度,负值代表逆时针,正值顺时针,从0开始
scale表示两个手指间的距离,从1开始变化,距离大变大,距离小变小
五,内存和性能
前言:在js中,添加到页面上的事件处理程序的数量直接关系到页面的整体运行性能。每个函数都是对象,都会占用内存,内存中对象越多,性能就越差。其次,DOM访问次数越多,也会延迟整个页面的交互就绪时间。
1,事件委托
利用了事件冒泡,只指定一次处理程序,就可以管理某一类型的所有事件。例如:
HTML部分代码:
<ul id="myLinks"> <li id="goSomeWhere">Go somewhere</li> <li id="doSomething">Do Something</li> <li id="sayHi">Say Hi</li> </ul>
JS部分代码:
var item1 = document.getElementById('goSomeWhere'); var item2 = document.getElementById('doSomething'); var item3 = document.getElementById('sayHi'); EventUtil.addHandler(item1, 'click', function(event){ location.href = "www.baidu.com" }); EventUtil.addHandler(item2, 'click', function(event){ document.title = 'i changed this title'; }); EventUtil.addHandler(item3, 'click', function(event){ alert('hi') });
这里给每一个DOM节点添加了事件处理程序,如果一个页面有无数个,那就会添加无数个,并且访问无数个DOM节点,所以非常消耗性能。而事件委托技术是在DOM树中尽量最高的层次上添加一个事件处理程序,如下:
var list = document.getElementById('myLinks'); EventUtil.addHandler(list, 'click', function(event){ var event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); switch (target.id) { case "goSomeWhere": location.href = "www.baidu.com"; break; case "doSomething": document.title = 'i changed this title'; break; case "sayHi": alert('hi') break; } })
这段代码中只为<ul>元素添加了一个事件处理程序onclick,由于所有列表项都是这个元素的子节点,并且都会事件冒泡,所以单击事件最终还是会被这个函数处理。所以只用一次就可以了。
最适合使用事件委托技术的事件有:click,mousedown,mouseup,keydown,keyup,keypress
2,移除事件处理程序
如果在DOM节点上添加了事件处理程序,此时直接把该节点删除或者替换掉(removeChild、replaceChild),原来添加的事件处理程序则无法被作为垃圾回收,所以在删除或替换之前,要先移除事件处理程序,具体的移除见前面,DOM0设为null,DOM2级removeEventListener,IE用的detachEvent
六,模拟事件
事件通常是通过用户操作来触发,但也可以使用JavaScript来触发,DOM2级为IE9及其他浏览器规定了模拟特定事件,IE有它自己的方式
1,DOM中的事件模拟
第一步:创建事件对象,在document对象上使用createEvent()创建一个event对象,接收一个字符串参数表示要创建的事件类型(UIEvents,MouseEvents,MutationEvents(一般化的DOM变动事件),HTMLEvents)
第二步:初始化事件对象,每一种类型的event对象都有一个特殊方法,为它传入一些参数就能初始化这个event对象
第三步:触发事件对象,调用dispatchEvent()方法,传一个参数,表示要触发事件的event对象,就是刚刚第一步创建的。
①模拟鼠标事件:
按照上面的步骤,
第一步传入的参数是"MouseEvents",
第二步初始化的特殊方法是initMouseEvent()方法,要传15个参数:
===
type:字符串,要触发的事件类型,例如'click',
bubbles:布尔值,是否应该冒泡,要设为true
cancelable:布尔值,是否可以取消,要设为true
view:与事件关联的视图,要设为document.defaultView
detail:整数,与事件有关的详细信息,设为0
screenX:整数,设为0
screenY:整数,设为0
clientX:整数,设为0
clientY:整数,设为0
ctrlKey:布尔值,是否按下ctrl,设为false
altKey:布尔值,设为false
shiftKey:false
metaKey:false
button:整数,表示按下了哪一个鼠标键,默认0
relatedTarget:表示与事件相关的对象,这个参数只在mouseover和mouseout中使用
===
var btn = document.getElementById('myBtn'); // 创建事件对象 var event = document.createEvent('MouseEvents'); // 初始化事件对象 event.initMouseEvent("click", true, true, document.defaultView, 0, 0, 0, 0, 0, false, false, false, false, 0, null); // 触发事件 btn.dispatchEvent(event);
这段代码非常重要,一定要记住,尤其是15个参数,顺序不能错!
②模拟键盘事件
目前只有Firefox模拟了键盘事件,所以这个具体就不写了,详见407-408;
③其他模拟事件
变动和HTML事件模拟也不多介绍,本身事件用得就不多,更别提还要模拟事件了
④自定义DOM事件
只有IE9+和Firefox6+支持
2,IE中的事件模拟
第一步:创建一个event对象,用createEventObject()
第二步:初始化事件对象,不传参数,手动添加属性并设置值
第三步:触发事件,fireEvent(),传两个参数:事件处理程序名和刚刚创建的事件对象
var btn = document.getElementById('myBtn'); // 创建事件对象 var event = document.createEventObject(); // 初始化事件对象 event.screenX = 100; event.screenY = 0; event.clientX = 0; event.clientY = 0; event.ctrlKey = false; event.altKey = false; event.shiftKey = false; event.button = 0; // 触发事件 btn.fireEvent('onclick', event);
end。