DOM 事宜详解

Click、touch、load、drag、change、input、error、risize — 这些都是冗杂的DOM(文档对象模子)事宜列表的一部份。事宜可以在文档(Document)构造的任何部份被触发,触发者可以是用户操纵,也可以是浏览器本身。事宜并不是只是在一处被触发和住手;他们在全部document中活动,具有它们本身的生命周期。而这个生命周期让DOM事宜有更多的用处和可扩大性。

作为一个开辟人员,我们必需要明白DOM事宜是怎样事变的,然后才更好的驾御它,运用它们潜伏的上风,开辟出更高交互性的介入体验(engaging experiences)。

反观我做前端开辟的这么长时候里,我以为我从来没有看到过一个关于DOM事宜是怎样事变的较为直接准确的诠释。本日我的目标就是在这个课题上给人人一个清楚的引见,让人人可以更疾速的相识它。 我首先会引见DOM事宜的基础运用体式格局,然后会深切发掘事宜内部的事变机制,诠释我们怎样运用这些机制来处置惩罚一些罕见的题目。

监听事宜

在过去,主流浏览器之间关于怎样给DOM节点增加事宜监听有着很大的不一致性。jQuery如许的前端库为我们封装和笼统了这些差别行动,为事宜处置惩罚带来了极大的轻易。

现在,我们正一步步走向一个规范化的浏览器时期,我们可以越发安全地运用官方范例的接口。为了简朴起见,这篇文章将重要引见在当代浏览器中怎样治理事宜。假如你在为IE8或许更低版本写JavaScript,我会引荐你运用 polyfill 或许一些框架(如jQuery)来治理事宜监听。

在JavaScript中,我们运用以下的体式格局为元素增加事宜监听:

element.addEventListener(<event-name>, <callback>, <use-capture>);
  • event-name(string)
    这是你想监听的事宜的称号或范例。它可以是任何的规范DOM事宜(click, mousedown, touchstart, transitionEnd,等等),固然也可以是你本身定义的事宜称号(我们会在背面引见自定义事宜相干内容)。
  • callback(function)(回调函数)
    这个函数会在事宜触发的时刻被挪用。相应的事宜(event)对象,以及事宜的数据,会被作为第一个参数传入这个函数。
  • use-capture(boolean)
    这个参数决议了回调函数(callback)是不是在“捕捉(capture)”阶段被触发。不必忧郁,我们稍后会对此做细致的诠释。
var element = document.getElementById('element');
function callback() {
  alert('Hello');
}

// Add listener
element.addEventListener('click', callback);

Demo: addEventListener

移除监听

移除不再运用的事宜监听是一个最好实践(迥殊关于长时候运转的Web运用)。我们运用element.removeEventListener()要领来移除事宜监听:

element.removeEventListener(<event-name>, <callback>, <use-capture>);

然则removeElementListener有一点须要注重的是:你必需要有这个被绑定的回调函数的援用。简朴地挪用element.removeEventListener(‘click’);是不能抵达想要的效果的。

本质上来说,假如我们斟酌要移除事宜监听(我们在长时候运转(long-lived)的运用中须要用到),那末我们就须要保留回调函数的句柄。意义就是说,我们不能运用匿名函数作为回调函数。

var element = document.getElementById('element');

function callback() {
  alert('Hello once');
  element.removeEventListener('click', callback);
}

// Add listener
element.addEventListener('click', callback);

Demo: removeEventListener

保护回调函数高低文

一个很轻易碰到的题目就是回调函数没有在料想的运转高低文被挪用。让我们看一个简朴的例子来诠释一下:

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

运用匿名函数(Anonymous Functions)

我们愿望回调函数中可以准确的输出”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)。

element.addEventListener('click', function() {
  user.greeting();
  // alert => 'My name is Wilson'
});

Demo:Anonymouse functions

运用Function.prototype.bind

上一种体式格局并不是异常好,因为我们不能取得回调函数的句柄以便背面经由历程.removeEventListener()移除事宜监听。别的,这类体式格局也比较貌寝。。我更喜好运用.bind()要领(做为ECMAScript 5的规范内建在一切的函数对象中)来天生一个新的函数(被绑定过的函数),这个函数会在指定的高低文中被实行。然后我们将这个被绑定过的函数作为参数传给.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);
与此同时,我们取得了回调函数的句柄,从而可以随时从元素上移除相应的事宜监听。
```javascrips
button.removeEventListener('click', user.greeting);

DEMO:Function.ptototype.bind
想猎取Function.prototype.bind的更多信息,请点击的浏览器支撑页面,以及polyfill的引见。

Event 对象

Event对象在event第一次触发的时刻被竖立出来,而且一向伴跟着事宜在DOM构造中流转的全部生命周期。event对象会被作为第一个参数通报给事宜监听的回调函数。我们可以经由历程这个event对象来猎取到大批当前事宜相干的信息:
* type (String) — 事宜的称号
* target (node) — 事宜劈头的DOM节点
* currentTarget?(node) — 当前回调函数被触发的DOM节点(背面会做比较细致的引见)
* bubbles (boolean) — 指明这个事宜是不是是一个冒泡事宜(接下来会做诠释)
* preventDefault(function) — 这个要领将阻挠浏览器中用户代办对当前事宜的相干默许行动被触发。比方阻挠 < a > 元素的click事宜加载一个新的页面
* stopPropagation (function) — 这个要领将阻挠当前事宜链上背面的元素的回调函数被触发,当前节点上针对此事宜的其他回调函数依旧会被触发。(我们稍后会细致引见。)
* stopImmediatePropagation (function) — 这个要领将阻挠当前事宜链上一切的回调函数被触发,也包括当前节点上针对此事宜已绑定的其他回调函数。
* cancelable (boolean) — 这个变量指明这个事宜的默许行动是不是可以经由历程挪用event.preventDefault来阻挠。也就是说,只需cancelable为true的时刻,挪用event.preventDefault才见效。
* defaultPrevented (boolean) — 这个状态变量表明当前事宜对象的preventDefault要领是不是被挪用过
* isTrusted (boolean) — 假如一个事宜是由装备本身(如浏览器)触发的,而不是经由历程JavaScript模仿合成的,谁人这个事宜被称为可信托的(trusted)
*eventPhase (number) — 这个数字变量示意当前这个事宜所处的阶段(phase):none(0), capture(1),target(2),bubbling(3)。我们会在下一个部份引见事宜的各个阶段
*timestamp (number) — 事宜发作的时候

另外事宜对象还能够具有许多其他的属性,然则他们都是针对特定的event的。比方,鼠标事宜包括clientX和clientY属性来表明鼠标在当前视窗的位置。

我们可以运用熟习的浏览器的调试东西或许经由历程console.log在控制台输出来更详细地检察事宜对象以及它的属性。

事宜阶段(Event Phases)

当一个DOM事宜被触发的时刻,它并不只是在它的劈头对象上触发一次,而是会阅历三个差别的阶段。简而言之:事宜一最先从文档的根节点流向目标对象(捕捉阶段),然后在目标对向上被触发(目标阶段),以后再回溯到文档的根节点(冒泡阶段)。
《DOM 事宜详解》
(图片泉源:W3C

Demo: Slow motion event path

事宜捕捉阶段(Capture Phase)

事宜的第一个阶段是捕捉阶段。事宜从文档的根节点动身,跟着DOM树的构造向事宜的目标节点流去。途中经由各个条理的DOM节点,并在各节点上触发捕捉事宜,直到抵达事宜的目标节点。捕捉阶段的重要任务是竖立流传门路,在冒泡阶段,事宜会经由历程这个门路回溯到文档跟节点。

正如文章一最先的处所提到,我们可以经由历程将addEventListener的第三个参数设置成true来为事宜的捕捉阶段增加监听回调函数。在现实运用中,我们并没有太多运用捕捉阶段监听的用例,然则经由历程在捕捉阶段对事宜的处置惩罚,我们可以阻挠类似clicks事宜在某个特定元素上被触发。

var form = document.querySelector('form');

form.addEventListener('click', function(event) {
  event.stopPropagation();
}, true); // Note: 'true'

假如你对这类用法不是很相识的话,最好照样将useCapture设置为false或许undefined,从而在冒泡阶段对事宜举行监听。

目标阶段(Target Phase)

当事宜抵达目标节点的,事宜就进入了目标阶段。事宜在目标节点上被触发,然后会逆向回流,直到流传至最外层的文档节点。

关于多层嵌套的节点,鼠标和指针事宜常常会被定位到最里层的元素上。假定,你在一个< div >元素上设置了click事宜的监听函数,而用户点击在了这个< div >元素内部的< p >元素上,那末< p >元素就是这个事宜的目标元素。事宜冒泡让我们可以在这个< div >(或许更上层的)元素上监听click事宜,而且事宜流传历程当中触发还调函数。

冒泡阶段(Bubble Phase)

事宜在目标元素上触发后,并不在这个元素上住手。它会跟着DOM树一层层向上冒泡,直到抵达最外层的根节点。也就是说,同一个事宜会顺次在目标节点的父节点,父节点的父节点。。。直到最外层的节点上被触发。

将DOM构造设想成一个洋葱,事宜目标是这个洋葱的中间。在捕捉阶段,事宜从最外层钻入洋葱,穿过门路的每一层。在抵达中间后,事宜被触发(目标阶段)。然后事宜最先回溯,再次经由每一层返回(冒泡阶段)。当抵达洋葱外表的时刻,此次路程就完毕了。

冒泡历程异常有效。它将我们从对特定元素的事宜监听中释放出来,相反,我们可以监听DOM树上更上层的元素,守候事宜冒泡的抵达。假如没有事宜冒泡,在某些状况下,我们须要监听许多差别的元夙来确保捕捉到想要的事宜。

Demo: Identifying event phases

绝大多数事宜会冒泡,但并不是一切的。当你发明有些事宜不冒泡的时刻,它肯定是有缘由的。不相信?你可以检察一下相应的范例申明

住手流传(Stopping Propagation)

可以经由历程挪用事宜对象的stopPropagation要领,在任何阶段(捕捉阶段或许冒泡阶段)中缀事宜的流传。今后,事宜不会在背面流传历程当中的经由的节点上挪用任何的监听函数。

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()要领。

child.addEventListener('click', function(event) {
 event.stopImmediatePropagation();
});

child.addEventListener('click', function(event) {
 // If the child element is clicked
 // this callback will not fire
});

Demo:Stopping propagation

阻挠浏览器默许行动

当特定事宜发作的时刻,浏览器会有一些默许的行动作为回响反映。最罕见的事宜不过于link被点击。当一个click事宜在一个< a >元素上被触发时,它会向上冒泡直到DOM构造的最外层document,浏览器会诠释href属性,而且在窗口中加载新地址的内容。

在web运用中,开辟人员常常愿望可以自行治理导航(navigation)信息,而不是经由历程革新页面。为了完成这个目标,我们须要阻挠浏览器针对点击事宜的默许行动,而运用我们本身的处置惩罚体式格局。这时候,我们就须要挪用event.preventDefault().

anchor.addEventListener('click', function(event) {
  event.preventDefault();
  // Do our own thing
});
我们可以阻挠浏览器的许多其他默许行动。比方,我们可以在HTML5游戏中阻挠敲击空格时的页面转动行动,或许阻挠文本挑选框的点击行动。

挪用event.stopPropagation()只会阻挠流传链中后续的回调函数被触发。它不会阻挠浏览器的本身的行动。

[Demo:Preventing default vehaviour](http://jsbin.com/ibotap/1/edit)

---

###自定义事宜
浏览器并不是唯一能触发DOM事宜的载体。我们可以竖立自定义的事宜并把它们分配给你文档中的恣意节点。这些自定义的事宜和一般的DOM事宜有雷同的行动。
```javascrips
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上有一篇相干的文章

几个注重点:

  • CustomEvent接口在IE 8以及IE更低版本不可用
  • 来自Twitter的Flight框架运用了自定义事宜举行模块间通讯。它强调了一种高度解耦的模块化架构。
    Demo:Custom events

代办事宜监听

代办事宜监听可以让你运用一个事宜监听器去监听大批的DOM节点的事宜,在这类状况下,它是一种越发轻易而且高性能的事宜监听要领。举例来说,假如有一个列表< ul >包括了100个子元素< li >,它们都须要对click事宜做出类似的相应,那末我们能够须要查询这100个子元素,并分别为他们增加上事宜监听器。如许的话,我们就会发生100个自力的事宜监听器。假如有一个新的元素被增加进去,我们也须要为它增加一样的监听器。这类体式格局不只价值比较大,保护起来也比较贫苦。

代办事宜监听可以让我们更简朴的处置惩罚这类状况。我们不去监听一切的子元素的click事宜,相反,我们监听他们的父元素< ul >。当一个< li >元素被点击的时刻,这个事宜会向上冒泡至< ul >,触发还调函数。我们可以经由历程搜检事宜的event.target属性来推断详细是哪个< 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库是更好的挑选,比方 FT Lab的ftdomdelegate。假如你在运用jQuery,你可以在挪用.on()要领的时刻,将一个挑选器作为第二个参数的体式格局来轻松的完成事宜代办。

// Not using event delegation
$('li').on('click', function(){});

// Using event delegation
$('ul').on('click', 'li', function(){});

Demo: Delegate event listeners

一些有效的事宜

load
load事宜可以在任何资本(包括被依靠的资本)被加载完成时被触发,这些资本可以是图片,css,剧本,视频,音频等文件,也可以是document或许window。

image.addEventListener('load', function(event) {
  image.classList.add('has-loaded');
});

Demo:Image load event

onbeforeunload
window.onbeforeunload让开辟人员可以在想用户脱离一个页面的时刻举行确认。这个在有些运用中异常有效,比方用户不小心封闭浏览器的tab,我们可以要求用户保留他的修正和数据,不然将会丧失他此次的操纵。

window.onbeforeunload = function() {
  if (textarea.value != textarea.defaultValue) {
    return 'Do you want to leave the page and discard changes?';
  }
};

须要注重的是,对页面增加onbeforeunload处置惩罚会致使浏览器不对页面举行缓存?,如许会影响页面的接见相应时候。 同时,onbeforeunload的处置惩罚函数必需是同步的(synchronous)。

Demo: onbeforeunload

在手机Safari上阻挠窗口发抖

在Financial Times中,我们运用了一个简朴的event.preventDefault相干的技能防备了Safari在转动的时刻涌现的发抖。(手机端开辟打仗的不多,所以能够有所误会,假如毛病,请相识的同砚提点一下。)

document.body.addEventListener('touchmove', function(event) {
 event.preventDefault();
});

须要提示的是这个操纵同时也会障碍一般的原生转动条的功用(比方运用overflow:scroll)。为了使得内部的子元素在须要的时刻可以运用转动条的功用,我们在支撑转动的元素上监听这个事宜,而且在事宜对象上设置一个标识属性。在回调函数中,在document这一层,我们经由历程对这个扩大的isScrollable标识属性来推断是不是对触摸事宜阻挠默许的转动行动。

// 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节点对向上。

resize

在一些庞杂的相应式规划中,对window对象监听resize事宜是异常经常使用的一个技能。仅仅经由历程css来抵达想要的规划效果比较难题。许多时刻,我们须要运用JavaScript来盘算并设置一个元素的大小。

window.addEventListener('resize', function() {
  // update the layout
});

我引荐运用防发抖的回调函数来一致调解回调的频次,从而防备规划上极度发抖的状况涌现。

Demo: Window resizing

transitionend

现在在项目中,我们常常运用CSS来实行一些转换和动画的效果。有些时刻,我们照样须要晓得一个特定动画的完毕时候。

el.addEventListener('transitionEnd', function() {
 // Do stuff
});

一些注重点:

假如你运用@keyframe动画,那末运用animationEnd事宜,而不是transitionEnd。
跟许多事宜一样,transitionEnd也向上冒泡。记得在子节点上挪用event.stopPropagation()或许搜检event.target来防备回调函数在不该被挪用的时刻被挪用。
事宜名现在照样被种种供应商增加了差别的前缀(比方webkitTransitionEnd, msTransitionEnd等等)。运用类似于Modernizr的库来猎取准确的事宜前缀。
Demo:Transition end

animtioniteration

animationiteration事宜会在当前的动画元素完成一个动画迭代的时刻被触发。这个事宜异常有效,迥殊是当我们想在某个迭代完成后住手一个动画,但又不是在动画历程当中打断它。

function start() {
  div.classList.add('spin');
}

function stop() {
  div.addEventListener('animationiteration', callback);

  function callback() {
    div.classList.remove('spin');
    div.removeEventListener('animationiteration', callback);
  }
}

假如你感兴趣的话,我在博客中有另一篇关于animationiteration事宜的文章。

Demo:Animation iteration

error

当我们的运用在加载资本的时刻发作了毛病,我们许多时刻须要去做点什么,迥殊当用户处于一个不稳定的收集状况下。Financial Times中,我们运用error事宜来监测文章中的某些图片加载失利,从而马上隐蔽它。因为“DOM Leven 3 Event”划定从新定义了error事宜不再冒泡,我们可以运用以下的两种体式格局来处置惩罚这个事宜。

imageNode.addEventListener('error', function(event) {
  image.style.display = 'none';
});

不幸的是,addEventListener并不能处置惩罚一切的状况。我的同事Kornel给了我一个很好的例子,申明确保图片加载毛病回调函数被实行的唯一体式格局是运用让人诟病内联事宜处置惩罚函数(inline event handlers)。

<img src="http://example.com/image.jpg" onerror="this.style.display='none';" />

缘由是你不能肯定绑定error事宜处置惩罚函数的代码会在error事宜发作之前被实行。而运用内联处置惩罚函数意味着在标签被剖析而且要求图片的时刻,error监听器也将并绑定。

Demo:Image error

从事宜模子中学到

从事宜模子的胜利上,我们可以学到许多。我们可以在我们的项目中运用类似的解耦的观点。运用中的模块可以有很高的很庞杂度,只需它的庞杂度被封装隐蔽在一套简朴的接口背地。许多前端框架(比方Backbone.js)都是重度基于事宜的,运用宣布-定阅(publish and subscribe)的体式格局来处置惩罚跨模块间的通讯,这点跟DOM异常类似。

基于事宜的架构是极好的。它提供给我们一套异常简朴通用的接口,经由历程针对这套接口的开辟,我们能完成顺应不计其数差别装备的运用。经由历程事宜,装备们能准确地通知我们正在发作的事变以及发作的时候,让我们为所欲为地做出相应。我们不再挂念场景背地详细发作的事变,而是经由历程一个更高条理的笼统来写出越发使人冷艳的运用。

进一步浏览

原文链接: smashingmagazine
转载自: 伯乐在线Owen Chen

    原文作者:JeOam
    原文地址: https://segmentfault.com/a/1190000000352393
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞