JavaScript不同绑定事件方式的异同
在 DOM 节点上绑定用户事件大致有三种方法,以绑定用户单击事件为例,分别为:
- 在 HTML 节点标签的
onclick
属性上绑定; - 在节点对象的
onclick
属性上绑定; - 通过
addEventListener
添加click
绑定;
本文主要整理介绍这三种常见方法的异同与利弊。
HTML标签内联绑定
标签内联绑定是早期比较常用的方法,以绑定单击事件为例:
<div onclick="alert('hello')">
click me
</div>
与之对应的,是将事件函数写在 script 标签内,或者从外部引入,然后在 onclick 属性上绑定事件函数的调用。
如下:
<div onclick="userClick()">
click me
</div>
<script>
function userClick (e) {
let event = e || window.event;
console.log("hello");
}
</script>
节点对象绑定
与内联绑定类似,但不同点是这种绑定方式全部在 JavaScript 中完成,也就是需要在 script 标签下完成。
如下:
<div id="div">
click me
</div>
<script>
// 直接使用匿名函数的方式绑定
document.getElementById("div").onclick = function (e) {
let event = e || window.event;
console.log("hello");
}
// 或者引用一个具名函数的引用
function userClick (e) {
let event = e || window.event;
console.log("hello");
}
document.getElementById("div").onclick = userClick;
</script>
无论使用上述那种方式绑定,onclick 属性都是保存事件函数的引用,所以这种方式只能实现单一绑定,即一个节点只能绑定一种事件函数,如果用户多次绑定,则最新绑定的函数会覆盖之前的函数,当用户触发事件时,只执行最新绑定的事件函数。
同时,这两种方式绑定的函数默认是不处理事件冒泡的。如果子元素和父元素都绑定了同名事件,那么在子元素上触发的事件会默认冒泡到父节点上。如果需要阻止事件冒泡,则可以通过 stopPropagation
来实现。
例如,组织子节点的 click 事件冒泡到父节点上:
<div id="father" onclick="userClick(event, 'father')">
father
<div id="child" onclick="userClick(event, 'child')">
child
</div>
</div>
<script>
function userClick(e, name) {
var event = e || window.event;
console.log(event, name);
// 使用 stopPropagation 阻止事件冒泡
event.stopPropagation();
// IE 浏览器阻止事件冒泡
// window.event.cancelBubble = true;
}
</script>
使用 addEventListener 绑定
使用 addEventListener
方式绑定的好处是它可以默认处理事件冒泡,该方法原型如下:
element.addEventListener(event, function, useCapture)
第三个参数 useCapture
表示事件获取的阶段,可取值:
- true : 在事件捕获阶段获取事件并执行;
- false: 在事件冒泡阶段获取事件并执行,默认为 false.
这里多出了一个概念,即 事件冒泡 与 事件捕获 ,这其实是 DOM2 级事件中定义的两个事件阶段,用户从点击一个按钮,到完成响应这个过程其实有三个阶段:
- 事件捕获阶段,事件从最外层的元素开始捕获,依次向内传递;
- 事件获取阶段,如果事件传递阶段有某个元素定义了事件回调,则事件被获取并执行;
- 事件冒泡阶段,当事件传递到最内层元素后,再有最内层元素一次向外层传递,直到根节点结束。
事件冒泡和事件捕获是两种不同的事件传递模型,DOM2 级事件将他们整合并规范化,默认情况下,使用 addEventListener
创建的事件回调会在冒泡阶段触发,但是我们可以通过第三个参数来修改事件的获取阶段。
如下:
<div id="father">
father
<div id="child">
child
</div>
</div>
<script>
function userClick(e) {
var event = e || window.event;
console.log(event);
}
document.getElementById('father').addEventListener('click', userClick, true);
document.getElementById('child').addEventListener('click', userClick, true);
</script>
上面的事件只会在捕获阶段获取,不会向上传递,所以点击子元素不会应该父元素。
以这种方式绑定的事件还有一个特点,即事件不是唯一的,对于一个节点对象可以调用 addEventListener
进行多次绑定,绑定的函数会按照绑定的顺序在用户触发时按顺序执行。
优化:事件代理
说到事件绑定就不得不说事件代理,事件代理是为了提高事件的性能。试想,加入有一个列表,列表的每一个元素都需要响应单击事件,加入有有 10000 个列表元素,我们是不太可能去绑定 10000 次的,那样只会带来性能损耗和资源浪费。一般这种情况可以在列表的父元素上绑定事件,通过父元素代理子元素的事件,这就是事件代理。
如下:
<ul id="father">
<li>1</li>
<li>2</li>
<li>3</li>
...
<li>10000</li>
</ul>
<script>
document.getElementById('father').addEventListener('click', function (e) {
let event = e || window.event;
console.log(event.target.innerText);
});
</script>
上面代码在父元素 ul
上绑定了 click
事件代理,以代理其下所有 li
元素的 click
事件,由于事件冒泡的存在,当点击子元素 li
时,事件被冒泡到了 ul
元素上,所以父元素边能收到 click
事件,这便实现了代理。
引申:阻止默认行为
试想页面中有一个 a
标签,我们为他绑定了 click
事件函数,在用户点击该元素的时候,我们需要判断是否可以跳转,如果可以跳转,则通过 a
标签的 href
跳转,否则禁止跳转。
由于 a
标签本就是被设计为跳转功能的,所以禁止跳转即禁止了 a 的默认事件。
阻止默认事件的方式如下:
- 直接在获取的事件函数中返回 false ;
- 使用 event.preventDefault() 进行阻止;
- IE浏览器需要使用 window.event.returnValue = false 来阻止。
不过,使用 return false
的方式有一定局限性,且只适用于传统方式绑定的情况下。
以 a
标签为例:
<a href="www.baidu.com" onclick="userClick()">click me</a>
可以在 userClick 函数中定义单击处理函数,并阻止页面跳转:
function userClick(e) {
let event = e || window.event;
// do something...
// 阻止默认事件,页面将不会跳转
// event.preventDefault();
// IE 下阻止页面跳转
// window.event.returnValue = false;
// 返回 false 也可以阻止默认事件
return false;
}