冒泡捕获流、事件与事件源对象、事件委托

冒泡、捕获

事件冒泡

从DOM结构来看,由最里层一层一层的把事件向外传递的现象,叫做冒泡现象。

例如,同时为父子元素添加click的事件处理函数,点击子元素时,父级元素的事件处理函数也会被触发,这就是一种冒泡现象。事件向父级冒泡,然后触发冒泡对应的事件处理函数。

事件捕获

从DOM结构来看,由最外层向最里层把事件向内传递的现象,叫做捕获现象。

总体来说,自嵌套关系最顶层的父级元素开始捕获事件,直到事件源的子元素,事件捕获完成。可以设置addEventListener的第三个参数为true,就是事件捕获,值为false,就是事件冒泡。

下面通过一个案例来观察捕获和冒泡行为。

index.html

<div class="wrapper">
  <div class="outer">
    <div class="inner"></div>
  </div>
</div>

index.css

.wrapper {
  width: 300px;
  height: 300px;
  background-color: green;
}

.wrapper .outer {
  width: 200px;
  height: 200px;
  background-color: orange;
}

.wrapper .outer .inner {
  width: 100px;
  height: 100px;
  background-color: blue;
}

index.js

var wrapper = document.getElementsByClassName('wrapper')[0],
    outer = wrapper.getElementsByClassName('outer')[0],
    inner = outer.getElementsByClassName('inner')[0];

wrapper.addEventListener('click', function () {
  console.log('wrapper');
}, false);

outer.addEventListener('click', function () {
  console.log('outer');
}, false);

inner.addEventListener('click', function () {
  console.log('inner');
}, false);

bubble_capture.png

点击inner盒子,参数为false,冒泡行为时,打印如下信息。

inner
outer
wrapper

点击inner盒子,参数为true,捕获行为时,打印如下信息。

wrapper
outer
inner

由此可以看出冒泡和捕获的执行现象,捕获是从顶层元素到里层元素,冒泡是从里层元素到顶层元素。

此外,两种现象是可以同时触发的,事件捕获是先执行的,事件冒泡是后执行的。事件捕获作用到事件源上时,不存在捕获现象,正常执行函数,不存在冒泡和捕获谁先谁后问题。

outer.addEventListener('click', function () {
  console.log('bubble outer');
}, false);

outer.addEventListener('click', function () {
  console.log('outer');
}, true);

打印:

bubble outer
outer

focus、blur、change、submit、reset、select没有冒泡和捕获现象。
IE浏览器没有事件捕获现象,老版本除chrome浏览器之外,没有事件捕获。

事件流

事件流:用来描述从页面中接收事件的顺序,与冒泡和捕获行为相关。

微软IE提出事件冒泡流(Event Bubbling)。
Netscape(网景)提出事件捕获流(Event Captureing)。

事件流分为3个阶段:

  1. 事件捕获阶段
  2. 处于目标阶段(事件源所绑定的事件处理函数触发)
  3. 事件冒泡阶段

DOM事件级别:

DOM事件级别是对事件类型的定义,是不同时期的DOM事件规范。

1. DOM0级

定义on之类的事件模型(onmouseover、onmouseout),以句柄的方式绑定事件处理函数。
以onlcik为例,支持两种写法。

  1. 元素属性 onclick=""
  2. Element.onclick = function () {}
2. DOM1级

没有定义事件模型。

3. DOM2级

定义addEventListener()、removeEventListener()、使用3个参数,成为W3C规范。
IE9以下,没有遵守规范,必须用attachEvent、detachEvent绑定事件处理函数。

4. DOM3级

对于DOM2级的扩展,增加更多的事件类型(load、scroll、keydown、keyup等)。
允许用户自定义事件。

事件与事件源对象

一旦对某个元素进行某一事件的触发,那么浏览器就会把把这个事件触发以后的详细信息包装成一个对象(e、ev、event),传递到事件处理函数的参数中去。

IE不是传到事件处理函数的参数中,是传到window.event中。

target、srcElement就是事件源对象。

FF(火狐)浏览器只有target属性;
IE浏览器只有srcElement属性;
chrome浏览器两个属性都有;

为此,编写程序时,可以做以下兼容性处理

btn.onclick = function (ev) {
    var e = ev || window.event,
 	tar = e.target || e.srcElement;
    console.log(e);
}

事件委托/事件代理

把事件委托给父级,事件被触发后会冒泡到父级,点击子元素时,父级元素也能得到被点击的事件源对象,根据事件源对象做出相应处理,

事件代理的好处:

  1. 减少多次绑定事件处理函数,对性能优化较好。
  2. 对于动态增加的子元素,可以获取到事件源对象。
  3. 可以动态获取子元素下标。

下面具体以一个案例来演示事件代理

index.html

<button>添加li元素</button>
<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
  <li>7</li>
  <li>8</li>
  <li>9</li>
  <li>10</li>
</ul>

index.js

var oList = document.getElementsByTagName('ul')[0],
    oLi = oList.getElementsByTagName('li'),
    oBtn = document.getElementsByTagName('button')[0];

oList.onclick = function (ev) {
  var e = ev || window.event,
      tar = e.target || e.srcElement,
      tagName = tar.tagName.toLowerCase(),
      idx = [].indexOf.call(oLi, tar);

  if (tagName === 'li') {
    console.log(tar, tar.innerText, tagName, idx);
  }
}

oBtn.onclick = function () {
  var li = document.createElement('li');
  li.innerText = oLi.length + 1;
  oList.appendChild(li);
}

使用事件代理可以避免重复为li元素绑定多个事件处理函数,有效减少性能开销。