DOM 事件介绍 by Wilson Page

原帖地址,翻译自己看看加深理解,欢迎拍砖

http://coding.smashingmagazine.com/2013/11/12/an-introduction-to-dom-events/

 

点击,接触,装载,拖拽,改变,输入,错误,调整大小——这份可能的DOM事件清单相当冗长。事件可以在一个文档的任何一个部分被触发,不论是用户的交互行为还是浏览器。并且无需在同一个位置开始和结束;它们的一整个生命周期都流动在文档中。这个生命周期使得DOM事件可扩展并且非常有用。作为开发人员,应该理解DOM事件的工作机制,以便利用它们的潜能并且构造出迷人的体验。

 

监听事件

 

过去,浏览器在将事件监听和DOM节点捆绑的方式上有着许多不一致。类似jQuery的程序库在抽象出这些古怪的东西方面是无价的。

如今我们更接近标准化的浏览器环境,可以更加安全的通过官方规范使用API。为了保持简洁,我会相对现代Web去描述如何控制事件。如果你正在为IE8及以下的浏览器编写JS,我会建议使用polyfill 或者框架(例如jQuery)去管理事件监听。

 

Javascript中,我们可以用以下方法去监听事件:

element.addEventListener(<event-name>, <callback>, <use-capture>);
  • event-name (string)
    这是将要监听的事件的名字或类型。可以是任何标准DOM事件(click,mousedown,touchstart,transitionEnd 等等)或者是自定义的事件名(后文会涉及到自定义事件)。
  • callback (function)
    当事件发生时这个函数被调用。包含相关数据的事件对象,将作为第一个参数传递。
  • use-capture (boolean)
    声明callback是否应该在捕获阶段被触发。(后文解释)
var element = document.getElementById('element');

function callback() {
  alert('Hello');
}

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

移除监听

当监听不再需要时移除事件监听是最好的练习(尤其是长时间运行的Web应用程序)。执行此操作,使用 element.removeEventListener()方法:
element.removeEventListener(<event-name>, <callback>, <use-capture>);
但是removeEventListener有一个问题:你必须引用一个已经被绑定的回调函数。仅仅调用element.removeEventListener('click');不会有作用。

实际上,如果我们对于移除事件监听有兴趣(在那些“长寿的”应用程序中应该用到),我们需要保持对于回调函数的处理。这意味着我们不能使用匿名函数。

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

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

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

维护回调函数的内容
一个简单的问题就是回调函数调用时包含不正确的内容。用一个简单的例子解释。
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函数到 addEventListener方法时,我们只是传递了一个函数的引用;user的内容并没有一起被传递。
在内部,回调函数在element的内容中被调用,意味着this指向了element,而不是user。因此。 this.firstname未定义。
有两种方式避免内容不匹配。首先,我们可以在一个匿名函数中使用正确的内容调用 user.greeting()
element.addEventListener('click', function() {
  user.greeting();
  // alert => 'My name is Wilson'
});
FUNCTION.PROTOTYPE.BIND

上一个方法不是很好因为现在当我们想要通过 .removeEventListener()移除时我们不能控制函数。不仅如此,这点还让人厌恶。我更倾向于使用.bind()方法(自ECMAScript5开始可以用于所有函数)创建一个新的函数(bound),它将在给出的内容中持续运行。然后我们可以将此函数作为回调函数传递给 .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 (string)
    事件名。
  • target (node)
    事件起源的DOM节点。
  • currentTarget (node)
    事件的回调函数当前执行的节点。(这个fire实在不知道怎么翻译准确)
  • bubbles (boolean)
    生命是否是“冒泡”事件(后面解释)。
  • preventDefault (function)
    防止任何的默认行为发生,也就是用户代理(也就是浏览器)可能会关于事件进行的执行(例如,阻止在一个a(链接)元素点击时加载新页面)。
  • stopPropagation (function)
    阻止任何的回调函数在远离事件链的节点上执行,但是并不阻止同样事件名的附加回调函数在当前节点上执行。(后文讨论)
  • stopImmediatePropagation (function)
    阻止任何的回调函数在远离事件链的节点上执行,包括上述附加的回调函数。
  • cancelable (boolean)
    声明事件的默认行为是否能通过调用event.preventDefault方法被阻止。
  • defaultPrevented (boolean)
    声明preventDefault方法是否已经在对象事件中调用。
  • isTrusted (boolean)
    一个事件会被“trusted”的前提是,当它是从本身起源,而不是从JavaScript内部合成。
  • eventPhase (number)
    这个数字代表事件当前执行的阶段:没有(0),捕获(1),目标(2),冒泡(3)。下文介绍事件阶段。
  • timestamp (number)
    事件发生的时间。
许多其他的属性可以在事件对象中找到,但是它们相对于在讨论中的事件类型是特定的。例如,鼠标事件将会包括clientX和clientY属性,在事件对象中表示指针在视窗中的定位。
最好使用浏览器调试器或者 console.log来更了解事件对象和它的属性。

事件阶段

当一个DOM事件在你的应用程序上执行,不止在事件起源的地方执行一次;它会发生于三个阶段。简言之,事件会从文档的根到目标(即捕获阶段)之间流动,然后在事件目标执行(目标阶段),最后回到文档的根(冒泡阶段)。


图片资源来自:W3C

捕获阶段

第一阶段就是捕获阶段。事件从文档的根开始它的流程,依次通过DOM的每一层,在没一个节点上执行知道到达事件目标。捕获阶段的工作就是建立传播路径——在冒泡阶段返回的路径。
如上所述,我们可以通过将 addEventListener的第三个参数设置为true来监听捕获阶段的事件。我没有找到许多应用捕获阶段监听的实例,但是如果事件在捕获阶段,你可以阻止任何点击行为触发某一个元素。

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

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

如果不确定,可以将 useCapture标记设置为false或者undefined来监听冒泡阶段的事件。

目标阶段

事件到达目标被称为目标阶段。在反向折回之前,事件在目标节点上执行,并传播回文档的最外层。
至于嵌入的元素,鼠标或指针事件通常会在最内部嵌入的元素上被当做目标。如果你已经监听到针对一个div元素的click事件,并且用户也点击了这个div中的p元素,那么p元素将会成为事件目标。事实上,事件冒泡的意思是你可以监听div(或者任何其他的祖先节点)上的点击行为并且事件通过时仍然会接受一个回调函数。

冒泡阶段

事件在目标上执行之后,并不会停止。它会向上冒泡(或传播)通过整个DOM直到文档的根。这意味着同一个事件会在目标的父节点执行,然后是父节点的父节点,直到没有父节点供事件传递。
将DOM看做一个洋葱,事件目标看做洋葱的心。在捕获阶段,事件钻进洋葱并通过每一层。当事件到达洋葱心时(洋葱不是没有心嘛- -这怎么翻译,理解就成吧。。),它就执行(目标阶段),然后反向通过每一层回去(传播阶段)。当事件回到了表层时,行程结束。
冒泡是很有用的。它使我们无需在准确的元素上监听事件;取而代之的是,监听DOM树更深层的元素,等待事件到达。如果事件不冒泡,在某些情况下,我们不得不在许多不同的元素上监听事件来确认它是否被捕捉。
大多数的事件都冒泡,但不是全部。当事件不冒泡时,通常都有正当理由。如果有疑问, 点击这里

停止传播

在行程中的任何一点(捕获或冒泡阶段)中断事件的路径可以通过对事件对象调用 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
});

阻止浏览器的默认行为

当某个文档中的事件发生时,浏览器的默认事件将会响应。最普遍的事件就是一个被点击的链接。当一个click事件在一个a元素上发生时,将会向上冒泡到DOM的文档层,浏览器则会解释href属性,并且在一个新的地址重载窗口。
在Web应用中,开发人员通常希望能够自己控制导航,无需引起页面的刷新。为此,我们需要阻止浏览器对点击的的默认响应,然后做我们自己的事。要执行此操作,调用 event.preventDefault()

anchor.addEventListener('click', function(event) {
  event.preventDefault();
  // Do our own thing
});

我们可以阻止许多其他的浏览器默认行为。例如在一个HTML5的游戏中,我们可以阻止按下空格键会滚动页面的行为。
这里调用 event.stopPropagation()只会阻止进一步依附在传播链的回调函数被执行。不会阻止浏览器执行此类行为。

自定义事件

浏览器不是唯一能够触发DOM事件的东西。我们可以创建自己的自定义事件然后将它们分派给文档中的任何元素。这一事件类型与普通的DOM事件一样运作。

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);

在元素上(例如click)合成“untrusted”DOM事件来模仿用户交互也是可行的。这对于检测与DOM相关的库来说是很有用的。如果有兴趣,Mozilla Developer Network上有一篇 相关报告
注意以下两点:
  • CustomEvent 接口在IE8及以下版本不适用
  • Twitter 的Flight 框架利用自定义事件在组件之间通信。执行了一个高度不挂钩的,模块化的架构。(This enforces a highly decoupled, modular architecture. 不太理解,贴上来吧

委派事件监听

委派事件监听是在一个具有大量DOM节点的文档中运用单独的事件监听器监听事件的一个更便捷、高性能的方式。例如,如果一个列表包含100项,并且需要以相似的方式响应一个click事件,我们可以查询DOM中的所有列表项然后将每一项附加一个事件监听器。这样会引起100个单独的事件监听器。不论什么时候一个新的项被加入到列表中,click的事件监听器都会被加入。这样不仅风险很大,而且难以维护。
委派事件监听可以让生活更简单。我们在父节点ul元素上进行监听来代替在每个元素上监听click事件。当一个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 实验室的 ftdomdelegate。如果你在使用jQuery,你可以通过传递一个选择器作为 .on()方法第二个参数来无缝地使用事件委派。

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

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

有用的事件

LOAD

load事件可以在任何已经载入完成的资源(包含从属的资源)上执行。可以是图片,样式表,脚本,视频,音频文件,文档或者窗口。

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

ONBEFOREUNLOAD

window.onbeforeunload 使开发人员能够询问用户来确认他们是否想离开此页面。这在需要用户保存修改的应用程序中很有用,因为如果浏览器的标签突然关闭时这些修改会丢失。

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

注意分配一个 onbeforeunload处理程序防止浏览器 捕捉页面,从而使回访减慢。另外, onbeforeunload处理程序必须同步。

STOPPING WINDOW BOUNCE IN MOBILE SAFARI

在金融时报网站上,我们使用了一个简单的 event.preventDefault技巧防止窗口滚动时手机版Safari弹出窗口。

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

注意这也会阻止原生的滚动运作(如 overflow: scroll)。为了允许在需要的的元素子集上使用原生滚动,我们在需要滚动效果的元素上监听同一事件并且在事件对象上设置标记。在文档级的回调函数中,我们根据 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对象上监听改变大小事件对于复杂的响应式布局是非常有用的。仅用CSS实现的布局通常是不可能的。当窗口调整大小或者设备的定位改变时,我们就需要重新调整尺寸。

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

我建议使用一个 debounced 回调函数来标准化回调率并且阻止强制同步布局。

TRANSITIONEND

如今我们使用CSS使我们程序中的过渡和动画变得强大。但是有时候,我们仍然需要知道某个特殊的动画什么时候结束的。
el.addEventListener('transitionEnd', function() {
 // Do stuff
});

注意以下几点:

  • 如果你在使用@keyframe 动画,使用animationEnd事件来代替transitionEnd
  • 像许多事件一样,transitionEnd也冒泡。记住在任何的子孙过渡事件调用event.stopPropagation()或者检查event.target来防止不应该运行的回调函数在逻辑上运行。
  • 事件名仍然是广泛的使用前缀(例如webkitTransitionEndmsTransitionEnd等等)。使用类似Modernizr的库来确保事件名正确的前缀。

ANIMATIONITERATION

animationiteration事件每当当前动画元素完成一次迭代就执行。如果我们需要停止一个动画但并不在中途停止时这个事件是有用的。

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

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

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

如果有兴趣,查看作者博客的这篇关于 animationiteration更详细介绍的文章。

ERROR

如果当资源载入时错误发生,我们可能想要对其进行处理,尤其是如果用户正在片状连接时。金融时报网使用了这个事件检测在一篇文章中载入失败的图片并且立即隐藏这些图片。因为 “DOM Level 3 Events” 规范重新定义了error事件为“不冒泡的”,我们可以用两种方式之一来控制该事件。

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

不幸的是, addEventListener不会处理所有的用例。我的同事Kornel曾经向我提出了一个 例子遗憾地证实了确保图片error事件回调函数的执行只有使用内联事件处理程序一种方法。

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

这个情况的原因是,你无法确定捆绑了error事件处理程序的代码是否在error事件发生之前被执行。使用内联的处理程序意味着当标记被解析,图片被请求时,我们的error监听器会被附加。

从事件模型中学到的


从DOM事件模型的成功,我们可以学到很多。在我们的项目中可以使用类似的不挂钩的概念。一个应用程序的模块可以要多复杂有多复杂,只要在一个简单的接口之后将这种复杂性封装起来(so long as that complexity is sealed away behind a simple interface不理解)。许多前端框架(例如Backbone.js)是强烈的基于事件的,解决了与DOM相似的发布-订阅模型中的跨模块通信。
基于事件的架构是很棒的。让我们有了一个简单通用的接口来编写通过成千上万的装置对物理的交互行为响应的应用程序。通过事件,装置准确的告诉我们发生了什么,何时发生的,并且让我们可以随兴的回应。在这些事件之后发生的事无需关心;我们获得了一个能够让我们自由的去建立了不起的应用的抽象层次。
1、资源项目源码均已通过严格测试验证,保证能够正常运行;、 2项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行;、 2项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 、 1资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看READmE.文件(md如有),本项目仅用作交流学习参考,请切勿用于商业用途。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值