An Introduction To DOM Events
Click,touch,load,drag,change ,input,error,resize ,dom 上面的事件非常的多。无论是因为用户的交互或是浏览器,事件可能在文档的任何一个地方被触发。他们不是在一个地方触发就在那个地方停止。他们会随着文档流,有一个自己的生命周期。事件不仅扩展性强而且还很有用。作为一个开发者,我们一定要了解DOM事件是如何工作的。这样我们就能够发挥事件的作用,做出更好的用户体验。
在我作为一个前端工程师的日子里,我感觉我从来没有对DOM事件的工作原理得到一个清晰的解释。我现在的目的就是针对这个问题给你们一个清晰的解释,让你们能更快的理解。
现在我会介绍基本的DOM时间,然后深入它们的内部工作原理,解释一下我们怎么用它们去解决一些共同问题的。
事件的监听
以前,每个浏览器对于DOM节点事件绑定的方式上很不一致。像jquery这种类库已经把这些不一致都兼容了。
当我们更接近于标准的浏览器环境时,我们就能够更加安全的使用正式规范中的API。为了简单的描述,我就解释下怎样在现在流行的浏览器中运用这些事件。如果你在还在ie8以下的浏览器环境下写javascript代码,我建议你用一下polyfill或是用一些类似jquery的类库去做兼容。
我们写一个监听事件的javascript代码是:
element.addEventListener(<event-name>,<callback>,<use-c apture>);
event-name(字符串)
这是一个你想要监听事件的名字。它可以是任意的标准DOM事件中的一个,或是你自定义的事件(我会在后面讲到)
Callback(方法)
当事件发生后这个方法才执行。事件对象,包含有关该事件的数据,作为第一个参数被传入
Use-capture(boolean型)
这个表示回调函数是否在捕获阶段被触发(我们接下来会谈到)
解除事件绑定
当事件一旦以后不被用到就立刻解除绑定是正确的,为了解除绑定我们用element.removeEventListener()这个方法
element.removeEventListener(<event-name>, <callback>, <use-capture>);
但是removeEventListener有一个值得注意的地方,你必须有一个对原来绑定回调函数的引用。只是写element.removeEventListener('click');是没用的。
最主要的是如果你想要把某个事件解除绑定,然后我们就需要找到那个回调函数,这样意味着回调函数不能是匿名函数。
保持回调函数的上下文
一个常见的例子就是回调函数在错误的上下文中执行。
var element = document.getElementById('element');
var user = {
firstname: 'Wilson',
greeting: function(){
alert('My name is ' + this.firstname);
}
};
// Attach user.greeting as a callback
element.addEventListener('click', user.greeting);
// alert => 'My name is undefined'
使用匿名函数
我们希望回调函数中能够正确的弹出My name is Wilson,事实上弹出的是My name is undefined 为了让this.firstname 返回Wilson。User.greeting必须再user的上下文中执行。
当我们把greeting这个方法传入事件时,我们只是传入了这个方法的引用。这个user的上下文却没有被传入。最终回调函数的上下文是这个元素的上下文,意思就是this指向的是这个元素,不是user,因此this.firsname 是undefined。
这里有个两个办法阻止这种上下文不匹配。一个是我们能够在一个匿名函数中正确的调用到user.greeting()的上下文。
element.addEventListener('click', function() {
user.greeting();
// alert => 'My name is Wilson'
});
方法的原型绑定(内部实现用apply)
最后这个方法不是最好的。因为你没有一个完全的控制权对于一个你想要用.removeEventListener()解除绑定的时候。加上这种方法很丑陋。我比较喜欢用.bind()方法去触发一个能够在指定上下文运行的方法。然后我们就把这个方法作为.addEventListener()的回调函数。
// Overwrite the original function with
// one bound to the context of 'user'
user.greeting = user.greeting.bind(user);
// Attach the bound user.greeting as a callback
button.addEventListener('click', user.greeting);
我们也可以用这个回调去解除绑定。
button.removeEventListener('click', user.greeting);
事件对象
当事件第一次被触发时事件就创建了一个事件对象。它会随着事件在dom里穿梭。我们监听事件里的回调函数被传递给事件对象,作为第一个参数。我们可以通过这个对象找到这个事件被触发时的所有信息。
Type(字符串)
这是事件的名字
Target(节点)
这是产生事件的节点
Bubbles(boolean型)
这个表明是否是冒泡事件
PreventDefault(方法)
这个会阻止事件的默认行为
StopPropagation(方法)
该方法将停止事件的传播,阻止它被分派到其他 Document 节点。在事件传播的任何阶段都可以调用它。注意,虽然该方法不能阻止同一个 Document 节点上的其他事件句柄被调用,但是它可以阻止把事件分派到其他节点。
stopImmediatePropagation(方法)
该方法能够阻止事件的传播更能够阻止在同一dom节点下其他的事件。
Cancelable(布尔型)
这个值就能够决定调用event.preventDefault方法是否能够阻止事件的默认行为
defaultPrevented(布尔型)
这个状态表明是否调用了preventDefault方法。
IsTrusted(布尔型)
一个事件如果说是isTrusted那它是来源于元素本身,不是有javascript合成的。
EventPhase(数字)
这个表明当前事件正在none (0), capture (1), target (2) or bubbling (3)。
TimeStamp(数字)
这是事件被触发的时间戳
事件对象中还有很多其他的属性。但是他们就是不同事件的不同特性了。比如,鼠标事件会有clientX和clientY这个两个属性去表示当前的鼠标的位置。
最好的就是用一个你熟悉的浏览器在控制台里console.log一下找出更多的属性
事件流
当事件触发时,他不是只在事件产生的节点触发一次。他会有三个阶段。第一个阶段就是捕获阶段,事件从dom的根节点到目标元素。第二阶段在事件目标元素上触发,第三阶段就是冒泡阶段,事件从目标元素传播回文档。
捕获阶段
事件的捕获是第一个阶段。事件从文档的根节点一层一层的向下传播直到它达到目标元素。捕获的工作就是建立一个事件传播的路径,这样在冒泡阶段事件就能传播回去。
如果你想监听由捕获过程产生的事件,可以设置addEventListener的第三个参数为true,我没有接触过很多在捕获过程中的监听事件,你可以阻止任何在某个元素的点击事件的捕获过程中阻止事件的传播。
如果你不确定的话,可以再事件的冒泡阶段设置usecapture为false或undefined。
处于目标阶段
这个就是当事件到达目标节点时。这个事件在冒泡阶段之前被触发。
在嵌套元素的情况下,鼠标和点击事件的目标元素总是最里面的那一层。如果你监听一个div上的点击事件,用户实际上是点击里面的p标签,所以p标签就是事件的目标元素。事实是事件冒泡让你可以监听到div上的click事件,然后当事件传播时也能收到一个回调。
冒泡阶段
当事件在目标元素触发后,它不会停在那里,它会沿着dom向上传播直到到达dom的根节点。这就表明相同的事件会在目标元素的父节点被触发,然后传播到父节点的父节点直到整个没有任何节点为止。
把dom想象成一个洋葱,事件目标元素就是洋葱的核心。在捕获阶段,事件从洋葱表皮慢慢传播。当到达核心触发后就调转方向,像之前传播的一样再传播回去,当回到表皮时事件就结束了。
冒泡是很有用的。我们就不用只监听到产生事件的特定元素,相反我们监听一个靠近dom树中的元素,然后等着事件发生,如果事件没有冒泡,我们就不得不监听更多的元素确保它能够被触发。
大多数情况下,不是所有,事件都会冒泡,如果事件不冒泡那肯定是有原因的,如有疑问http://www.w3.org/TR/DOM-Level-3-Events/#event-types
Stopping Propagation
用一个简单的stopPropagation方法就可以在事件传播过程中阻止其进一步传播。这样在事件捕获或是冒泡过程中就不会被任何监听的节点触发。
如果同一元素上有很多监听,调用stopPropagation方法不会阻止其他在目标元素上监听的事件发生。如果你想阻止同一个节点上的其他事件发生,你可以用 event.stopImmediatePropagation()方法。
阻止浏览器的默认行为
当某个固定事件被触发时浏览器有个默认的行为 ,最常见的就是点击一个链接。当点击一个a标签时它会冒泡到dom根节点,浏览器会解析href。然后把页面载入到另一个地址。
在web应用中,开发者经常自己去控制navigation,从而阻止页面的刷新,为了做到这个我们必须阻止浏览器的默认点击行为,然后做我们自己想要的事情。这样我们就需要调用event.preventDefault()方法。
我们调用这个方法去阻止浏览器的默认行为。比如我们可以阻止在一个html5游戏中按下空格键滚动页面这个事件,或者可以阻止选中文字的点击操作。
调用event.stopPropagation()方法,只会阻止在传播过程中的其他事件的发生,但是不会阻止浏览器的默认行为。
自定义事件
浏览器不是唯一一个能够触发事件的工具,我们能够自己定义事件然后绑定在文档的任意元素上面。这种事件就像普通的dom事件一样。
varmyEvent = new CustomEvent("myevent", {
detail: {
name: "Wilson"
},
bubbles: true,
cancelable: false
});
// Listen for 'myevent' on an element
myElement.addEventListener('myevent', function(event) {
alert('Hello ' + event.detail.name);
});
// Trigger the 'myevent'
myElement.dispatchEvent(myEvent);
合成一个不被相信的dom元素去响应用户的操作也是可以的。对于检测dom相关的类库很有用。如果你对此很有兴趣,火狐提供了
请记住:
自定义事件在ie8及其以下版本不起作用。
Flight这个框架是Twitter公司用自定义事件来完成两个模块间的通信。这需要模块间有很强的解耦性。
事件的委托
事件的委托对于实现只用一个事件监听很多dom节点是很合适和可行的办法。比如,一百个元素在用同一种方式响应一个click事件,我们就需要把这些节点都遍历一遍然后在每个节点上加入监听事
件。这就可能创造了100个独立的事件。只要一个加入一个新的节点我们就必须加一个新的事件。这样做不仅成本高而且不易维护。
事件的委托可以让我们更加轻松。如果要坚挺每一个li元素我们只需要监听它们的父元素ul,当li被点击时,事件会冒泡到ul然后触发事件。我们可以通过检查目标元素来判断哪一个li被点击过。下面就是一个例子:
var list = document.querySelector('ul');
list.addEventListener('click', function(event) {
var target = event.target;
while (target.tagName !== 'LI') {
target = target.parentNode;
if (target === list) return;
}
// Do stuff here
});
这样做更好因为我们只需要在最顶层加一个监听事件,这样就算新增加一个子元素我们也不会担心去增加一个新的监听事件了,这种原理比较简单但是非常好用。
我不建议你用这样粗俗的代码在你的应用中。你可以用javascript类库中的delegate事件。就像FTLab’sftdomdelegate。如果你用jquery的话,你可以直接用on方法然后在第二个参数中把要监听的元素传入。
常用事件
Load
加载事件在页面所有资源都加载完成后才发生。可以是image, style sheet, script, video, audio file, document or window。
ONBEFOREUNLOAD
Window.onbeforeunload 会让开发者去确认用户是否要离开页面。在应用中很有用,如果这个页面突然关掉的话可以让用户去保存可能失去的改变。
值得注意的是如果用了这个事件就阻止了浏览器缓存这个页面,如果再想访问的话加载速度就会慢一点。而且。Onbeforeunload事件必须是同步的。
STOPPING WINDOW BOUNCE IN MOBILE SAFARI
现在我们用一个简单的event.preventDefault技术去阻止手机safari滚动过界时候窗口弹回来。
但是这个也会阻止掉本身的滚动功能。为了让需要滚动的元素滚动,我们需要监听需要滚动的元素,然后在事件对象上设置一个标示。在文档的回调函数中,我们通过在触摸元素上的标示决定是否去阻止滚动的默认行为。
在ie8及其以下版本不支持控制事件对象。作为一种变通方式,你可以在目标节点上设置属性。
RESIZE
监听window对象的resize事件对于复杂的响应式布局很有作用。只用css去完成布局不是永远行得通的,有时javascript会帮助我们去计算元素的大小和尺寸,当window的大小改变或是设备的方向改变时我们可能就需要去调整这些尺寸。
我建议可以在回调函数中使用去除抖动的方法时回调更加平稳,布局也不会变乱。
Transitionend
现在我们用css去完成应用中的主要动画。有时我们只需要知道动画到底什么时候结束。
el.addEventListener('transitionEnd', function() {
// Do stuff
});
注意下面的内容:
如果你用@keyframe这样动画,用animationend事件而不是transitionend事件。
像大多数事件一样,transitionend也会冒泡。记得用event.stopProgation()在任何一个后代事件或是检查目标元素去阻止不该发生的回调函数。
ANIMATIONITERATION
Animationiteration事件会在一个动画刚好完成一次的时候触发,如果我们想要停止动画,但是不想在动画的中间停止就非常有用。
如果你有兴趣的话,可以看看这个http://wilsonpage.co.uk/animation-iteration-event/
ERROR
当资源在加载时发生了一个错误,我们需要做些什么尤其是我们用户在一个网络很不好的环境下。现在我们用这个事件去检测图片加载,如果图片加载出错马上隐藏掉。因为DOM Level3Event特别重写了error这个事件阻止其冒泡,所以我们可以用下面任何一种方式去调用error事件。
imageNode.addEventListener('error', function(event) {
image.style.display = 'none';
});
不幸的是,addEventLinstener没有覆盖到所有的情况,我的同事就给我举了一个例子error事件没有触发的例子,所以为了保证image的error事件能够触发我们只有用行内定义确保error事件被绑定。
<img src="http://example.com/image.jpg" onerror="this.style.display='none';" />
这样做的原因是你无法保证被绑定到error事件的代码在error事件触发后能够执行。用行内句柄就是当标记被解析后,图片请求后我们的error事件能够被绑定上。
Lessons From The Event Model
我们可以从dom事件模型的成功中中学到很多。我们可以使用类似的解耦思想在我们自己的项目中,在一个应用中模型可以很复杂,他们被封装在一个简单的接口后面。很多前端框架(就像backbone)就是基于事件的。解决发布模块和订阅模块中跨模块通信问题的办法和dom是类似的。
基于事件的架构是伟大的,他们给我们在写一个应用时提供一个非常简单的接口去响应跨越数千台设备之间的通信。通过事件,设备能够准确的告诉我们什么发生了,什么时候发生的,我们想去怎么解决。我们不关心在屏幕后面发生了什么,我们得到了一系列的抽象接口去帮助我们创建一个很棒的app。
posted on
浙公网安备 33010602011771号