JavaScript事件相关基础及事件委托

JavaScript 与 HTML 之间的交互是经过事件来实现的, 因此了解事件的基本内容是必要的。文章将讨论 JavaScript 事件的部分相关内容,包括事件流、事件处理程序、事件对象、事件委托,以上内容分别构成了文章的 1~4 小节。html

1 事件流

已知网页的 DOM 节点构成的是树状结构,称为 DOM 树。浏览器

并且又知当页面上的一个节点触发了事件时,接收到事件通知的不只仅只有这个节点,而是沿着它所在的DOM树的一支从 Document 到这个节点自己的全部元素均可以接收到事件的通知,可是接收到通知也是有前后顺序的,事件流就描述了这一支DOM树中上的元素接收到事件通知的顺序。函数

不一样浏览器厂商对于事件流的方向的实现不一样甚至相反,能够分为两种:冒泡流和捕获流。有趣的是,这两种事件流几乎是彻底相反的,下面分别讨论这两种事件流。同时讨论DOM规定的事件流的三个阶段。性能

1.1 事件冒泡

IE的事件流叫作事件冒泡(event bubbling),顾名思义,这是从下到上来的。即从DOM树的下层向上层流动,也就是从具体的元素向较为不具体的元素传播,也能够说是子元素向父元素的方向传播。优化

例如在下面的DOM结构(缩进表示节点的层级)中:ui

html
        head
        body
            div
复制代码

DOM树结构和冒泡事件流方向是下面这样的:this

因此可知若是触发了上面div元素的事件,则接收到事件通知的元素和顺序为:spa

div -> body -> html -> document
复制代码

1.2 事件捕获

与冒泡流相反,事件捕获的思想是从DOM树的最上方传到触发事件的节点自己,也就是从较为不具体的元素传向具体的元素传播,也能够说从父元素向子元素传播。设计

以上一节的DOM结构为例,可知事件捕获流的示意图为:code

即若是在div元素上触发了事件,则受到事件通知的元素的顺序是:

document -> html -> body -> div
复制代码

虽然事件是在div身上触发的,但它倒是最后才收到事件通知的。

1.3 DOM 事件流

DOM2级事件规定的事件流是分阶段的,共分红三个阶段,即:

  1. 捕获阶段
  2. 处于目标阶段
  3. 冒泡阶段

也就是说DOM2级规定的事件流包括了冒泡和捕获这两个不一样方向的事件流,同时加上了一个处于目标的阶段。

须要注意的是DOM2级定义的捕获阶段并不会发生到触发事件的元素自己,而是只到它的上层节点;而处于目标阶段则发生且只发生在触发事件的元素上;冒泡阶段管的是最宽的,它从触发事件的元素自己到DOM树的顶部都会涉及到。

仍然以1.1节中的DOM结构为例,从下面的示意图看这三个阶段会比较清晰(为了方便,略去了无关的head节点):

从上面能够看到,捕获阶段从根节点到body节点就中止了;处于目标阶段只发生在了div身上;冒泡阶段则是从div一直通知到了根节点。

虽然DOM规定了在捕获阶段不要涉及到目标元素,可是大多数浏览器都没有遵循这个规定,都涉及到了目标元素,即以下图所示:

2 事件处理程序

添加事件处理程序的方式能够分红如下几种:

  1. HTML 标签中直接添加处理程序
  2. DOM 0 级处理程序
  3. DOM 2级处理程序
  4. IE 的事件处理程序

2.1 HTML 标签中直接处理事件

能够在HTML标签中直接用onXXX来指定事件处理程序,例如指定元素被点击时输出一个字符串能够这样作:

<div onclick="console.log('div is clicked'); alert('clicked')"></div>
复制代码

则控制台里会输出div is clicked,窗口会弹框输出clicked

也能够调用页面中其余部分的代码,例如调用一个在script标签中定义的函数:

<script>
//  定义了一个函数 fn    
function fn(){
    console.log(this + '调用了函数 fn!');
}
</script>

<!-- 调用页面中的 fn 函数 -->
<div onclick="fn()"></div>
复制代码

注意:通常不会用到这种方式直接处理事件,由于有以下几个缺点:

  1. 若是函数在被解析以前就调用了,则会报错
  2. 这样会致使 HTML 和 JavaScript 的强耦合,改动比较麻烦

2.2 DOM 0 级事件处理程序

这种方式为用 on加上事件名称来指定事件处理程序,例如

element.onClick = handler
复制代码

下面举一个更具体的例子,获取页面上的一个idcontainer的元素,并指定点击事件的处理函数:

<!-- id是target的元素 -->
<div id="target"></div>

<script>
// 获取id是target的元素
let target = document.getElementById('target');

// 给 target 指定点击事件的处理程序
target.onclick = function(){
    console.log('div#target is clicked');
}

</script>
复制代码

当点击 div#target时,控制台会输出div#target is clicked

2.2.1 解绑事件处理程序

能够将事件处理程序指定为 null 来解除原来为元素指定的事件处理程序,例如将上面target的点击事件的处理程序给解绑:

// 解绑点击事件的处理程序
target.onclick = null;
复制代码

这样再点击这个元素就不会有任何反应了

2.2.2 注意

这种事件处理程序的指定方式有以下特色和须要注意的点:

  1. 事件处理程序中的this指向的是这个元素自己,因此能够直接引用它身上的属性,例如给上个例子里的 target元素加上三个类名,并在事件处理程序中经过 this属性来获得它的类名这个属性:

    <div id="target" class="cname1 cname2 cname3"></div>
    
    target.onclick = function(){
        console.log(this.className);
    }
    复制代码

    当点击div#target时控制台会输出:cname1 cname2 cname3

  2. 这种方法只能够指定一个事件处理程序,后面指定的会覆盖前面的,例如:

    // 给 target 指定 第一个 点击事件的处理程序
    target.onclick = function(){
        console.log('我是第一个');
    }
    
    // 给 target 指定 第二个 点击事件的处理程序
    target.onclick = function(){
        console.log('我是第二个');
    }
    
    // 点击 target 时会输出:我是第二个
    复制代码

    经过上面的实验能够看出后面定义的处理程序覆盖了前面的。

2.3 DOM 2 级处理程序

DOM 2 级定义了两个函数来给一个元素绑定和解绑事件处理程序,分别是

  1. addEventListener(eventName, handler, flag)来给调用的元素指定事件处理程序
  2. removeEventListener(eventName, handler, flag)来给调用的元素指定事件处理程序

这两个函数都接收三个参数,分别是:

  • eventName: 事件的名称,注意事件的名称是不以on开头的,例如点击事件名为click
  • handler:事件处理程序,能够是个匿名函数,也能够是实现定义好的具名函数
  • flag:布尔值,可取true || false;当取true时,会在捕获阶段调用事件处理程序,不然会在冒泡阶段调用

例如,为元素div#target使用这个方法添加事件处理程序:

target.addEventListener('click', function(){
    console.log('我是 addEventListener 添加的事件处理程序');
}, false);
// 点击 target 时会输出: 我是 addEventListener 添加的事件处理程序
复制代码

上面的事件处理程序会在冒泡阶段调用, 由于第三个参数 flagfalse

再例,用 removeEventListener 给元素取消某个事件处理程序:

// 添加事件处理程序
target.addEventListener('click', handler, false);
// 在没写下面的代码以前点击 target 会输出 '我是事件处理程序'

// 取消事件处理程序
target.removeEventListener('click', handler, false);
// 如今点击 target 元素就没有什么反应了

// 这个函数会被做为事件处理程序来调用
function handler(){
    console.log('我是事件处理程序');
}
复制代码

2.3.1 注意

这种添加和取消事件处理程序的方法有以下特色:

  • 能够添加多个事件处理程序,会按添加的顺序触发

    // 添加两个事件处理程序
    target.addEventListener('click', function(){
        console.log('我是第一个事件处理程序');
    }, false);
    
    target.addEventListener('click', function(){
        console.log('我是第二个事件处理程序');
    }, false);
    
    /* 点击 target 元素会输出 我是第一个事件处理程序 我是第二个事件处理程序 */
    复制代码
  • 使用 removeEventListener 给元素取消事件处理程序时,参数要和对应的addEventListener的参数彻底对应才能够,这意味着使用匿名函数做为事件处理函数时没法被取消,由于每一个匿名函数都不是同一个:

    target.addEventListener('click', function(){
        console.log(1);
    }, false);
    
    // 试图取消上面的事件处理函数
    target.removeEventListener('click', function(){
        console.log(1);
    }, false);
    
    // 点击 target 仍会输出 1, 由于 removeEventListener 中做为的事件处理程序的匿名函数
    // 并非 addEventListener 中的事件处理函数同样,虽然他们内部定义的行为同样
    复制代码

    因此,若是但愿随时取消一个事件处理程序,那么不要使用匿名函数。

2.4 IE 的事件处理程序

IE 实现了两个与DOM2级类似的方法:

  1. attachEvent(eventName, handler):添加事件处理程序
  2. detachEvent(eventName, handler):解绑事件处理程序

这两个函数都接收两个参数,分别是:

  • eventName: 事件的名称,注意事件的名称与DOM 2级的两个方法addEventListener 和 removeEventListener不一样,此处是以on开头的,例如点击事件名为onclick
  • handler:事件处理程序,能够是个匿名函数,也能够是实现定义好的具名函数

2.4.1 注意

IE 的这两个事件处理程序有几个要注意的点:

  • 第一个参数事件名 eventName 是以 on 开头的,和DOM 2级的两个方法的 eventName 的不以 on开头不一样
  • 一样能够添加多个事件处理程序,可是后面添加的程序会在前面被调用,这和DOM 2级的方法依然不一样
  • 事件处理程序的this属性指向的是全局对象,不是当前的元素
  • 事件处理程序只会在冒泡阶段被调用,不涉及事件捕获阶段

在编写跨浏览器的事件处理程序时,为了保持和IE 浏览器的一致性,应将DOM 2级的两个方法的第三个参数设置为 false ,以在事件冒泡阶段被调用。

3 事件对象

在一个事件被触发时,会产生一个事件对象,这个事件对象包含着与当前事件有关的全部信息,并且会被传入事件处理程序中以便于获得关于事件的全部信息。

全部浏览器都支持事件对象,可是具体实现不一样,能够分红DOM中的事件对象和IE中的事件对象两种。

3.1 DOM 中的事件对象

不管是DOM0级仍是DOM2级都支持事件对象,并且行为相同。

例如,在将事件对象以 event 为名并传入事件处理程序中,并且在其中访问它的一个属性:

target.addEventListener('click', function(event){
    console.log('DOM 2 级中的event.type是: ' + event.type);
}, false);

target.onclick = function(event){
    console.log('DOM 0 级中的event.type是: ' + event.type);
}

/* 点击时会输出 DOM 2 级中的event.type是 click DOM 0 级中的event.type是 click */
复制代码

上面的结果能够看出两种事件的指定方法中的事件对象的type属性值没有区别。

DOM的事件对象中有不少属性和方法可供访问和使用,下面举几个经常使用的例子,更详细的信息能够去红宝书上查阅。

  • currentTarget: 当前事件处理程序在哪一个元素上,因为有事件流,因此事件会在多个元素上被处理,如前文第1节所述所述
  • target:触发事件的元素
  • preventDefault()函数:取消事件的默认行为,这个函数最为熟悉的用法应该是用来取消连接元素<a>的默认跳转行为,若是使用了这个函数,则点击<a>连接时就不会自动跳转了。注意:只对cancelable属性值为true的事件对象才有效
  • stopPropagation()函数:取消事件继续冒泡或者捕获

其中currentTargettarget属性值得区分一下,简单的说前者能够用来判断一个事件已经流到了哪里,也就是已经捕获或者冒泡到了哪里;后者能够用来判断一个事件的触发者是什么元素。例以下面的例子:

定义两层嵌套的div元素,都在事件处理函数中输出currentTargettarget,观察两个元素中的属性差异:

<div id="outer" style="width: 300px; height: 300px; background-color: brown;">
    <div id="inner" style="width: 100px; height: 100px; background-color: cadetblue;"></div>
</div>

outer.addEventListener('click', function(event){
    console.log('>> 这是 outer 的事件处理函数');
    console.log('如今事件冒泡到了 ' + event.currentTarget.id);
    console.log('这个事件的发起元素是 ' + event.target.id);
}, false);

inner.addEventListener('click', function(event){
    console.log('>> 这是 inner 的事件处理函数');
    console.log('如今事件冒泡到了 ' + event.currentTarget.id);
    console.log('这个事件的发起元素是 ' + event.target.id);
}, false);

/*
当点击内部元素时,控制台输出:
    >> 这是 inner 的事件处理函数
    如今事件冒泡到了 inner
    这个事件的发起元素是 inner
    >> 这是 outer 的事件处理函数
    如今事件冒泡到了 outer
    这个事件的发起元素是 inner
*/
复制代码

能够看到 currentTarget属性随着事件流的流向而变化,当事件冒泡到 inner 函数时, currentTarget指向的是 inner,当冒泡到 outer时,currentTarget指向的是 outer

同时能够看出 current 属性始终是指向inner的,由于innerclick事件的触发元素,因此不管事件冒泡到了哪一个元素,事件对象的target属性始终指向的是inner

3.2 IE8及以前的事件对象

IE的事件对象也有本身特有的属性,在IE8以前的事件对象的属性虽然和DOM中的事件对象的属性名称不一样,可是具备对应关系,具体以下:

  • canceBubble属性:布尔类型,当设置为true时能够取消事件冒泡,默认为false
  • returnValue属性:布尔类型,当设置为false时能够取消事件的默认行为,默认值是true
  • srcElement属性:指向事件的触发元素,与DOM中的target属性对应
  • type属性:值为数据类型

IE8及以前中的事件对象的获取方式根据指定方式的不一样而不一样:当经过DOM 0级指定事件处理程序时,事件对象是windowevent属性,这并非默认传入事件处理函数中的:

outer.onclick = function(e){
    var event = window.event;
    console.log(event.src);
}
复制代码

在IE9及以后的版本中,实现了兼容DOM事件对象。因此在IE9及以后,既可使用DOM事件对象,也可使用IE自己特有的事件对象(MSEventObj)。并且在IE11以后,在DOM0级的方式中,经过传递的event对象和从window中取出的event对象均指向同一个对象:

outer.onclick = function(e){
    console.log(e === window.event);  // IE11:true; IE10-IE8: false 
}
复制代码

4 事件委托

页面中事件处理程序的数量会影响到应用的性能,这是由于每一个事件处理函数都是对象,内存中对象越多,则占用内存越多;其次,事件处理程序越多,每每意味着其中的DOM操做越多,而DOM操做的频率是影响页面性能的重要因素。

因为1. 事件流 和 2. 事件对象中target属性始终会指向当前事件的触发元素自己这两个性质的存在,便产生了事件委托策略来减小页面中事件处理程序的绑定数量。

如有下面的DOM结构:

html
	head
    body
    	div#div1
        div#div2
复制代码

对应的DOM树为

能够看到div#containerbutton#btn两个元素的事件流都会通过document元素。

若是想给div#containerbutton#btn两个元素分别加上一个事件处理程序,例如:

div1.addEventListener('click', function(event){
    console.log('>> 这是 div1 的事件处理函数');
}, false);

/* 点击 div1, 会输出: >> 这是 div1 的事件处理函数 */


div2.addEventListener('click', function(event){
    console.log('>> 这是 div2 的事件处理函数');
}, false);

/* 按下 div2, 会输出: >> 这是 div2 的事件处理函数 */
复制代码

上面的例子给两个div元素都添加了一个事件处理函数,可是咱们知道这两个事件都会流到document上,所以能够在document的事件处理程序中使用事件对象的target属性来判断事件的触发元素是谁,并做出相应的处理:

document.addEventListener('click', function(event){
    switch (event.target.id) {  // 取出事件触发者的 id 
        case 'div1':  // 若事件的触发者是 div1
            console.log('>> 这是 div1 的事件处理函数');
            break;
        case 'div2':  // 若事件的触发者是 div2
            console.log('>> 这是 div2 的事件处理函数');
            break;
        default:  
            break;
    }
}, false);

/* 点击 div1, 会输出: >> 这是 div1 的事件处理函数 按下 div2, 会输出: >> 这是 div2 的事件处理函数 */
复制代码

从上面能够看出,灵活的利用事件流和事件对象的属性能够完成事件委托策略,将具体元素的事件委托到顶部元素上处理,这样虽然增长了些许判断的操做,却能显著减小页面中事件处理程序的数量,达到优化性能的目的。

参考文献:《JavaScript高级程序设计》
若有错误,感谢指正~

#完

相关文章
相关标签/搜索