Fork me on GitHub

浏览器事件机制

事件触发三阶段

事件触发有三个阶段

  • 捕获阶段:window往事件触发处传播,遇到注册的捕获会触发
  • 传播到事件触发处时触发注册的事件
  • 冒泡阶段从事件触发处往window传播,遇到注册的冒泡事件会触发

事件冒泡与事件捕获

事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念都是为了解决页面中事件流(事件发生顺序)的问题。

1
2
3
<div id="outer">
<p id="inner">Click me!</p>
</div>

上面的代码当中一个div元素当中有一个p子元素,如果两个元素都有一个click的处理函数,那么我们怎么才能知道哪一个函数会首先被触发呢?

事件冒泡

浏览器检查实际点击的元素是否在冒泡阶段中注册了一个事件处理程序,如果是,则运行它 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达最外层祖先元素。

因此在事件冒泡的概念下在p元素上发生click事件的顺序应该是p -> div -> body -> html -> document

事件捕获

浏览器检查元素的最外层祖先,是否在捕获阶段中注册了一个事件处理程序,如果是,则运行它。 然后,它移动到最外层祖先中的下一个元素,并执行相同的操作,然后是下一个元素,依此类推,直到到达实际点击的元素。因此在事件捕获的概念下在p元素上发生click事件的顺序应该是document -> html -> body -> div -> p

事件监听器

事件监听函数 element.addEventListener(<event-name>, <callback>, <use-capture>)。在 element 这个对象上面添加一个事件监听器,当监听到有 事件发生的时候,调用 这个回调函数。至于 这个参数,表示该事件监听是在“捕获”阶段中监听(设置为 true)还是在“冒泡”阶段中监听(设置为 false)。 如果没有指定, useCapture 默认为 false 。

其他事件监听方式
  1. HTML内联属性

类似<button onclick="alert('你点击了这个按钮');">点击这个按钮</button>的方式,这种方式会使 JS 与 HTML 高度耦合,不利于开发和维护,不推荐使用。

  1. DOM属性绑定

使用DOM元素的onXXX属性设置,简单易懂,兼容性好。确定是只能绑定一个处理函数。

1
2
var btn = document.querySelector('button');
btn.onclick = function(event){alert('Hello world');};

事件捕获和事件冒泡同时存在的情况

事件按照什么样的顺序触发的,按照事件触发的三个阶段依次分析就行。但是也有特例,如果给一个目标节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。

1
2
3
4
5
6
7
// 以下会先打印冒泡然后是捕获
node.addEventListener('click',(event) =>{
trueconsole.log('冒泡')
},false);
node.addEventListener('click',(event) =>{
trueconsole.log('捕获 ')
},true)

事件对象

事件处理函数可以附加在各种对象上,包括 DOM元素,window 对象 等。当事件发生时, event 对象就会被创建并依次传递给事件监听器。

在处理函数中,将event对象作为第一个参数参数,可以访问 DOM Event 接口。下面简单的实例则简单说明了 event对象是如何传入事件处理函数,在函数中是如何被使用的。

1
2
3
4
5
function foo(evt) {
// the evt parameter is automatically assigned the event object
alert(evt);
}
table_el.onclick = foo;

event比较常用的属性和方法:

属性

  • event.bubbles(只读):

    返回一个布尔值,表明当前事件是否会向DOM树上层元素冒泡

  • event.cancelable(只读):

    事件的 cancelable 属性表明该事件是否可以被取消默认行为, 如果该事件可以用 preventDefault() 可以阻止与事件关联的默认行为,则返回 true,否则为 false。如果该事件的 cancelable 属性为 false, 则该事件的监听器无法阻止默认行为, 调用preventDefault() 将产生错误.

  • event.currentTarget

    当事件遍历DOM时,标识事件的当前目标。它总是引用事件处理程序附加到的元素,而不是event.target,event.target标识事件发生的元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function hide(e){
    e.currentTarget.style.visibility = "hidden";
    console.log(e.currentTarget);
    // 该函数用作事件处理器时: this === e.currentTarget
    }
    var ps = document.getElementsByTagName('p');
    for(var i = 0; i < ps.length; i++){
    // console: 打印被点击的p元素
    ps[i].addEventListener('click', hide, false);
    }
    // console: 打印body元素
    document.body.addEventListener('click', hide, false);

    上述代码运行后,点击网页中的p节点,由于注册的事件监听器都是冒泡属性,所以会依次打印点击的p节点、和body节点。

  • event.target

    一个触发事件的对象的引用。

  • event.defaultPrevented(只读):

    返回一个布尔值,表明当前事件是否调用了 event.preventDefault()方法。

  • event.type(只读):

    只读属性 Event.type 会返回一个字符串, 表示该事件对象的事件类型。

方法

  • preventDefault()

    这个方法可以禁止一切默认的行为,例如点击 a 标签时,会打开一个新页面,如果为 a 标签监听事件 click 同时调用该方法,则不会打开新页面。

  • event.stopPropagation()

    阻止捕获和冒泡阶段中当前事件的进一步传播。

事件委托

事件监听后,检测顺序就会从被绑定的DOM下滑到触发的元素,再冒泡会绑定的DOM上。也就是说,如果你监听了一个DOM节点,那也就等于你监听了其所有的后代节点。

使用事件委托能够避免对特定的每个节点添加事件监听器;事件监听器是被添加到它们的父元素上。事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件。利用事件冒泡的特性,将里层的事件委托给外层事件,根据event对象的属性进行事件委托,改善性能。

要用到e.target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Make a list
var ul = document.createElement('ul');
document.body.appendChild(ul);
var li1 = document.createElement('li');
var li2 = document.createElement('li');
ul.appendChild(li1);
ul.appendChild(li2);
function hide(e){
// e.target refers to the clicked <li> element
// This is different than e.currentTarget which would refer to the parent <ul> in this context
e.target.style.visibility = 'hidden';
}
// Attach the listener to the list
// It will fire when each <li> is clicked
ul.addEventListener('click', hide, false);
undefined