JavaScript、浏览器、事件之间的关系
JavaScript程序采用了异步事件驱动编程(Event-driven programming)模型,维基百科对它的解释是:
事件驱动程序设计(Event-driven programming)是一种电脑程序设计模型。这种模型的程序运行流程是由用户的动作(如鼠标的按键,键盘的按键动作)或者是由其他程序的消息来决定的。相对于批处理程序设计(batch programming)而言,程序运行的流程是由程序员来决定。批量的程序设计在初级程序设计教学课程上是一种方式。然而,事件驱动程序设计这种设计模型是在交互程序(Interactive program)的情况下孕育而生的
简言之,在web前端编程里面JavaScript通过浏览器提供的事件模型API和用户交互,接收用户的输入。
由于用户的行为是不确定的。这种场景是传统的同步编程模型没法解决的,因为你不可能等用户操作完了才执行后面的代码。所以在javascript中使用了异步事件,也就是说:js中的事件都是异步执行的。
事件驱动程序模型基本的实现原理基本上都是使用 事件循环(Event Loop),这部分内容涉及浏览器事件模型、回调原理。
JavaScript DOM、BOM模型中,同样异步的还有setTimeout,XMLHTTPRequest这类API并不是JavaScript语言本身就有的。
事件绑定的方法
事件绑定有3种方法:
行内绑定
直接在DOM元素上通过设置on + eventType
绑定事件处理程序。例如:
<a href="#none" onclick="alert('clicked')">点击我</a>
这种方法有两个缺点:
- 事件处理程序和HTML结构混杂在一起,不符合MVX的规范。为了让内容、表现和行为分开,我们应该避免这种写法。
- 这样写的代码判断具有全局作用域,可能会产生命名冲突,导致不可预见的严重的后果。
在DOM元素上直接重写事件回调函数
使用DOM Element上面的on + eventType属性 API
var el = getElementById('button'); //button是一个<button>元素
el.onclick = function(){ alert('button clicked.') };
el.onclick = function(){ alert('Button Clicked.') };
//实际之弹出'Button Clicked.',函数发生了覆盖
这种方法也有一个缺点:后绑定的函数会覆盖之前的函数。比如我们注册一个window.onload事件,可能会覆盖某个库中已有的事件函数。当然,这个可以有解决方法:
function addEvent(element, EventName, fun) { //EventName = 'on' + eventType
var oldFun = element[EventName];
if (typeof oldFun !== 'function') {
element[EventName] = fun;
} else {
element[EventName] = function() {
oldFun();
fun();
};
}
}
addEvent(window, "onload", function() { alert('onload 1') });
addEvent(window, "onload", function() { alert('onload 2') });
当然,一般情况下使用DOM Ready就可以了,因为JavaScript在DOM加载完就可以执行了
标准绑定方法
标准的绑定方法有两种,addEventListener和attachEvent前者是标准浏览器支持的API,后者是IE8以下浏览器支持的API:
//例如给一个button注册click事件
var el = getElementById('button'); //button是一个<button>元素
if(el.addEventLister){
el.addEventListener("click", function(e){
alert("button clicked.");
},false);
}
if(el.attachEvent){
el.attachEvent("onclick", function(e){
alert("button clicked.");
});
}
需要注意的是:
- addEventLister的第一个参数事件类型是不加on前缀的,而attachEvent中需要加on前缀。
- addEventLister中的事件回调函数中的this指向事件元素target本身,而attachEvent中的事件回调函数的this指向的是window。
- addEventLister有第三个参数,true表示事件工作在捕获阶段,false为冒泡阶段(默认值:false)。而attachEvent只能工作在冒泡阶段。
在chrome中运行如下代码:
<a href="javascript:alert(1)" onclick="alert(2)" id="link">click me</a>
<script>
var link = document.getElementById('link');
link.onclick = function() { alert(3); }; //覆盖了行内的onclick定义
link.addEventListener('click', function() { alert(4); },false);
link.addEventListener('click', function() { alert(5); },false);
</script>
点击后弹出顺序是: 3 -> 4 -> 5 -> 1
这里第4行代码覆盖了行内的onclick定义,如果注释了这一行,输入顺序为: 2 -> 4 -> 5 -> 1,而addEventListener之间不会发生覆盖。
解除事件绑定
对于上述的前二个方法,解除事件绑定只需要将对应的事件函数设为null,就可以了:
var el = document.getElementById('button');
el.onclick = null;
对于上述第三种方法使用removeListen()方法即可,在IE8中,对应使用detachEvent()。注意,他们和上面的注册方法一一对应,不能混用。
//这是一段错误代码,不能实现事件移除
//建立一个事件
var el = document.getElementById('button'); //button是一个<button>元素
if(el.addEventLister){
el.addEventListener("click", function(e){
alert("button clicked.");
},false);
}
if(el.attachEvent){
el.attachEvent("onclick", function(e){
alert("button clicked.");
});
}
//试图移除这个事件
if(el.removeEventLister){
el.addEventListener("click", function(e){
alert("button clicked.");
},false);
}
if(el.detachEvent){
el.datachEvent("onclick", function(e){
alert("button clicked.");
});
}
//移除失败
以上的错误在于事件函数这样定义时,虽然看着完全一样,但在内存中地址不一样。这样一来,电脑不会认为解除的和绑定的是同一个函数,自然也就不会正确解除。应该这样写:
//建立一个事件
var el = document.getElementById('button'); //button是一个<button>元素
var handler = function(e){alert("button clicked.");};
if(el.addEventLister){
el.addEventListener("click", handler,false);
}
if(el.attachEvent){
el.attachEvent("onclick", handler);
}
//试图移除这个事件
if(el.removeEventLister){
el.addEventListener("click", handler, false);
}
if(el.detachEvent){
el.datachEvent("onclick", handler);
}
//移除成功
事件的捕获与冒泡
之前说addEventListener函数的第三个参数表示捕获和冒泡,这个是一个重点!
我自己描述一下他们的定义就是:
冒泡:在一个元素上触发的某一事件,会在这个元素的父辈元素上会依次由内向外触发该事件,直到window元素。
捕获:在一个元素上触发的某一事件,这个元素的每一层的所有子元素上触发该事件,并逐层向内,直到所有元素不再有子元素。
如下图(注:图片来自百度搜索)
事件间回到函数参数是一个事件对象,它里面包括许多事件属性和方法,比如,我们可以用以下方式阻止冒泡和默认事件:
//该例子只写了handler函数
function handler(event) {
event = event || window.event;
//阻止冒泡
if (event.stopPropagation) {
event.stopPropagation(); //标准方法
} else {
event.cancelBubble = true; // IE8
}
//组织默认事件
if (event.perventDefault) {
event.perventDefault(); //标准方法
} else {
event.returnValue = false; // IE8
}
}
其次,普通注册事件只能阻止默认事件,不能阻止冒泡
element = document.getElemenById("submit");
element.onclick = function(e){
/*...*/
return false; //通过返回false,阻止冒泡
}
事件对象
事件函数中有一个参数是事件对象,它包含了事件发生的所有信息,比如键盘时间会包括点击了什么按键,包括什么组合键等等,而鼠标事件会包括一系列屏幕中的各种坐标和点击类型,甚至拖拽等等。当然,它里面也会包括很多DOM信息,比如点击了什么元素,拖拽进入了什么元素,事件的当前状态等等。
这里关于事件兼容性有必要强调一下:
document.addEventListener('click', function(event) {
event = event || window.event; //该对象是注册在window上的
console.log(event); //可以输出事件对象看一看, 属性很多很多
var target = event.target || event.srcElement; //前者是标准事件目标,后者是IE的事件目标
},false);
关于鼠标事件坐标的问题,可以看另一篇博客:元素和鼠标事件的距离属性
事件触发
除了用户操作以外,我们也可以写代码主动触发一个事件,以ele元素的click事件为例:
ele.click(); //触发ele元素上的单击事件
事件代理
有时候我们需要给不存在的的一段DOM元素绑定事件,比如用户动态添加的元素,或者一段 Ajax 请求完成后渲染的DOM节点。一般绑定事件的逻辑会在渲染前执行,但绑定的时候找不到元素所以并不能成功。
为了解决这个问题,我们通常使用事件代理/委托(Event Delegation)。而且通常来说使用 事件代理的性能会比单独绑定事件高很多,我们来看个例子。
- 传统注册事件方法,当内容很多时效率低,不支持动态添加元素
<ul id="list">
<li>item-1</li>
<li>item-2</li>
<li>item-3</li>
<li>item-4</li>
<li>item-5</li>
</ul>
<script>
var lists = document.getElementsByTagName('li');
for(var i = 0; i < lists.length; ++i){
lists[i].onclick = (function(i){
return function(){
console.log("item-" + (i + 1));
};
})(i);
}
//添加节点
var list = document.getElementById('list');
var newNode = document.createElement('li');
newNode.innerHTML = "item-6";
list.appendChild(newNode);
</script>
- 事件委托注册方法,不论内容有多少都只注册1次,支持动态添加元素:
<ul id="list">
<li>item-1</li>
<li>item-2</li>
<li>item-3</li>
<li>item-4</li>
<li>item-5</li>
</ul>
<script>
var list = document.getElementById('list');
var handler = function(e){
e = e || window.event;
var target = e.target || e.srcElement;
if(target.nodeName && target.nodeName === "LI"){
console.log(target.innerHTML);
}
};
if(list.addEventListener){
list.addEventListener("click", handler);
} else {
list.attachEvent("onclick", handler);
}
//添加节点
var list = document.getElementById('list');
var newNode = document.createElement('li');
newNode.innerHTML = "item-6";
list.appendChild(newNode);
</script>
事件封装
很明显,处理浏览器兼容太麻烦了,所以这里把js中的事件注册相关函数封装一下,作为整理。
//均采用冒泡事件模型
var myEventUtil={
//添加事件函数
addEvent: function(ele, event, func){
var target = event.target || event.srcElement;
if(ele.addEventListener){
ele.addEventListener(event, func, false);
} else if(ele.attachEvent) {
ele.attachEvent('on' + event, func); //func中this是window
} else {
ele['on' + event] = func; //会发生覆盖
}
},
//删除事件函数
delEvent:function(ele, event, func) {
if(ele.removeEventListener){
ele.removeEventListener(event, func, false);
} else if(ele.detachEvent) {
ele.detachEvent('on' + event, func);
} else {
ele['on' + event] = null;
}
},
//获取触发事件的源DOM元素
getSrcElement: function(event){
return event.target || event.srcElement;
},
//获取事件类型
getType: function(event){
return event.type;
},
//获取事件
getEvent:function(event){
return event || window.event;
},
//阻止事件冒泡
stopPropagation: function(event) {
if(event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBuble = false;
}
},
//禁用默认行为
preventDefault: function(event){
if(event.preventDefault){
event.preventDefault();
} else {
event.returnValue = false;
}
}
};
jQuery中的事件
需要注意的是: JQuery中的事件都工作在冒泡阶段,且只能工作在冒泡阶段
注册、解除事件
- 方法一:
//不会发生覆盖,但不利于解除,不能动态操作事件
<button id="button">here</button>
$("#button").click(function(){ //注册一个click事件,当然可以用其他事件名的函数注册其他事件
console.log("clicked");
});
- 方法二:
//不会发生覆盖,利于解除,不能动态操作事件
<button id="button">here</button>
//注册一个事件
$("#button").bind("click", function() { //注册一个click事件,当然可以用其他事件名的函数注册其他事件
console.log("clicked");
});
//当然还可以这样写,给事件指定命名空间
$(document).bind('click.handler1', function() { console.log(1);})
$(document).bind('click.handler2', function() { console.log(2);})
//解除一个事件
$("#button").unbind(".handler1"); //解除元素上所以handler1命名空间中的事件
$("#button").unbind('click.handler2'); // 解除元素上的click.handler2事件
$("#button").unbind('click'); // 解除元素上所有点击事件
$("#button").unbind() // 解除元素上所有事件
//bind()方法还介受3个参数形式,这里就不赘述了,感兴趣可以自己看看相关资料。
- 方法三:
//不会发生覆盖,但不利于解除,能动态操作事件,依赖于事件冒泡
//注册事件
$(document).delegate(".item", "click", function(){console.log(this.innerHTML);}); //第一个是选择器, 第二个是事件类型, 第三个是事件函数
//移除事件
$(document).undelegate(".item", "click", handler); //移除元素上指定事件
$(document).undelegate(".item", "click"); //移除元素上所有click事件
$(document).undelegate(".item"); //移除元素上所有事件
- 方法四:
//不会发生覆盖,但不利于解除,能动态操作事件,不依赖于事件冒泡
//注册事件
#(".item").live("click", function(){console.log(this.innerHTML);}) //第一参数是事件类型, 第二参数是事件函数
//移除事件
$(".item").die("click", handler); //移除元素上指定click事件
$(".item").die("click"); //移除元素上所有click事件
- 两个简化方法:
//hover方法
$("#button").hover(function(){
//鼠标移入时的动作,不冒泡
}, function(){
//鼠标移出时的动作,不冒泡
});
//toggle方法
$("#button").toggle(function(){
//第一次点击时的动作
}, function(){
//第二次点击时的动作
}, .../*可以放多个函数,依次循环响应*/);
事件触发
//不能触发addEventListener和attachEvent
//主动触发一个事件
$("#button").trigger("click"); //触发所有click事件
$("#button").trigger("click.handler1"); //触发所有click.handler1事件
$("#button").trigger(".handler1"); //触发所有handler1命名空间的事件
$("#button").trigger("click!"); //触发所有没有命名空间的click事件
$("#button").trigger(event); //在该元素上触发和事件event一样的事件
$("#button").trigger({type:"click", sync: true}); //触发click事件,同步