《jQuery基础教程》学习笔记(一)——jQuery事件
一、事件处理程序综述
总的来说,jQuery用于响应网页的加载事件都是通过事件处理程序 $(document).ready()
来处理函数中的代码。因为jQuery是一个基于JavaScript的类库,我们当然可以通过js原生的 window.onload
来实现相同的效果。但是这二者之间在触发的时间上存在细微的差别。具体如下:
window.onload
事件只有在文档全部加载完毕后才会触发,而$(document).ready()
事件处理程序在DOM加载完毕后就可以被调用
这样的差异在规模较小的网页中基本感觉不出来有什么不同。但假如一个网页包含上千张图片,如果采用原生的window.onload
方式则要在所有的图片都加载完毕后才会执行,与之不同,$(document).ready()
在网页DOM加载完毕后就可以执行。
采用第一种方式,在网页尚未加载完毕的情况下,如果用户已经点击了一些具备默认行为的元素(e.g. 链接),则浏览器会跳转到该链接的目标地址,而不是js捆绑的自定义事件。
window.onload
在js中只能被绑定一次,而$(document).ready()
可以绑定多个事件。
如果在一个js文件中同时出现了
window.onload = firstEvent;
function firstEvent() {
//first event...
}
window.onload = secondEvent;
function secondEvent() {
//second event...
}
则第二次绑定的事件会覆盖第一个绑定的事件,即待网页全部加载完毕后,只会执行函数 secondEvent()
中定义的行为。而使用$(document).ready()
绑定多个事件后,每次调用这个方法都会向内部行为队列添加一个新的函数,当页面DOM加载完毕后,这些函数会按照之前注册的顺序依次执行。
当给 $(document).ready()
传递一个函数作为参数时,jQuery会隐式地调用 .ready()
方法。即,对于:
$(document).ready(function() {
//some codes...
});
可以简单写成:
$(function() {
//some codes...
});
不过我个人喜欢第一种方式,显式地声明 .ready() 方式有助于提醒我jQuery事件执行的时间。
二、事件传播
理解jQuery的事件传播机制能够避免一些意想不到的情况,对此加以利用还可以很好的使用事件委托功能。假设一个HTML页面如下:
<div class="foo">
<span class="bar">
<a href="http://www.example.com/">
The quick brown fox jumps over the lazy dog.
</a>
</span>
<p>
How razorback-jumping frogs can level six piqued gymnasts!
</p>
</div>
这段代码在浏览器中结构如下:
图1 网页结构
如果点击了<a>
元素,那么处于同一位置的它的祖先<span>
元素和<div>
元素都可以响应到点击操作。允许多个网页元素响应同一事件的第一种策略是事件捕获。在事件捕获的过程中,事件首先会交给最外层的元素,接着再交给更具体的元素。在这个例子中,意味着单击事件首先会传递给<div>
,然后是<span>
,最后是<a>
。如下图所示。
图2 事件捕获模型
另一种相反的策略叫做事件冒泡。即当事件发生时,会首先发送给最具体的元素,在这个元
素获得响应机会之后,事件会向上冒泡到更一般的元素。在这个例子中, <a>
会首先处理事件,然后按照顺序依次是<span>
和<div>
。在DOM标准中同时采取这两种策略:首先,事件要从一般元素到具体元素逐层捕获,然后,事件再通过**冒泡返回**DOM树的顶层。而事件处理程序可以注册到这个过程中的任何一个阶段。
因此, 为了确保跨浏览器的一致性, jQuery始终会在模型的冒泡阶段注册事件处理程序。因此,可以认为在jQuery中最具体的元素会首先获得响应事件的机会。
2.1 事件冒泡的副作用
参考这样一段HTML代码:
<div id="switcher" class="switcher">
<h3>Style Switcher</h3>
<button id="switcher-default">Default</button>
<button id="switcher-narrow">Narrow Column</button>
<button id="switcher-large">Large Print</button>
</div>
相应的jQuery代码如下:
$(document).ready(function() {
$('#switcher').click(function() {
$('#switcher button').toggleClass('hidden');
});
});
可以看出,这段代码的本意是当点击<div>
时,内部的三个<button>
在隐藏/出现这两个效果中交替;点击三个<button>
按键时,改变网页的字体大小。但是由于事件冒泡的机制,点击<button>
这个事件会冒泡上浮至其上层DOM元素——<div id="switcher">
捕获,从而触发代码 $('#switcher').click(function() {});
中定义的内容——使得三个按键隐藏。
2.2 通过事件对象改变事件的旅程
对于上面描述的关于事件冒泡产生负作用,可以有多种方式解决。但是不论采取何种方式,我们必定要通过对事件这一对象进行处理,才能达到我们理想的效果。因此,在jQuery中提出了事件对象 这一术语。事件对象是一种DOM结构,它会在元素获得处理事件的机会时传递给被调用的事件处理程序。这个对象中包含着与事件有关的信息(例如事件发生时的鼠标指针位置),也提供了可以用来影响事件在DOM中传递进程的一些方法。因此,我们需要给相应的函数添加一个参数作为处理程序中的事件对象。习惯上我们将事件对象命名为event。
$(document).ready(function() {
$('#switcher').click(function(event) {
$('#switcher button').toggleClass('hidden');
});
});
2.2.1 事件目标
由上面描述可知,事件处理程序中的变量event保存着事件对象。因此,只需要对变量event进行相应操作就可以改变事件。在DOM API中规定了event有一个target属性,保存着发生事件的目标元素。通过.target,可以确定DOM中首先接收到事件的元素(即实际被单击的元素)。
因此,2.1中提到的事件传播的副作用可以通过判断事件目标(.target)来避免。
$(document).ready(function() {
$('#switcher').click(function(event) {
if (event.target == this) {
$('#switcher button').toggleClass('hidden');
}
});
});
这里的event代表当文件加载完毕后的点击事件;.target代表了具体是哪一个 DOM 元素触发了点击事件;this引用的是捕捉到点击操作的DOM元素<div id="switcher">
。当点击<div id="switcher">
的内部DOM元素(如<button>
或者<h3>
)时,event.target代表被点击的是<button>
元素或<h3>
元素,若此时event.target == this
,即被<div id="switcher">
捕获到的click事件是通过点击自身而不是通过点击子元素<button>
或<h3>
冒泡而来,在满足这种条件下,相应元素才进行隐藏/显示效果。因此,上述代码通过判断事件目标来源的方式阻止了事件冒泡。
2.2.2 停止事件传播
除了通过根据判断事件目标的方式来阻止事件传播的方式外,jQuery还提供了一个一劳永逸的方式——通过事件对象的.stopPropagation()方法来完全阻止事件冒泡。所以,我们可以对2.2.1中的代码进行修改,去掉if条件判断,而是在<button>
的click事件中调用.stopPropagation()方法阻止了事件的传播。
$(document).ready(function() {
$('#switcher').click(function(event) {
$('#switcher button').toggleClass('hidden');
});
});
$(document).ready(function() {
$('#switcher button').click(function(event) {
//some code...
event.stopPropagation();
});
});
正常情况下,点击了<button id="switcher-*">
之后,会将这个事件冒泡至其所有的DOM祖先节点,但是由于event.stopPropagation()
方法的调用,阻止了click事件的传播,从而避免其祖先节点响应这个事件。
三 事件委托
3.1 有效地利用事件传播机制
我们回到2.2.1中的例子,通过if语句可以排除点击<div id="switcher">
内部节点的情况,不过,如果我们添加了else语句,就可以处理点击<div id="switcher">
内部节点的时间;更进一步,通过判断event.target
的值还可以细分到底是点击<div id="switcher">
事件、点击<button>
事件还是点击<h3>
事件。所有的这些操作都只需要在$('#switcher').click(function(event) {}
中完成即可,相比于在<div>
、<button>
和<h3>
中分别定义函数操作以及相应地调用event.stopPropagation()
方法,不仅节省了代码编写,而且jQuery只需要遍历一次DOM元素就可以完成三种情况的操作,这在大规模网页代码效率的影响是很重要的。例如,有一个显示信息的大型表格,每一行都有一项需要注册单击处理程序。虽然不难通过隐式迭代来指定所有单击处理程序,但性能可能会很成问题,因为循环是由jQuery在内部完成的,而且要维护所有处理程序也需要占用很多内存。
为解决这个问题,可以只在DOM中的一个祖先元素上指定一个单击处理程序。由于事件会冒泡,未遭拦截的单击事件最终会到达这个祖先元素,而我们可以在此时再作出相应处理。因此,我们可以看到,事件冒泡并不总是带来问题,也可以利用它为我们带来好处。 事件委托就是利用冒泡的一项高级技术。通过事件委托,可以借助一个元素上的事件处理程序完成很多工作。
$(document).ready(function() {
$('#switcher').click(function(event) {
if ($(event.target).is('button')) {
//some code...
} else {
$('#switcher button').toggleClass('hidden');
}
});
});
<div id="switcher>
是转换器的祖先元素,因此在这个div中不论点击什么元素,最终都会由于事件冒泡作用而被<div id="switcher>
捕获。之后再通过事件target属性来分配具体操作,减少了多次遍历DOM元素的工作,提高了效率。
3.2 内置的事件委托功能
由于事件委托可以解决很多问题,jQuery提供了.on()方法可以接受相应参数实现事件委托。
$('#switcher').on('click', 'button', function() {
//some code...
});
这种做法是将<div id="switcher">
内部所有DOM元素(包括<button>
和<h3>
)的click事件委托给<div id="switcher">
,从而当每次<div id="switcher">
检测到点击事件时(不论是电机自身瀚还是子元素事件冒泡而来),jQuery会把click事件处理程序绑定到#switcher对象,同时比较event.target
和选择符表达式(.on()
方法的第二个参数 ,这里是’button’)是否匹配,如果是,jQuery会把this关键字映射到匹配的元素(一切关于button的操作不再需要$('#button')
或event.target
来查找,只需通过$(this)
即可),完成该方法定义的事件。
四 改变事件绑定
4.1 解除绑定
有时候,我们需要停用以前注册的事件处理程序。可能是因为页面的状态发生了变化,导致相应的操作不再有必要。处理这种情形的一种典型做法,就是在事件处理程序中使用条件语句。但是,如果能够完全移除处理程序绑定显然更有效率。
还是回到一开始引出的那个例子。<div id="switcher">
是一个样式选择器,里面包含三个按钮。
<div id="switcher" class="switcher">
<h3>Style Switcher</h3>
<button id="switcher-default">Default</button>
<button id="switcher-narrow">Narrow Column</button>
<button id="switcher-large">Large Print</button>
</div>
当点击Narrow Column按钮时页面为小号字体、宽度为屏幕一般;当点击Large Print按钮时页面大字体显示;当点击Default按钮时页面以默认格式显示。此外,当点击<div id="switcher">
内部而不是<button>
时,三个按钮隐藏。
上述要求通过前面的事件委托和阻止事件传播均可实现,下面来考虑一些额外要求。假设我们希望折叠样式转换器<div id="switcher">
在页面没有使用正常样式的情况下保持扩展状态,即当Narrow Column或 Large Print按钮被选中时,单击样式转换器的背景区域不应该引发任何操作。为此,可以在单击非默认样式转换按钮时,调用.off()方法移除折叠处理程序。
$(document).ready(function() {
$('#switcher').click(function(event) {
if (!$(event.target).is('button')) {
$('#switcher button').toggleClass('hidden');
}
});
$('#switcher-narrow, #switcher-large').click(function() {
$('#switcher').off('click');
});
});
现在,如果单击Narrow Column按钮,样式转换器(<div>
)上的单击处理程序就会被移除。然后,再单击背景区域将不会导致它折叠起来。但是,按钮本身的作用却失效了!这是因为我们将点击<button>
事件委托给<div>
来处理,由于之前点击<button>
使得<div>
通过.off()方法移除了click事件,因此,委托给<div>
的click<button>
事件也由于<div>
click事件的移除而消失。换句话说,在调用$('#switcher').off('click')
时,会导致按钮上绑定的两个事件处理程序都被移除。
4.2 为事件处理程序添加命名空间
因此,应该让对.off()的调用更有针对性,以避免把注册的两个单击处理程序全都移除。一种实现方式是使用事件命名空间,即在绑定事件时引入附加信息,以便将来识别特定的处理程序。要使用命名空间,必须使用绑定事件处理程序的非简写方法,即.on()方法本身。
我们为.on()方法传递的第一个参数,应该是想要截获的事件的名称。不过,在此可以使用
一种特殊的语法形式,即对事件加以细分。
$(document).ready(function() {
$('#switcher').on('click.collapse', function(event) {
if (!$(event.target).is('button')) {
$('#switcher button').toggleClass('hidden');
}
});
$('#switcher-narrow, #switcher-large').click(function() {
$('#switcher').off('click.collapse');
});
});
对于事件处理系统而言,后缀.collapse是不可见的。这里仍然会像编写.on('click')
一样,让注册的函数响应单击事件。但是,通过附加的命名空间信息,则可以解除对这个特定处理程序的绑定,同时不影响为按钮注册的其他单击处理程序。换句话说,对于<div>
捕获的所有click事件,我们将其分为两类,其中一类放置在一个称为.collapse的明命名空间,它特指隐藏<button>
的click事件。因此,我们将这一特定事件解绑并不会影响到另一个click事件——即改变页面显示的click事件。
4.3 重新绑定事件
现在单击Narrow Column或Large Print按钮,会导致样式转换器的折叠功能失效。可是,我们希望该功能在单击Default按钮时恢复。为此,应该在Default按钮被单击时, 重新绑定事件处理程序。
$(document).ready(function() {
var toggleSwitcher = function(event) {
if (!$(event.target).is('button')) {
$('#switcher button').toggleClass('hidden');
}
};
$('#switcher').on('click.collapse', toggleSwitcher);
});
这里使用了另一种定义函数的语法,即没有使用函数声明(前置function关
键字) ,而是将一个匿名函数表达式指定给了一个局部变量,也叫做命令函数。它有以下两点好处:
- 将函数表达式指定给局部变量,在后续调用该函数时可以直接用该变量来代替,避免了大段的函数代码块,易于阅读;
- 使用命令函数还可以省去命名空间的麻烦,因为.off()操作可以讲这个可以将这个命名函数作为第二个参数。换句话说,
$('#switcher').off('click', toggleSwitcher)
相当于$('#switcher').on('click.toggleSwitcher')
。
不过,上述代码还存在另一个问题,在jQuery中把处理程序绑定到事件时,之前绑定的处理程序仍然有效。在这个例子中,每次点击Normal,就会有一个toggleSwitcher的副本被绑定到样式转换器。在用户单击Narrow或Large Print之前(这样就可以一次性地解除对toggleSwitcher的绑定),每多单击一次都会多调用一次这个函数。
在绑定toggleSwitcher偶数次的情况下,单击样式转换器(不是按钮),好像一切都没有发生变化。事实上,这是因为切换了hidden类偶数次,结果状态与开始的时候相同。为了解决这个问题,可以在用户单击任意按钮时解除绑定,并在确定单击按钮的ID是switcher-default的情况下再重新绑定
$(document).ready(function() {
var toggleSwitcher = function(event) {
if (!$(event.target).is('button')) {
$('#switcher button').toggleClass('hidden');
}
};
$('#switcher').on('click', toggleSwitcher);
$('#switcher button').click(function() {
$('#switcher').off('click', toggleSwitcher);
if (this.id== 'switcher-default') {
$('#switcher').on('click', toggleSwitcher);
}
});
});
五 键盘事件
对于敲击键盘事件,主要分为两类:
- 对键盘按键给出响应的事件(keyup和keydown)
- 对文本输入给出响应的事件(keypress)
keyup和keydown只关心按下的是哪个键,keydown 事件会在键盘按下时触发,而
keyup 事件会在按键释放时触发。keypress则关心用户输入的是什么字符,例如只按A键输入的文本应该是小写字母a,而同时按Shift和A键,输入的文本是大写字母A。
在这个例子中,我们想实现按下D键相当于点击Default按钮功能,按下N键相当于点击Narrow按钮功能,按下L键相当于点击Large按钮功能。所以,我们并不关心用户输入的具体是大写字母还是小写字母,因此,我们只需要调用keyup()方法即可。
敲击键盘后,需要知道按下的具体某个键的值,jQuery提供了了两种方法:
- event.keyCode()
- String.fromCharCode(event.which)
event.keyCode()获得相应的ASCII码值,例如按下上、下、左、右键,分别返回38、40、37和39;对于String.fromCharCode(event.which)而言,事件对象的.which属性包含着被按下的那个键的标识符。对于字母键而言,这个标识符就是相应大写字母的ASCII值。
$(document).ready(function() {
var triggers = {
D: 'default',
N: 'narrow',
L: 'large'
};
$(document).keyup(function(event) {
var key = String.fromCharCode(event.which);
if (key in triggers) {
$('#switcher-' + triggers[key]).click();
}
});
});
至此,《jQuery基础教程》第三章《事件》就学习结束了。当然,jQuery事件的内容不仅仅只有这些,还有许多更深入的知识有待学习。一开始,我仅仅会使用.click()这类基本的简化方法,学习了这一章之后才了解到还有事件传播、事件委托等重要知识。而且有关事件委托的内容我也是第二遍看书是才完全理解。书本上有些语句叙述地有点难以理解,因此将书本上重要的部分以及自己的理解记录下来,以备日后忘记时回顾一二。