web前端之MVC的JavaScript Web富应用开发二:事件和监听
事件是JavaScript应用程序的核心,是所有内容的驱动,它决定了在应用程序产生用户交互的起始时刻。然而在JavaScript诞生之初“ 事件” 的实现并不标准,甚至非常丑陋。在之后的浏览器大战中网景和微软分道扬镳,他们各自实现的事件模型互不兼容。尽管后来W3C对此做了标准化,但IE仍然坚持使用与W3C不兼容的事件模型,直到最新发布的IE9才遵循标准。
幸运的是,有很多诸如jQuery和Prototye的类库很好地处理了兼容性问题,对外提供了统一的API来实现事件。但是了解事件的机制仍然是非常重要的,因此这里首先讲解W3C中的事件模型,然后展示各种流行类库的一些实例。
监听事件:
绑定事件监听的函数叫做addEventListener(),它有三个参数:type(比如click),listener(比如callback)及useCapture(后续会讲到useCapture)。使用前两个参数可以给DOM元素绑定一个函数,当特定的事件(比如点击)被触发时执行这个函数:
var button=document.getElmentById(“createButton”);
button.addEventListener(“click”,function(){/…/},false);
可以使用removeEventListener()来移除事件监听,参数和传入addEventListener()的一样。如果监听的函数时匿名函数,没有任何引用指向它,在不销毁这个元素的前提下,这个监听是无法被移除的。
var div=document.getElementById(“div”);
var listener=function(event){/…/};
div.addEventListener(“click”,listener,false);
div.removeEventListener(“click”,listener,false);
带入listener函数的第一个参数是event对象,通过event对象可以得到事件的相关信息,比如时间戳、坐标和事件宿主元素(target)。它同样包含很多方法来停止事件冒泡和阻止事件的默认行为。
不同的浏览器对事件类型的支持也不尽相同,但所有的现代浏览器都支持这些事件:
click、dbclick、mousemove、mouseover、mouseout、focus、blur、change(表单输入框特有)、submit(表单特有)
事件顺序:
在进一步讨论之前, 很有必要介绍一下事件顺序。 如果一个节点和它的一个父节点都绑定了相同事件类型的回调, 当事件触发时哪个回调会先执行?
你可以自行选择要注册的事件处理程序的调用类型,捕捉或冒泡,通过给addEventListener()传入第3个参数useCapture来设置。如果addEventListener()的最后一个参数是true,事件处理程序以捕捉模式触发:如果是false,事件处理程序以冒泡模式触发 。
取消事件:
当事件冒泡时, 可以通过 stopPropagation() 函数来终止冒泡, 这个函数是 event 对象中的方法。 比如这段代码, 任何父节点的事件回调都不会触发 :
button.addEventListener("click",function(event){
event.stopPropagation();
/*...*/
},false);
此外, 一些类库比如 jQuery 还支持 stopImmediatePropagation() 函数, 用来阻止后续所有的事件触发——哪怕这些事件是注册在同一个节点元素上的也不例外。
浏览器同样给事件赋予了默认行为。 比如, 当你点击一个链接时, 浏览器的默认行为是载入新页面。 可以通过调用 event 对象的 preventDefault() 函数来阻止默认行为, 同样也可以通过在回调中返回 false 来实现同样的效果 :
bform.addEventListener("submit", function(e){
/* ... */
return confirm("Are you super sure?");
}, false);
如果调用 confirm() 返回 false( 用户点击了对话框的取消按钮), 这个事件回调函数就返
回 false, 这样就会取消事件, 阻止表单的提交。
事件对象:
和上面提到的函数 stopPropagation() 和 preventDefault() 一样, event 对象还包含很多有用的属性。
bubbles :布尔值, 表示事件是否通过 DOM 以冒泡形式触发。
事件发生时, 反映当前环境信息的属性 :
button :表示( 如果有) 鼠标所按下的按钮。
ctrlKey :布尔值, 表示 Ctrl 键是否按下。
altKey :布尔值, 表示 Alt 键是否按下。
shiftKey :布尔值, 表示 Shift 键是否按下。
metaKey :布尔值, 表示 Meta 键注 1 是否按下。
表示键盘事件的属性 :
isChar :布尔值, 表示当前按下的键是否表示一个字符。
charCode :表示当前按键的 unicode 值( 仅对 keypress 事件有效)。
keyCode :表示非字符按键的 unicode 值。
which :表示当前按键的 unicode 值, 不管当前按键是否表示一个字符。
事件发生时的环境参数 :
pageX, pageY :事件发生时相对于页面( 如 viewport 区域) 的坐标。
screenX, screenY :事件发生时相对于屏幕的坐标。
和事件相关的元素 :
currentTarget :事件冒泡阶段所在的当前 DOM 元素。
target,originalTarget :原始的 DOM 元素。
relatedTarget :其他和事件相关的 DOM 元素( 如果有的话)。
不同的浏览器对这些属性的兼容性也不同,尤其是那些不兼容 W3C 的浏览器。 幸运的是,诸如 jQuery 和 Prototype 这些类库为我们解决了这些兼容性问题。
事件库:
jQuery 的 API 提供了 bind() 函数用来跨浏览器绑定事件监听。 在一个 jQuery 实例上调用此函数, 传入事件名称和回调函数 :
jQuery(“#element”).bind(eventName, handler);
比如, 给一个元素注册点击事件 :
jQuery("#element").bind("click", function(event) {
// ...
});
jQuery 提供了一些常用事件的快捷方法, 比如 click、 submit 和 mouseover。 看这段代码:
$("#myDiv").click(function(){
// ...
});
需要注意的是, 使用这个方法之前要确保 DOM 元素是存在的, 这一点很重要。 例如,应当在页面载入完成后绑定事件, 因此需要绑定 window 的 load 事件, 然后添加监听 :
jQuery(window).bind("load", function() {
$("#signinForm").submit(checkForm);
});
这个函数是兼容各个浏览器的 :
jQuery.ready(function($)){
$("#myForm"). bind("submit", function(){ /*...*/});
});
实际上, 可以不用 ready() 函数而直接将回调函数写入 jQuery 对象。
jQuery(function($){
// 当页面内容可用时调用
});
切换上下文:
关于事件有一点经常让人感到迷惑,那就是调用事件回调函数时上下文的切换。当使用浏览器内置的 addEventListener() 时, 上下文从局部变量切换为目标 HTML 元素 :
new function(){
this.appName="wem";
document.body.addEventListener("click",function(e){
//上下文发生改变时,因此appName是undefined
alert(this.appName);
},false);
};
要想保持原有的上下文, 需要将回调函数包装进一个匿名函数, 然后定义一个引用指向它。这在jQuery 中也是一种很常用的模式, 包括一个 proxy() 函数, 只需将指定的上下文传入函数即可 :
$(“signinForm”).submit($.proxy(function(){ /* … */ }, this));
委托事件:
从事件冒泡时开始就发生了事件委托, 我们可以直接给父元素绑定事件监听, 用来检测在其子元素内发生的事件。
//在ul列表上做了事件委托
list.addEventListener("click",function(e){
if(e.currentTarget.tagName=="li"){
/*....*/
return false;
}
},false);
jQuery 的处理方式更妙, 只需给 delegate() 函数传入子元素的选择器、 事件类型和回调函数即可。 如果使用事件绑定的话, 就会给每一个 li 元素都绑定 click 事件, 然而使用 delegate() 方法就能减少这种事件监听的数量, 改善代码性能 :
// 不要这样做, 这样会给每个 li 元素都添加事件监听( 非常浪费)
$("ul li").click(function(){ /* ... */ });
// 这样只会添加一个事件监听
$("ul").delegate("li", "click", /* ... */);
使用事件委托的另一个好处是, 所有为元素动态添加的子元素都具有事件监听。 因此,在上面的例子中, 在页面载入完成后添加的 li 节点同样可以触发点击事件的回调。
自定义事件:
jQuery 中可以使用 trigger() 函数来触发自定义事件。 可以通过命名空间的形式来管理事件名称, 命名空间中的单词用点号分隔注, 比如 :
// 绑定自定义事件
$(".class").bind("refresh.widget",function(){});
// 触发自定义事件
$(".class").trigger("refresh.widget");
通过给 trigger() 传入一个额外的参数来给事件处理程序传入数据。 数据会以附加参数
的形式带入回调 :
$(".class").bind("frob.widget", function(event, dataNumber){
console.log(dataNumber);
});
$(".class").trigger("frob.widget", 5);
和内置事件一样, 自定义事件同样会沿着 DOM 树做冒泡。
自定义事件和 jQuery 插件:
我们来看一个简单的 jQuery 插件——选项卡。 我们让 ul 列表来响应点击事件。当用户点击一个列表项, 给这个列表项添加一个名为 active 的类, 同时将其他列表项中的 active 类移除 :
<ul>
<li data-tab="users">Users</li>
<li data-tab="groups">Groups</li>
</ul>
<div id="tabsContent">
<div data-tab="users">Users...</div>
<div data-tab="groups">Groups...</div>
</div>
另外, id 为 tabsContent 的 div 用来存放每个选项卡对应的实际内容。 根据当前激活的选项卡, 来对应地给 div 的子节点添加或删除 active 类。 实际的显示和隐藏选项卡和内容都由 CSS 来控制, 我们的插件仅仅处理 active 类 :
jQuery.fn.tabs=function(control){
var element=$(this);
control=$(control);
element.delegate("li","click",function(){//只添加一个li事件
//遍历选项卡名称
var tabName=$(this).attr("data-tab");
//在点击选项卡时触发自定义事件
element.trigger("change.tabs",tabName);
});
element.bind("change.tabs",function(e,tabName){
element.find("li").removeClass("active");
element.find(">[data-tab='"+tabName+"']").addClass("active");
});
element.bind("change.tabs",function(e,tabName){
control.find(">[data-tab]").removeClass("active");
control.find(">[data-tab='"+tabName+"']").addClass("active");
});
//激活第一个选项卡
var firstName=element.find("li:first").attr("data-tab");
element.trigger("change.tabs",firstName);
return this;
};
我们看到使用自定义事件回调可以让代码更加整洁。 这也意味着选项卡状态切换回调彼此分离, 这也让插件代码更具扩展性。 比如我们可以在程序中直接更改选项卡的状态,只需触发被观察列表的 change.tabs 事件即可 :$(“#tabs”).trigger(“change.tabs”, “users”);
同样, 我们可以将切换选项卡的动作和窗口的 hash 做关联, 这样就可以使用浏览器的后退按钮了 :
$("#tabs").bind("change.tabs", function(e, tabName){
window.location.hash = tabName;
});
$(window).bind("hashchange", function(){
var tabName = window.location.hash.slice(1);
$("#tabs").trigger("change.tabs", tabName);
});