js的事件学习笔记

0、参考

JavaScript高级程序设计(第三版),第13章

1、事件流

js的事件流分为捕获和冒泡两类,目前主流的方式是使用冒泡,在特殊情况下才会启用捕获(比如这种需求:一个div中有多个子元素,希望可以用鼠标在页面上拖动整个div而不触发子元素的事件,就可以用事件捕获)

冒泡传播

考虑如下的DOM结构,外面有3层div盒子,最内部有一个button按钮

<div class="box1">
<div class="box2">
<div class="box3">
<button class="btn">
点击按钮
</button>
</div>
</div>
</div>

buttondiv分别注册事件,查看当点击按钮时会发生的执行结果

var box1 = document.getElementsByClassName('box1')[0];
var box2 = document.getElementsByClassName('box2')[0];
var box3 = document.getElementsByClassName('box3')[0];
var btn = document.getElementsByClassName('btn')[0]; btn.onclick = function () {
console.log('btn被触发')
}; box3.onclick = function () {
console.log('box3被触发')
}; box2.onclick = function () {
console.log('box2被触发')
}; box1.onclick = function () {
console.log('box1被触发')
};

执行结果:

btn被触发 m5-js.js:27:5
box3被触发 m5-js.js:32:5
box2被触发 m5-js.js:37:5
box1被触发 m5-js.js:41:5

事件捕获

依然考虑如下的DOM结构

<div class="box1">
<div class="box2">
<div class="box3">
<button class="btn">
点击按钮
</button>
</div>
</div>
</div>

使用addEventListener接口可以指定捕获或者冒泡阶段


var box1 = document.getElementsByClassName('box1')[0];
var box2 = document.getElementsByClassName('box2')[0];
var box3 = document.getElementsByClassName('box3')[0];
var btn = document.getElementsByClassName('btn')[0]; box1.addEventListener('click', function (evt) {
console.log('捕获阶段--box1被触发');
}, true); box2.addEventListener('click', function (evt) {
console.log('捕获阶段--box2被触发');
}, true); box3.addEventListener('click', function (evt) {
console.log('捕获阶段--box3被触发');
}, true); btn.addEventListener('click', function (evt) {
console.log('捕获阶段--btn被触发');
}, true); btn.addEventListener('click', function (evt) {
console.log('冒泡阶段--btn被触发');
}, false); box1.addEventListener('click', function (evt) {
console.log('冒泡阶段--box1被触发');
}, false); box2.addEventListener('click', function (evt) {
console.log('冒泡阶段--box2被触发');
}, false); box3.addEventListener('click', function (evt) {
console.log('冒泡阶段--box3被触发');
}, false);

结果:

捕获阶段--box1被触发 m5-js.js:27:5
捕获阶段--box2被触发 m5-js.js:31:5
捕获阶段--box3被触发 m5-js.js:35:5
捕获阶段--btn被触发 m5-js.js:39:5
冒泡阶段--btn被触发 m5-js.js:43:5
冒泡阶段--box3被触发 m5-js.js:55:5
冒泡阶段--box2被触发 m5-js.js:51:5
冒泡阶段--box1被触发 m5-js.js:47:5

2、事件绑定--onclick接口

事件绑定有两个主要的接口,第一种接口:

button.onclick = fn

DOM 0级,此类注册事件只会出现在冒泡阶段。

这种方式将事件触发接口(onclick)作为元素的一个属性,值即为事件触发后执行的回调函数(fn)。因为一个元素的一个属性只能指向一个值,故此类绑定方式中,元素只能注册一个同类的事件,旧的注册事件会被新的注册事件所覆盖,事件注销时,使用button.onclick = null

onclick类的接口,只能注册一个同类事件

考虑下面的DOM结构

<button class="btn">点击按钮</button>

在按钮button上先后绑定两个同类事件,后绑定的事件会覆盖前绑定的事件

var btn = document.getElementsByClassName('btn')[0];

btn.onclick = function (event) {
console.log('hello word')
}; btn.onclick = function () {
console.log('你好世界')
};

执行结果

你好世界 m5-js.js:32:5

onclick类的接口,使用button.onclick = null的方式注销事件

依然考虑如下的DOM结构

<button class="btn">点击按钮</button>

button上绑定一个点击事件,然后启动一个定时器,在4s之后注销此事件

var btn = document.getElementsByClassName('btn')[0];

setTimeout(function () {
btn.onclick = null;
}, 2000); btn.onclick = function (event) {
console.log('hello word');
};

执行结果

在网页加载完毕后的2s内,每次点击button按钮都会打印hello world,但是2s之后点击就不再有反应。

hello word m5-js.js:31:5

3、事件绑定--addEventListener接口

事件绑定的第二种接口:

button.addEventListener('click', fn, false)

DOM 2级,此类注册事件可以自选出现在捕获或者冒泡阶段。

这种事件触发接口(addEventListener)作为元素的一个事件注册函数,参数是事件类型、回调函数、布尔值(true为选择捕获/false为选择冒泡,默认为false)。此注册函数实现了类似'回调函数队列'的效果,一个元素可以注册多个同类的事件,当事件发生时,多个回调函数会依次执行。

事件注销时,使用button.removeEventListner('click', fn, false),参数的值必须和注册时指向的对象一致。

注意:使用removeEventListener接口方式,无法处理匿名回调函数的事件注销。(因为匿名函数一旦创建,后续就无法获取对它的引用)

addEventListener接口,可以注册多个同类事件,发生时,依次执行回调函数

考虑如下DOM结构

<button class="btn">点击按钮</button>

为button注册多个同类click事件,这些回调函数不会覆盖,而是会依次执行

var btn = document.getElementsByClassName('btn')[0];

btn.addEventListener('click', function (evt) {
console.log('hello func - 1');
}, false); btn.addEventListener('click', function (evt) {
console.log('hello func - 2');
}, false); btn.addEventListener('click', function (evt) {
console.log('hello func - 3');
}, false); btn.addEventListener('click', function (evt) {
console.log('hello func - 4');
}, false);

执行结果

hello func - 1 m5-js.js:27:5
hello func - 2 m5-js.js:32:5
hello func - 3 m5-js.js:36:5
hello func - 4 m5-js.js:40:5

addEventListener接口无法注销以匿名函数注册的事件

考虑如下DOM结构

<button class="btn">点击按钮</button>

button按钮注册了两个click事件,其中一个指向有名回调函数sayHello,一个指向匿名函数。2s后尝试注销这两个事件,最终发现,有名函数的事件可以被注销,匿名函数的事件无法注销。

var btn = document.getElementsByClassName('btn')[0];

function sayHello() {
console.log('hello world')
} // button注册了两个click事件,其中一个使用有名回调函数,一个使用匿名回调函数
btn.addEventListener('click', sayHello, false);
btn.addEventListener('click', function (evt) {
console.log('你好世界');
}); // 2s后尝试注销button的click事件
setTimeout(function () {
btn.removeEventListener('click', sayHello, false);
btn.removeEventListener('click', function (evt) {
console.log('你好世界');
});
}, 2000);

4、事件对象

每一次预定事件发生时,在回调函数中都可以直接引用event对象,此对象代表了事件对象,包含事件发生的一些必要信息。事件对象中有两个关于目标的属性,event.currentTarget代表着当前回调函数所属的对象,event.target代表着触发事件的源对象。

考虑如下DOM结构

<button class="btn">点击按钮</button>

button按钮绑定一个事件,在回调函数中可以直接调用event对象

var btn = document.getElementsByClassName('btn')[0];

btn.onclick = function (event) {
console.log(event.currentTarget === this);
console.log(event.target === btn);
};

执行结果

true m5-js.js:27:5
true m5-js.js:28:5

thisevent.currentTarget总是同样的引用,代表的是回调函数所属的对象。

event.target可以获取发生事件的源头对象。


5、阻止冒泡传播和阻止默认行为

冒泡类事件流,默认情况下会将事件传播至所有父级元素,但在某些时候我们需要主动停止冒泡事件流的传播。

考虑如下DOM结构

<div class="box1">
<button class="btn">点击按钮</button>
</div>

buttonbox1均注册点击事件,默认情况下,button的点击事件也会触发box1的点击行为。但如果在button的回调函数中执行event.stopPropagation()就可以阻止冒泡事件的向上传播。

var box1 = document.getElementsByClassName('box1')[0];
var btn = document.getElementsByClassName('btn')[0]; box1.onclick = function () {
console.log('box1发生了click事件');
}; btn.onclick = function (event) {
console.log('btn发生了click事件');
event.stopPropagation();
};

执行结果

btn发生了click事件 m5-js.js:30:5

某些html元素含有默认的事件行为,某些时候我们也需要主动停止默认行为的发生。

考虑如下DOM结构

<div class="box1">
<a href="http://www.baidu.com" class="link">点击去往百度</a>
</div>

当点击a元素的时候,会执行默认行为,即前往指定的herf地址。但是如果使用event.preventDefault()即可阻止默认行为的发生。如下执行结果并不会跳转到指定href页面。

var a = document.getElementsByClassName('link')[0];

a.onclick = function (event) {
console.log('连接a发生了点击事件,取消默认行为');
event.preventDefault();
};

执行结果

连接a发生了点击事件,取消默认行为 m5-js.js:36:5

有时候,我们希望既可以阻止冒泡事件的传播,同时也可以阻止默认行为,那么就可以使用return false


6、事件带来的问题

第一个问题在于页面上会出现过多数量的事件,这些事件的回调函数都是需要占用内存,事件越多占用的内存也越大。某些事件有重复的情况,比如多个同类的li标签可能绑定着同样的事件回调函数,这种事件触发的方式效比较低,还可以进一步优化,以上问题可以使用事件委托(代理)。

第二个问题在于如何妥善的处理事件的注销。比如元素在发生innerHTML修改或者被remove节点时,对应的回调函数也会失去引用,这些失去引用的事件可能无法被垃圾回收机制正确回收。所以在处理一个含有事件的元素时,应特别注意事件的注销操作,使用onclick = null或者使用removeEventListener


7、事件委托(代理)

事件委托技术依赖于事件流冒泡传播。同类的子元素的事件发生,不应该在子元素上注册事件,而应该在共同的父元素上注册事件,由父元素来处理子元素的事件发生,此父元素即为子元素的事件委托者或者代理者。

考虑如下DOM结构

<ul class="item-list">
<li class="item">选项1</li>
<li class="item">选项2</li>
<li class="item">选项3</li>
<li class="item">选项4</li>
<li class="item">选项5</li>
<li class="item">选项6</li>
<li class="item">选项7</li>
<li class="item">选项8</li>
<li class="item">选项9</li>
<li class="item">选项10</li>
</ul>

使用事件代理,只需要在父元素上绑定事件,所有子类元素的事件均会通过冒泡的形式传播到父元素的回调函数中处理。

var itemList = document.getElementsByClassName('item-list')[0];

itemList.onclick = function (event) {
// 利用冒泡原理来实现事件代理
var origin = event.target;
var originText = origin.innerText; console.log('发生点击事件的元素是:', originText);
};

事件委托技术减少了多个同类子元素的重复事件注册,减少了内存的开销,事件处理的效率也更高。

此外,事件委托还有一个好处是不用关心新增子元素的事件注册操作,新增子元素只需要加入父元素即可,不必再次执行事件注册。

父元素也可以通过switch判断子元素的方式来对不同的子元素执行相应的处理函数。

考虑如下的DOM结构

<div class="item-list">
<button class="item">选项1</button>
<li class="item">选项2</li>
<a href="#" class="item">选项3</a>
</div>

父元素执行事件代理,同时通过switch判断子元素的tagName,针对不同的标签执行不同的事件处理。

var itemList = document.getElementsByClassName('item-list')[0];

itemList.onclick = function (event) {
// 利用冒泡原理来实现事件代理
var origin = event.target; switch (origin.tagName) {
case 'BUTTON':
console.log('点击的是一个按钮');
break;
case 'LI':
console.log('点击的是一个列表项目');
break;
case 'A':
console.log('点击的是一个链接');
break;
}
};

执行结果

点击的是一个按钮 m5-js.js:26:13
点击的是一个列表项目 m5-js.js:29:13
点击的是一个链接 m5-js.js:32:13

8、模拟事件触发

jQuery中,提供了trigger接口作为模拟事件的触发,此接口让我们非常方便的触发指定元素的指定事件。

考虑如下DOM结构

<button id="btn1">按钮1</button>
<button id="btn2">按钮2</button>

为两个button都注册对应的事件,在button1的回调函数中,执行button2trigger函数,触发button2的点击事件。点击button1后,即使在没有点击button2的情况下,也会执行button2的回调函数。

var $btn1 = $('#btn1');
var $btn2 = $('#btn2'); $btn1.bind('click', function () {
console.log('btn1被点击');
$btn2.trigger('click');
}); $btn2.bind('click', function () {
console.log('btn2被点击');
});

执行结果(只点击了button1

btn1被点击 m5-js.js:21:5
btn2被点击 m5-js.js:26:5

在原生JS中,需要使用createEvent接口和dispatchEvent接口实现同样的效果。

考虑如下DOM结构

<button id="btn1">按钮1</button>
<button id="btn2">按钮2</button>

点击button1的时候,触发button2click事件

var btn1 = document.getElementById('btn1');
var btn2 = document.getElementById('btn2'); var event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, true, document.defaultView, 0, 0, 0, 0, 0,
false, false, false, false, 0, null); btn1.onclick = function () {
console.log('btn1被点击');
btn2.dispatchEvent(event);
}; btn2.onclick = function () {
console.log('btn2被点击');
};

执行结果(只点击了button1

btn1被点击 m5-js.js:24:5
btn2被点击 m5-js.js:29:5

9、总结

事件是js中非常重要的模块,它完成了js代码和html代码之间的互动,通过事件可以提供丰富的高体验性用户交互,可以说通过js实现的用户交互就是以事件作为驱动的。js的事件模型为:目标元素+事件对象+回调函数。

上一篇:Python之路(第三十一篇) 网络编程:简单的tcp套接字通信、粘包现象


下一篇:IE haslayout的理解与bug修复