事件
HTML的是基于事件驱动的。JS采用异步事件驱动编程模型,当文档、浏览器、元素或与之相关对象发生特定事情时,浏览器会产生事件。如果我们的JS代码关注了特定类型事件,那么它可以注册当这类事件发生时要调用的句柄-我们的回调函数。
事件流
事件流描述的是从页面中接收事件的顺序,比如有两个父子元素的div,点击子元素的div,这时候是子元素的div先触发click事件还是父元素先触发?目前主要有三种模型:
IE的事件冒泡:事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的元素。
Netscape的事件捕获:不太具体的节点更早接收事件,而最具体的元素最后接收事件,和事件冒泡相反。
DOM事件流:DOM2级事件规定事件流包括三个阶段,事件捕获阶段,处于目标阶段,事件冒泡阶段,首先发生的是事件捕获,为截取事件提供机会,然后是实际目标接收事件,最后是冒泡句阶段。
现在Opera、Firefox、Chrome、Safari都支持DOM事件流,IE不支持事件流,只支持事件冒泡。
listener
listener,事件就是用户或浏览器自身执行的某种动作。比如click、load、moseover等,都是事件类型(俗称事件名称),而响应某个事件的方法就叫做事件处理程序或者事件监听器或者事件句柄,事件处理程序名字是:on+事件类型。我们一般定义当事件发生时完成期望的事情,这个事情函数就是listener。
添加listener
<button id="btn1" onclick="clickme()">Click Me</button>
或者
<script type="text/javascript">
function fun() {
// body...
}
domobj.onclick = fun;
</script>
对于第一种方式,我们是没有办法移除事件处理程序的(设属性为null也不行)。第二种方式注册时,设onclick属性赋为null即可删除。
DOM2事件
另外DOM2事件也为我们提供了两个方法:addEventListener和removeEventListener。
domobj.addEventListener(eventType, handler, capture);
三个参数:事件类型、事件处理方法、一个布尔值。最后布尔参数如果是true表示在捕获阶段调用事件处理程序,如果是false,则是在事件冒泡阶段处理。示例:
<div id="parent" >
Parent
<div id="childen">
<p>childen</p>
</div>
</div>
<script type="text/javascript">
var parent = document.getElementById('parent');
var childen = document.getElementById('childen');
parent.addEventListener('click', function () {
console.log("Parent was clicked")
}, true);
childen.addEventListener('click' ,function () {
console.log("Childen was clicked")
}, false);
</script>
这里点击childen时,反而先执行了parent的事件处理程序。输出
Parent was clicked
Childen was clicked
解释:根据DOM流, parent的click事件的处理是在capture时就执行。而childen的click事件的handler定义到冒泡时才执行。一般情况下,我们考虑到浏览器的兼容性,都是设为在冒泡阶段才执行。对于removeEventListener,所需要的参数与添加时是一样的。不过不能移除匿名函数。好的办法是使用函数名来添加、移除。
IE的attachEvent
说到浏览器兼容,不得不提下IE兼容性问题。IE是并不支持addEventListener和removeEventListener方法,不过对应的,它提供了两个类似的方法attachEventdetachEvent,这两个方法都接收两个相同的参数,事件处理程序名称和事件处理程序方法。注意这里是“事件处理程序名称”,而不是“事件类型”, 比如是使用"onclick", 而不是"click"。由于IE只支持事件冒泡,所以添加的程序会被添加到冒泡阶段执行。
<script type="text/javascript">
var btn1 = document.getElementById('btn1');
var handler1 = function() {
console.log(this.id);
}
btn1.attachEvent('onclick', handler1);
// btn1.detachEvent('onclick', handler);
</script>
对于上面的程序,我们期待的执行的结果是输出btn1,然后这里却输出undefined。解释:
其实这是两个函数的事件处理程序的作用域不相同造成的。addEventListener得作用域是元素本身,即函数内this指向事件触发元素,而attachEvent事件处理程序会在全局变量内运行,this是window。因此我们的程序输出undefined,而不是元素id。
事实上,很多时候在事件处理程序中,我们都会用到this,因此改写成下面的写法:
domobj.attachEvent('onclick', function() { handler.apply(domobj); });
这样我们成功的结局了作用域问题,不过还有个问题--无法使用detachEvent移除处理程序。注意,这里是使用匿名函数来绑定的。jQuery作者有一段实现:
function addEvent(node, type, handler) {
if (!node) return false;
if (node.addEventListener) {
node.addEventListener(type, handler, false);
return true;
}
else if (node.attachEvent) {
node['e' + type + handler] = handler;
node[type + handler] = function() {
node['e' + type + handler](window.event);
};
node.attachEvent('on' + type, node[type + handler]);
return true;
}
return false;
}
相应的如果要移除事件处理,如下:
function removeEvent(node, type, handler) {
if (!node) return false;
if (node.removeEventListener) {
node.removeEventListener(type, handler, false);
return true;
}
else if (node.detachEvent) {
node.detachEvent('on' + type, node[type + handler]);
node[type + handler] = null;
}
return false;
}
利用闭包完美的解决了这个问题。