Click、touch、load、drag、change、input、error、risize — 这些都是冗长的DOM(文档对象模型)事件列表的一部分。事件能够在文档(Document)结构的任何部分被触发,触发者能够是用户操做,也能够是浏览 器自己。事件并非只是在一处被触发和终止;他们在整个document中流动,拥有它们本身的生命周期。而这个生命周期让DOM事件有更多的用途和可扩 展性。javascript
做为一个开发人员,咱们必需要理解DOM事件是如何工做的,而后才能更好的驾驭它,利用它们潜在的优点,开发出更高交互性的参与体验(engaging experiences)。css
反观我作前端开发的这么长时间里,我以为我历来没有看到过一个关于DOM事件是如何工做的较为直接准确的解释。今天个人目标就是在这个课题上给你们一 个清晰的介绍,让你们可以更快速的了解它。 我首先会介绍DOM事件的基本使用方式,而后会深刻挖掘事件内部的工做机制,解释咱们如何使用这些机制来解决一些常见的问题。html
在过去,主流浏览器之间对于如何给DOM节点添加事件监听有着很大的不一致性。jQuery这样的前端库为咱们封装和抽象了这些差别行为,为事件处理带来了极大的便利。前端
现在,咱们正一步步走向一个标准化的浏览器时代,咱们能够更加安全地使用官方规范的接口。为了简单起见,这篇文章将主要介绍在现代浏览器中如何管理事件。若是你在为IE8或者更低版本写JavaScript,我会推荐你使用polyfill或者一些框架(如jQuery)来管理事件监听。java
在JavaScript中,咱们使用以下的方式为元素添加事件监听:node
1
|
element.addEventListener(<event-name>, <callback>, <use-capture>);
|
1
2
3
4
5
6
7
8
|
var element = document.getElementById( 'element' );
function callback() {
alert( 'Hello' );
}
// Add listener
element.addEventListener( 'click' , callback);
|
Demo: addEventListenerjquery
移除再也不使用的事件监听是一个最佳实践(尤为对于长时间运行的Web应用)。咱们使用element.removeEventListener()方法来移除事件监听:git
1
|
element.removeEventListener(<event-name>, <callback>, <use-capture>);
|
可是removeElementListener有一点须要注意的是:你必需要有这个被绑定的回调函数的引用。简单地调用element.removeEventListener('click');是不能达到想要的效果的。github
本质上来说,若是咱们考虑要移除事件监听(咱们在长时间运行(long-lived)的应用中须要用到),那么咱们就须要保留回调函数的句柄。意思就是说,咱们不能使用匿名函数做为回调函数。web
1
2
3
4
5
6
7
8
9
|
var element = document.getElementById( 'element' );
function callback() {
alert( 'Hello once' );
element.removeEventListener( 'click' , callback);
}
// Add listener
element.addEventListener( 'click' , callback);
|
一个很容易遇到的问题就是回调函数没有在预想的运行上下文被调用。让咱们看一个简单的例子来解释一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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'
|
Demo: Incorrect callback context
咱们但愿回调函数中可以正确的输出”My name is Wilson”。事实上,结果确是”My name is undefined”。为了使得 this.firstName 可以返回”Wilson”,user.greeting必须在user对象的上下文环境(context)中被执行(这里的运行上下文指的是.号左边的对 象)。
当咱们将greeting函数传给addEventListener方法的时候,咱们传递的是一个函数的引用;user相应的上下文并无传递过去。 运行的时候,这个回调函数其实是在element的上下文中被执行了,也就是说,在运行的时候,this指向的是element,而不是user。因此 this.firstName是undefined。
有两种方式能够避免这种上下文错误的问题。第一种方法,咱们能够在一个匿名函数内部调用user.greeting()方法,从而得到正确的函数执行上下文(user)。
1
2
3
4
|
element.addEventListener( 'click' , function () {
user.greeting();
// alert => 'My name is Wilson'
});
|
上一种方式并非很是好,由于咱们不能得到回调函数的句柄以便后面经过.removeEventListener()移除事件监听。另外,这种方式也 比较丑陋。。我更喜欢使用.bind()方法(作为ECMAScript 5的标准内建在全部的函数对象中)来生成一个新的函数(被绑定过的函数),这个函数会在指定的上下文中被执行。而后咱们将这个被绑定过的函数做为参数传 给.addEventListener()的回调函数。
1
2
3
4
5
6
|
// 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);
|
与此同时,咱们得到了回调函数的句柄,从而能够随时从元素上移除相应的事件监听。
1
|
button.removeEventListener( 'click' , user.greeting);
|
想获取Function.prototype.bind的更多信息,请点击的浏览器支持页面,以及polyfill的介绍。
Event对象在event第一次触发的时候被建立出来,而且一直伴随着事件在DOM结构中流转的整个生命周期。event对象会被做为第一个参数传递给事件监听的回调函数。咱们能够经过这个event对象来获取到大量当前事件相关的信息:
此外事件对象还可能拥有不少其余的属性,可是他们都是针对特定的event的。好比,鼠标事件包含clientX和clientY属性来代表鼠标在当前视窗的位置。
咱们可使用熟悉的浏览器的调试工具或者经过console.log在控制台输出来更具体地查看事件对象以及它的属性。
当一个DOM事件被触发的时候,它并不仅是在它的起源对象上触发一次,而是会经历三个不一样的阶段。简而言之:事件一开始从文档的根节点流向目标对象(捕获阶段),而后在目标对向上被触发(目标阶段),以后再回溯到文档的根节点(冒泡阶段)。
(图片来源:W3C)
事件的第一个阶段是捕获阶段。事件从文档的根节点出发,随着DOM树的结构向事件的目标节点流去。途中通过各个层次的DOM节点,并在各节点上触发捕 获事件,直到到达事件的目标节点。捕获阶段的主要任务是创建传播路径,在冒泡阶段,事件会经过这个路径回溯到文档跟节点。
正如文章一开始的地方提到,咱们能够经过将addEventListener的第三个参数设置成true来为事件的捕获阶段添加监听回调函数。在实际 应用中,咱们并无太多使用捕获阶段监听的用例,可是经过在捕获阶段对事件的处理,咱们能够阻止相似clicks事件在某个特定元素上被触发。
1
2
3
4
5
|
var form = document.querySelector( 'form' );
form.addEventListener( 'click' , function (event) {
event.stopPropagation();
}, true ); // Note: 'true'
|
若是你对这种用法不是很了解的话,最好仍是将useCapture设置为false或者undefined,从而在冒泡阶段对事件进行监听。
当事件到达目标节点的,事件就进入了目标阶段。事件在目标节点上被触发,而后会逆向回流,直到传播至最外层的文档节点。
对于多层嵌套的节点,鼠标和指针事件常常会被定位到最里层的元素上。假设,你在一个<div>元素上设置了click事件的监听函数,而 用户点击在了这个<div>元素内部的<p>元素上,那么<p>元素就是这个事件的目标元素。事件冒泡让咱们能够在 这个<div>(或者更上层的)元素上监听click事件,而且事件传播过程当中触发回调函数。
事件在目标元素上触发后,并不在这个元素上终止。它会随着DOM树一层层向上冒泡,直到到达最外层的根节点。也就是说,同一个事件会依次在目标节点的父节点,父节点的父节点。。。直到最外层的节点上被触发。
将DOM结构想象成一个洋葱,事件目标是这个洋葱的中心。在捕获阶段,事件从最外层钻入洋葱,穿过途径的每一层。在到达中心后,事件被触发(目标阶段)。而后事件开始回溯,再次通过每一层返回(冒泡阶段)。当到达洋葱表面的时候,此次旅程就结束了。
冒泡过程很是有用。它将咱们从对特定元素的事件监听中释放出来,相反,咱们能够监听DOM树上更上层的元素,等待事件冒泡的到达。若是没有事件冒泡,在某些状况下,咱们须要监听不少不一样的元素来确保捕获到想要的事件。
Demo: Identifying event phases
绝大多数事件会冒泡,但并不是全部的。当你发现有些事件不冒泡的时候,它确定是有缘由的。不相信?你能够查看一下相应的规范说明。
能够经过调用事件对象的stopPropagation方法,在任何阶段(捕获阶段或者冒泡阶段)中断事件的传播。此后,事件不会在后面传播过程当中的通过的节点上调用任何的监听函数。
1
2
3
4
5
6
7
8
|
child.addEventListener( 'click' , function (event) {
event.stopPropagation();
});
parent.addEventListener( 'click' , function (event) {
// If the child element is clicked
// this callback will not fire
});
|
调用event.stopPropagation()
不会阻止当前节点上此事件其余的监听函数被调用。若是你但愿阻止当前节点上的其余回调函数被调用的话,你可使用更激进的event.stopImmediatePropagation()
方法。
1
2
3
4
5
6
7
8
|
child.addEventListener( 'click' , function (event) {
event.stopImmediatePropagation();
});
child.addEventListener( 'click' , function (event) {
// If the child element is clicked
// this callback will not fire
});
|
当特定事件发生的时候,浏览器会有一些默认的行为做为反应。最多见的事件不过于link被点击。当一个click事件在一个<a>元素上 被触发时,它会向上冒泡直到DOM结构的最外层document,浏览器会解释href属性,而且在窗口中加载新地址的内容。
在web应用中,开发人员常常但愿可以自行管理导航(navigation)信息,而不是经过刷新页面。为了实现这个目的,咱们须要阻止浏览器针对点击事件的默认行为,而使用咱们本身的处理方式。这时,咱们就须要调用event.preventDefault().
1
2
3
4
|
anchor.addEventListener( 'click' , function (event) {
event.preventDefault();
// Do our own thing
});
|
咱们能够阻止浏览器的不少其余默认行为。好比,咱们能够在HTML5游戏中阻止敲击空格时的页面滚动行为,或者阻止文本选择框的点击行为。
调用event.stopPropagation()只会阻止传播链中后续的回调函数被触发。它不会阻止浏览器的自身的行为。
Demo:Preventing default vehaviour
浏览器并非惟一能触发DOM事件的载体。咱们能够建立自定义的事件并把它们分派给你文档中的任意节点。这些自定义的事件和一般的DOM事件有相同的行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var myEvent = 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);
|
在元素上合成不可信任的(untrusted)DOM事件(如click)来模拟用户操做也是可行的。这个在对DOM相关的代码库进行测试的时候特别有用。若是你对此感兴趣的话,在Mozilla Developer Network上有一篇相关的文章。
几个注意点:
代理事件监听可让你使用一个事件监听器去监听大量的DOM节点的事件,在这种状况下,它是一种更加方便而且高性能的事件监听方法。举例来讲,若是有 一个列表<ul>包含了100个子元素<li>,它们都须要对click事件作出类似的响应,那么咱们可能须要查询这100个子 元素,并分别为他们添加上事件监听器。这样的话,咱们就会产生100个独立的事件监听器。若是有一个新的元素被添加进去,咱们也须要为它添加一样的监听 器。这种方式不但代价比较大,维护起来也比较麻烦。
代理事件监听可让咱们更简单的处理这种状况。咱们不去监听全部的子元素的click事件,相反,咱们监听他们的父元素<ul>。当一 个<li>元素被点击的时候,这个事件会向上冒泡至<ul>,触发回调函数。咱们能够经过检查事件的event.target属 性来判断具体是哪个<li>被点击了。下面咱们举个简单的例子来讲明:
1
2
3
4
5
6
7
8
9
10
11
12
|
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库是更好的选择,好比 FT Lab的ftdomdelegate。若是你在使用jQuery,你能够在调用.on()方法的时候,将一个选择器做为第二个参数的方式来轻松的实现事件代理。
1
2
3
4
5
|
// Not using event delegation
$( 'li' ).on( 'click' , function (){});
// Using event delegation
$( 'ul' ).on( 'click' , 'li' , function (){});
|
Demo: Delegate event listeners
load事件能够在任何资源(包括被依赖的资源)被加载完成时被触发,这些资源能够是图片,css,脚本,视频,音频等文件,也能够是document或者window。
1
2
3
|
image.addEventListener( 'load' , function (event) {
image.classList.add( 'has-loaded' );
});
|
window.onbeforeunload
让开发人员能够在想用户离开一个页面的时候进行确认。这个在有些应用中很是有用,好比用户不当心关闭浏览器的tab,咱们能够要求用户保存他的修改和数据,不然将会丢失他此次的操做。
1
2
3
4
5
|
window.onbeforeunload =
function
() {
if
(textarea.value != textarea.defaultValue) {
return
'Do you want to leave the page and discard changes?'
;
}
};
|
须要注意的是,对页面添加onbeforeunload
处理会致使浏览器不对页面进行缓存?,这样会影响页面的访问响应时间。 同时,onbeforeunload
的处理函数必须是同步的(synchronous)。
在Financial Times中,咱们使用了一个简单的event.preventDefault相关的技巧防止了Safari在滚动的时候出现的抖动。(手机端开发接触的很少,因此可能有所误解,若是错误,请了解的同窗提点一下。)
1
2
3
|
document.body.addEventListener(
'touchmove'
,
function
(event) {
event.preventDefault();
});
|
须要提醒的是这个操做同时也会阻碍正常的原生滚动条的功能(好比使用overflow:scroll)。为了使得内部的子元素在须要的时候可以使用滚 动条的功能,咱们在支持滚动的元素上监听这个事件,而且在事件对象上设置一个标识属性。在回调函数中,在document这一层,咱们经过对这个扩展的 isScrollable标识属性来判断是否对触摸事件阻止默认的滚动行为。
1
2
3
4
5
6
7
8
9
10
|
// Lower down in the DOM we set a flag
scrollableElement.addEventListener(
'touchmove'
,
function
(event) {
event.isScrollable =
true
;
});
// Higher up the DOM we check for this flag to decide
// whether to let the browser handle the scroll
document.addEventListener(
'touchmove'
,
function
(event) {
if
(!event.isScrollable) event.preventDefault();
});
|
在IE8即一下的版本中,咱们是不能操做事件对象的。做为一个变通方案,咱们将一些扩展的属性设置在event.target节点对向上。
在一些复杂的响应式布局中,对window对象监听resize事件是很是经常使用的一个技巧。仅仅经过css来达到想要的布局效果比较困难。不少时候,咱们须要使用JavaScript来计算并设置一个元素的大小。
1
2
3
|
window.addEventListener(
'resize'
,
function
() {
// update the layout
});
|
我推荐使用防抖动的回调函数来统一调整回调的频率,从而防止布局上极端抖动的状况出现。
如今在项目中,咱们常用CSS来执行一些转换和动画的效果。有些时候,咱们仍是须要知道一个特定动画的结束时间。
1
2
3
|
el.addEventListener(
'transitionEnd'
,
function
() {
// Do stuff
});
|
一些注意点:
animationiteration事件会在当前的动画元素完成一个动画迭代的时候被触发。这个事件很是有用,特别是当咱们想在某个迭代完成后中止一个动画,但又不是在动画过程当中打断它。
1
2
3
4
5
6
7
8
9
10
11
12
|
function
start() {
div.classList.add(
'spin'
);
}
function
stop() {
div.addEventListener(
'animationiteration'
, callback);
function
callback() {
div.classList.remove(
'spin'
);
div.removeEventListener(
'animationiteration'
, callback);
}
}
|
若是你感兴趣的话,我在博客中有另外一篇关于animationiteration事件的文章。
当咱们的应用在加载资源的时候发生了错误,咱们不少时候须要去作点什么,尤为当用户处于一个不稳定的网络状况下。Financial Times中,咱们使用error事件来监测文章中的某些图片加载失败,从而马上隐藏它。因为“DOM Leven 3 Event”规定从新定义了error事件再也不冒泡,咱们可使用以下的两种方式来处理这个事件。
1
2
3
|
imageNode.addEventListener(
'error'
,
function
(event) {
image.style.display =
'none'
;
});
|
不幸的是,addEventListener并不能处理全部的状况。个人同事Kornel给了我一个很好的例子,说明确保图片加载错误回调函数被执行的惟一方式是使用让人诟病内联事件处理函数(inline event handlers)。
1
|
|
缘由是你不能肯定绑定error事件处理函数的代码会在error事件发生以前被执行。而使用内联处理函数意味着在标签被解析而且请求图片的时候,error监听器也将并绑定。
从事件模型的成功上,咱们能够学到不少。咱们能够在咱们的项目中使用相似的解耦的概念。应用中的模块能够有很高的很复杂度,只要它的复杂度被封装隐藏 在一套简单的接口背后。不少前端框架(好比Backbone.js)都是重度基于事件的,使用发布-订阅(publish and subscribe)的方式来处理跨模块间的通讯,这点跟DOM很是类似。
基于事件的架构是极好的。它提供给咱们一套很是简单通用的接口,经过针对这套接口的开发,咱们能完成适应成千上 万不一样设备的应用。经过事件,设备们能准确地告诉咱们正在发生的事情以及发生的时间,让咱们为所欲为地作出响应。咱们再也不顾虑场景背后具体发生的事情,而 是经过一个更高层次的抽象来写出更加使人惊艳的应用。
特别感谢Kornel对这篇文章作出的精彩的技术审查。
原文连接: smashingmagazine 翻译: 伯乐在线 - Owen Chen