整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

HTML DOM-事件

TML DOM 允许 JavaScript 对 HTML 事件作出反应。

实例

Mouse Over Me

Click Me

avaScript与HTML之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。可以使用侦听器(或处理程序)来预定义事件,以便事件发生时执行相应的代码。这种在传统软件工程中被称为观察者模式的模型,支持页面的行为与页面的外观之间的松散耦合。

事件最早是在IE3和NetscapeNavigator 2中出现的,当时是作为分担服务器运算负载的一种手段。在IE4和Navigator4发布时,这两种浏览器都提供了相似但不相同的API,这些API并存经过了好几个主要版本。DOM2级规范开始尝试以一种符合逻辑的方式来标准化DOM事件。IE9、Firefox、Opera、Safari和Chrome全都实现了"DOM2级事件"模块的核心部分。IE8是最后一个仍然使用其专有事件系统的主要浏览器。

事件流

当浏览器发展到第四代时,浏览器开发团队遇到了一个很有意思的问题:页面的哪一部分会拥有某个特定的事件?要明白这个问题问的是什么,可以想象画在一张纸上的一组同心圆。如果把手指放在圆心上,那么指向的不是一个圆,而是纸上的所有圆。两家公司的浏览器开发团队在看待浏览器事件方面还是一致的。如果单击了某个按钮,都认为单击事件不仅仅发生在按钮上。也就是说,单击按钮的同时,也单击了按钮的容器元素,甚至也单击了整个页面。

事件流描述的是从页面中接收事件的顺序。但有意思的是,IE和Netscape开发团队居然提出了差不多是完全相反的事件流的概念。IE的事件流是事件冒泡流,而Netscape Communicator的事件流是事件捕获流。

事件冒泡

IE的事件流叫做事件冒泡,即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。所有现代浏览器都支持事件冒泡,但在具体实现上还是有一些差别。IE5.5及更早版本中的事件冒泡会跳过<html>元素(从<body>直接跳到document)。IE9、Firefox、Chrome和Safari则将事件一直冒泡到window对象。

事件捕获

NetscapeCommunicator团队提出的另一种事件流叫做事件捕获。事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用以在于在事件到达预定目标之前捕获它。虽然事件捕获是Netscape Communicator唯一支持的事件流模型,但IE9、Safari、Chrome、Opera和Firefox目前也支持这种事件流模型。尽管"DOM2级事件"规范要求事件应该从document对象开始传播,但这些浏览器都是从window对象开始捕获事件的。

DOM事件流

"DOM2级事件"规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。

事件处理程序

事件就是用户或浏览器自身执行的某种动作。诸如click、load和mouseover,嗾使事件的名字。而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字以"on"开头,因此click事件的事件处理程序就是onclick、load事件的事件处理程序就是onload。为事件指定处理程序的方式有好几种。

HTML事件处理程序

某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的HTML特性来指定。这个特性的值应该是能够执行的JavaScript代码。例如,要在按钮被单击时执行一些JavaScript,可以像下面这样编写代码:

<input type="button"onclick="alert('Clicked')" value="Click!"/>

当单击这个按钮时,就会显示一个警告框。这个操作是通过指定onclick特性并将一些JavaScript代码作为它的值来定义的。由于这个值是JavaScript,因此不能在其中使用未经转义的HTML语法字符,例如和号(&)、双引号("")、小于号(<)或大于号(>)。为了避免使用HTML实体,这里使用了单引号。如果想要使用双引号,那么既要将代码改成如下所示:

<input type="button"onclick="alert("Clicked")" value="Click!"/>

在HTML中定义的事件处理程序可以包含要执行的具体动作,也可以调用在页面其他地方定义的脚本。如下面的例子所示:

<script type="text/javascript">

function showMessage(){

alert("Hello World!");

}

</script>

<input type="button"onclick="showMessage()"value="Click!"/>

在这个例子中,单击按钮就会调用showMessage()函数。这个函数是在一个独立的<script>元素中定义的,当然也可以被包含在一个外部文件中。事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。

这样指定事件处理程序具有一些独到之处。首先,这样会创建一个封装着元素属性值的函数。这个函数中有一个局部变量event,也就是事件对象。

<!--输出"click" -->

<input type="button"onclick="alert(event.type)" value="Click!">

通过event变量,可以直接访问事件对象,不用自己定义它,也不用从函数的参数列表中读取。在这个函数内部,this值等于事件的目标元素。

<!--输出"Click!" -->

<input type="button"onclick="alert(this.value)" value="Click!">

关于这个动态创建的函数,另一个有意思的地方是它扩展作用域的方式。在这个函数内部,可以像访问局部变量一样访问document及该元素本身的成员。这个函数使用with像下面这样扩展作用域:

function funcName(){

with(document){

with(this){

//元素属性值

}

}

}

如此一来,事件处理程序要访问自己的属性就简单多了。下面这行代码与前面的例子效果相同:

<!--输出"Click!" -->

<input type="button"onclick="alert(value)"value="Click!">

如果当前元素是一个表单输入元素,则作用域中还会包含表单元素(父元素)的入口,这个函数就变成了如下所示:

function funcName(){

with(this.form){

with(this){

//元素属性值

}

}

}

实际上,这样扩展作用域的方式无非就是想让事件处理程序无需引用表单元素就能访问其他表单字段。例如:

<form method="post">

<input type="text"name="username_" value="">

<input type="button"value="Echo Username" onclick="alert(username_.value)">

</form>

在这个例子中,单击按钮会显示文本框中的文本,值得注意的是,这里直接引用了username_元素。不过,在HTML中指定事件处理程序有两个缺点。首先,存在一个时差问题。因为用户可能会在HTML元素一出现在页面上就触发相应的事件,但当时的事件处理程序可能尚不举杯执行条件。以前面的例子来说明,假设showMessage()函数是在按钮下方、页面的最底部定义的。如果用户在页面解析showMessage()函数之前就点击了按钮,就会引发错误。为此,很多HTML事件处理程序就会被封装在try-catch块中,以便错误不会浮出水面。

另一个缺点是,这样扩展时间处理程序的作用域链在不同浏览器中会导致不同结果。不同JavaScript引擎遵循的标识符解析规则略有差异,很可能在访问非限定对象成员时出错。

通过HTML指定时间处理程序的最后一个缺点是HTML与JavaScript代码紧密耦合。如果要更换事件处理程序,就要改动两个地方:HTML代码和JavaScript代码。而这也是许多开发人员摒弃HTML事件处理程序,转而使用JavaScript指定时间处理程序的原因所在。

DOM0 级事件处理程序

通过JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个时间处理程序属性。这种为事件处理程序赋值的方法是在第四代Web浏览器中出现的,而且至今仍然为所有现代浏览器所支持。原因一是简单,二是具有跨浏览器的优势。要使用JavaScript指定事件处理程序,首先必须取得一个要操作的对象的引用。

每个元素(包括window和document)都有自己的事件处理程序属性,这些属性通常全部小写,例如onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序,如:

varbtn=document.getElementById("myBtn");

btn.onclick=function(){

alert("Clicked");

}

在此,我们通过文档对象取得了一个按钮的引用,然后为它指定了onclick事件处理程序。但要注意,在这些代码运行以前不会指定事件处理程序,因此如果这些代码在页面中位于按钮后面,就有可能在一段时间内怎么单击都没有反应。

使用DOM 0 级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的this引用当前元素。可以在事件处理程序中通过this访问元素的任何属性和方法。以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。

也可删除通过DOM0级方法指定的事件处理程序,只要像下面这样将时间处理程序属性的值设置为null即可:

btn.onclick=null;//删除事件处理程序

如果使用HTML指定事件处理程序,那么onclick属性的值就是一个包含着在同名HTML特性中指定的代码的函数。而将相应的属性设置为null,也可以删除以这种方式指定的事件处理程序。

DOM2级事件处理程序

"DOM2级事件"定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()和removeEventListener()。所有DOM节点中都包含这两个方法,并且它们都接受3个参数:要处理的事件名、作为时间处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。

要在click事件添加事件处理程序,可以使用下列代码:

varbtn=document.getElementById("myBtn");

btn.addEventListener("click",function(){

alert(this.id);

},false);

上面的代码为一个按钮添加了onclick事件处理程序,而且该事件会在冒泡阶段被触发(因为最后一个参数是false)。与DOM0级方法一样,这里添加的事件处理程序也是在其依附的元素的作用域中运行。使用DOM2级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。如:

varbtn=document.getElementById("myBtn");

btn.addEventListener("click",function(){

alert(this.id);

},false);

btn.addEventListener("click",function(){

alert("Hello World");

},false);

这里为按钮添加了两个事件处理程序。这两个事件处理程序会按照添加它们的顺序触发,因此首先会显示元素的ID,其次会显示"Hello World"消息。

通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除,移除时传入的参数与添加时使用的参数相同。这也意味着通过addEventListener()添加的匿名函数将无法移除,如:

varbtn=document.getElementById("myBtn");

btn.addEventListener("click",function(){

alert(this.id);

},false);

btn.removeEventListener("click",function(){//没有用!

alert(this.id);

},false);

在这个例子中,使用addEventListener()添加了一个事件处理程序。虽然调用removeEventListener()时看似使用了相同的参数,但实际上,第二个参数与传入addEventListener()中的那一个是完全不同的函数。而传入removeEventListener()中的事件处理程序函数必须与传入addEventListener()中的相同,如:

var handler = function(){

alert(this.id);

}

varbtn=document.getElementById("myBtn");

btn.addEventListener("click",handler,false);

btn.removeEventListener("click",handler,false);//有效

重写后的例子没有问题,是因为在addEventListener()和removeEventListener()中使用了相同的函数。

大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。最好只在需要在事件达到目标之前截获它的时候将事件处理程序添加到捕获阶段。如果不是特别需要,不建议在事件捕获阶段注册事件处理程序。

IE事件处理程序

IE实现了与DOM中类似的两个方法:attachEvent()和detachEvent()。这两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。由于IE8及更早版本只支持事件冒泡,所以通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段。

要使用attachEvent()为按钮添加一个事件处理程序,可以使用以下代码。

varbtn=document.getElementById("myBtn");

btn.attachEvent("onclick",function(){

alert("Clicked");

});

注意,attachEvent()的第一个参数是"onclicke",而非DOM的addEventListener()方法中的"click"。

在IE中使用attachEvent()与使用DOM0级方法的主要区别在于事件处理程序的作用域。在使用DOM0级方法的情况下,事件处理程序会在其所属元素的作用域内运行;在使用attachEvent()方法的情况下,事件处理程序会在全局作用域中运行,因此this等于window。在编写跨浏览器的代码时,牢记这一区别非常重要。

与addEventListener()类似,attachEvent()方法也可以用来为一个元素添加多个事件处理程序。如:

varbtn=document.getElementById("myBtn");

btn.attachEvent("onclick",function(){

alert("Clicked");

});

btn.attachEvent("onclick",function(){

alert("Hello World");

});

这里,调用了两次attachEvent(),为同一个按钮添加了两个不同的事件处理程序。不过,与DOM方法不同的是,这些事件处理程序不是以添加它们的顺序执行,而是以相反的顺序被触发。单击这个例子中的按钮,首先看到的是"Hello World",然后才是"Clicked"。

使用attachEvent()添加的事件可以通过detachEvent()来移除,条件是必须提供相同的参数。

IE11中需要使用addEventListener()和removeEventListener()。

跨浏览器的事件处理程序

为了以跨浏览器的方式处理事件,不少开发人员会使用能够隔离浏览器差异的JavaScript库,还有一些开发人员会自己开发最合适的事件处理的方法。自己编写代码其实也不难,只要恰当地使用能力检测即可。要保证处理事件的代码能在大多数浏览器下一致地运行,只需关注冒泡阶段。

第一个要创建的方法是addHandler(),它的职责是视情况分别使用DOM0级方法、DOM2级方法或IE方法来添加事件。这个方法属于一个叫EventUtil的对象。addHandler()方法接受3个参数:要操作的元素、事件名称和事件处理程序函数。

与addHandler()对应的方法是removeHandler(),它也接受相同的参数。这个方法的职责是移除之前添加的事件处理程序——无论事件程序是采用什么方式添加到元素中的,如果其他方法无效,默认采用DOM0级方法。

var EventUtil={

addHandler:function(element,type,handler){

type=type.toLowerCase();

type=type.substring(0,2)=="on"?type:"on"+type;

if(element.addEventListener){

element.addEventListener(type.substring(2),handler,false);

}else if(element.attachEvent){

element.attachEvent(type,handler);

}else{

element[type]=handler;

}

},

removeHandler:function(element,type,handler){

type=type.toLowerCase();

type=type.substring(0,2)=="on"?type:"on"+type;

if(element.removeEventListener){

element.removeEventListener(type.substring(2),handler,false);

}else if(element.detachEvent){

element.detachEvent(type,handler);

}else{

element[type]=null;

}

}

};

这两个方法首先都会检测传入的元素是否存在DOM2级方法。如果存在DOM2级方法,则使用该方法,其次是IE方法,最后是DOM0级方法。无论传入的事件名称是DOM类型的还是IE中的,都会自动匹配成适合的而且会进行小写转换。使用上面方法时先获取到element,定义好handler事件传入对应的方法中即可。

事件对象

在触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件对象中,会包含鼠标位置的信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。所有浏览器都支持event对象,但支持方式不同。

DOM中的事件对象

兼容DOM的浏览器会将一个event对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法(DOM0级或DOM2级),都会传入event对象。event对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。不过,所有事件都会有下表列出的成员。

属性/方法类型读/写说明bubblesBoolean只读表明事件是否冒泡cancelableBoolean只读表名是否可以取消事件的默认行为currentTargetElement只读其事件处理程序当前正在处理事件的那个元素defaultPreventedBoolean只读为true表示已经调用了preventDefault()detailInteger只读与事件相关的细节信息eventPhaseInteger只读调用事件处理程序的阶段:1事件捕获,2处于目标,3冒泡阶段preventDefault()Function只读取消事件的默认行为。如果cancelable为true,可以使用这个方法stopImmediatePropagation()Function只读取消事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用。stopPropagation()Function只读取消事件的进一步捕获或冒泡。如果bubbles为true,则可以使用这个方法targetElement只读事件的目标trustedBoolean只读为true表示事件是浏览器生成的,false表示事件是由开发人员通过JavaScript创建的typeString只读被触发的事件的类型viewAbstractView只读与事件关联的抽象视图。等同于发生事件的window对象在事件处理程序内部,对象this始终等于currentTarget的值,而target则只包含事件的实际目标。如果直接将事件处理程序指定给了目标元素,则this、currentTarget和target包含相同的值。

在需要通过一个函数处理多个事件时,可以使用type属性。下面使用事件对象做一个例子,模拟购物网站中鼠标在图片上时显示该图片的局部放大图,点击该图片时,在新的网页中显示该图片,需要做一些准备:id为kitty的背景图(beautykitty.jpg)的像素大小为该div的大小,将该图片复制一份(beautykittybig.jpg),并调整大小为原图片宽高x4。然后就可以实验下面的例子了。

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>Sample Page</title>

</head>

<body>

<table>

<tr>

<td>

<div id='kitty' style=";">

<div id="SQ" style="position: relative;background:#2196F3;;"></div>

</div>

</td>

<td>

<div id='bigImg' style=";"></div>

</td>

</tr>

</table>

</body>

<script type="text/javascript">

var EventUtil={

addHandler:function(element,type,handler){

type=type.toLowerCase();

type=type.substring(0,2)=="on"?type:"on"+type;

if(element.addEventListener){

element.addEventListener(type.substring(2),handler,false);

}else if(element.attachEvent){

element.attachEvent(type,handler);

}else{

element[type]=handler;

}

},

removeHandler:function(element,type,handler){

type=type.toLowerCase();

type=type.substring(0,2)=="on"?type:"on"+type;

if(element.removeEventListener){

element.removeEventListener(type.substring(2),handler,false);

}else if(element.detachEvent){

element.detachEvent(type,handler);

}else{

element[type]=null;

}

}

};

varkitty=document.getElementById('kitty');

var clickHandler=function(){

window.open("http://localhost:8080/demo/fiels/beautykitty.jpg","HellKitty");

}

varmousemoveHandler=function(){

var offsetX=(event.offsetX)*4;

var offsetY=(event.offsetY)*4;

var SQStyle=document.getElementById("SQ").style;

SQStyle.visibility="visible";

SQStyle.top=event.offsetY + "px";

SQStyle.left=event.offsetX + "px";

document.getElementById("bigImg").style.background="url('../fiels/beautykittybig.jpg')-"+offsetX+"px -"+offsetY+"px";

}

varmouseoutHandler=function(){

document.getElementById("bigImg").style.background="gray";

}

EventUtil.addHandler(kitty,"onmousemove",mousemoveHandler);

EventUtil.addHandler(kitty,"onclick",clickHandler);

EventUtil.addHandler(kitty,"onmouseout",mouseoutHandler);

</script>

</html>

做出来的效果就是:

要阻止事件的默认行为,可以使用preventDefault()方法。例如,连接的默认行为就是在被单击时会导航到其href特性指定的URL。如果项阻止连接导航这一默认行为,那么通过连接的onclick事件处理程序就可以取消它,如:

varlink=document.getElementById('myLink');

link.onclick=function(){

if(new Date().getHours() > 15){

event.preventDefault();

}

}

只有cancelable属性设置为true的事件,才可以使用preventDefault()来取消其默认行为。

另外,stopPropagation()方法用于立即停止事件在DOM层次中的传播,即取消进一步的事件捕获或冒泡。例如,直接添加到一个按钮的事件处理程序可以调用stopPropagation(),从而避免触发注册在document.body上面的事件处理程序,如:

varlink=document.getElementById('myLink');

link.onclick=function(){

alert('Link clicked!');

event.preventDefault();

event.stopPropagation();

}

document.body.onclick=function(){

alert("Body clicked!");

}

对于这个例子而言,如果步调用stopPropagation(),就会在单击连接时出现两个警告框。调用stopPropagation()后click事件就不会传播到document.body,因此就不会触发注册在这个元素上的onclick事件处理程序。

事件对象的eventPhase属性,可以用来确定事件当前位于事件流的哪个阶段。如果是在捕获阶段调用的事件处理程序,那么eventPhase等于1;如果事件处理程序处于目标对象上,则eventPhase等于2;如果是在冒泡阶段调用的事件处理程序,eventPhase等于3。这里要注意的是,尽管"处于目标"发生在冒泡阶段,但eventPhase仍然一直等于2。

事件类型

Web浏览器中可能发生的事件有很多类型。如前所述,不同的事件类型具有不同的信息,而"DOM3级事件"规定了以下几类事件。

UI(UserInterface,用户界面)事件,当用户与页面上的元素交互时触发;

焦点事件,当元素获得或失去焦点时触发;

鼠标事件,当用户通过鼠标在页面上执行操作时触发;

滚轮事件,当使用鼠标滚轮(或类似设备)时触发;

文本事件,当在文档中输入文本时触发;

键盘事件,当用户通过键盘在页面上执行操作时触发;

合成事件,当为IME(Input MethodEditor,输入法编辑器)输入字符时触发;

变动事件,当底层DOM结构发生变化时触发;

变动名称事件,当元素或属性名称变动时触发。此类事件已经废弃,没有任何浏览器实现它们。

除了这几类事件之外,HTML5也定义了一组事件,而有些浏览器还会在DOM和BOM中实现其他专有事件。这些专有的事件一般都是根据开发人员需求定制的,没有什么规范,因此不用浏览器的实现有可能也不一致。

DOM3级事件模块在DOM2级事件模块基础上重新定义了这些事件,也添加了一些新事件。包括IE9+在内的所有主流浏览器都支持DOM2级事件。IE9+也支持了DOM3级事件。

UI事件

UI事件指的是那些不一定与用户操作有关的事件。这些事件在DOM规范出现之前,都是以这种或那种形式存在的,而在DOM规范中保留是为了向后兼容。现有的UI事件如下。

DOMActivate:表示元素已经被用户操作(通过鼠标或键盘)激活。这个事件在DOM3级事件中被废弃,但Firefox2+和Chrome支持它。考虑到不同浏览器实现的差异,不建议使用这个事件。

load:当页面完全加载后在window上面触发,当所有框架都加载完毕时在框架集上面触发,当图像加载完毕时在<img>元素上触发,或者当嵌入的内容加载完毕时在<object>元素上面触发。

unload:当页面完全卸载后在window上面触发,当所有框架都卸载后在框架集上面触发,或者当嵌入的内容卸载完毕后在<object>元素上面触发。

abort:在用户停止下载过程时,如果嵌入的内容没有加载完,则在<object>元素上面触发。

error:当发生JavaScript错误时window上面触发,当无法加载如想时在<img>元素上面触发,当无法加载嵌入内容时在<object>元素上面触发,或者当有一或多geese框架无法加载时在框架集上面触发。

select:当用户选择文本框(<input>或<textarea>)中的一或多个字符时触发。

resize:当窗口或框架的大小变化时在window或框架上面触发。

scroll:当用户滚动带滚动条的元素中的内容时,在该元素上面触发。<body>元素中包含加载页面的滚动条。

多数这些事件都与window对象或表单控件相关。

处理DOMActivate之外,其他事件在DOM2级事件中都归为HTML事件(DOMActivate在DOM2级事件中仍然属于UI事件)。要确定浏览器是否支持DOM2级事件规定的HTML事件,可以使用如下代码:

var isSupported =document.implementation.hasFeature("UIEvent","2.0");

注意,只有根据"DOM2级事件"实现这些事件的浏览器才会返回true。而以非标准方式支持这些事件的浏览器则会返回false。要确定浏览器是否支持"DOM3级事件"定义的事件,可以使用如下代码:

var isSupported = document.implementation.hasFeature("UIEvent","3.0");

load事件

JavaScript中最常用的一个事件就是load。当页面完全加载后(包括所有图像、JavaScript文件、CSS文件等外部资源),就会触发window上面的load事件。有两种定义onload事件处理程序的方式。第一种方式是使用如下的JavaScript代码:

EventUtil.addHandler(window,"load", function(event){

alert("Loaded!");

});

这是通过JavaScript来指定事件处理程序的方式,使用的前面定义的跨浏览器的EventUtil对象。与添加其他事件一样,这里也给事件处理程序传入了一个event对象。这个event对象中不包含有关这个事件的任何附加信息,但在兼容DOM的浏览器中,event.target属性的值会被设置为document。

第二种指定onload事件处理程序的方式是为<body>元素添加一个onload特性。一般来说,在window上面发生的任何事件都可以在<body/>元素中通过相应的特性来指定,因为在HTML中无法访问window元素。实际上,这只是为了保证向后兼容的一种权宜之计,但所有浏览器都能很好地支持这种方式,建议尽可能使用JavaScript方式。

图像上面也可以触发load事件,无论是在DOM中的图像元素还是HTML中的图像元素。因此可以在HTML中为任何图像指定onload事件处理程序,例如:

<img src="smile.gif" onload="alert('Imageloaded.')">

这样,当例子中的图像加载完毕后就会显示一个警告框。同样的功能还可以使用JavaScript来实现。

还有一些元素也以非标准的方式支持load事件。在IE9+、Firefox、Opera、Chrome和Safari3+及更高版本中,<script>元素也会触发load事件,以便开发人员确定动态加载的JavaScript文件是否加载完毕。与图像不同,只有在设置了<script>元素的src属性并将该元素添加到文档后,才会开始下载JavaScript文件。换句话说,对于<script>元素而言,指定src属性和指定事件处理程序的先后顺序就不重要了。

unload事件

与load事件对应的是unload事件,这个事件在文档被完全卸载后触发。只要用户从一个页面切换到另一个页面,就会发生unload事件。而利用这个事件最多的情况是清除引用,以避免内存泄漏。

resize事件

当浏览器窗口被调整到一个新的高度或宽度时,就会触发resize事件。这个事件在window(窗口)上面触发,因此可以通过JavaScript或者<body>元素中的onresize特性来指定事件处理程序。

关于何时会触发resize事件,不同的浏览器有不同的机制。IE、Safari、Chrome和Opera会在浏览器窗口变化了1像素时就触发resize事件,然后随着变化不断重复触发,应该注意不要在这个事件的处理程序中加入大计算量的代码,因为这些代码可能被频繁执行,从而导致浏览器反应明显变慢。浏览器窗口最小化或最大化时也会触发resize事件。

scroll事件

虽然scroll事件是在window对象上发生的,但它实际表示的则是页面中相应元素的变化。在混杂模式下,可以通过<body>元素的scrollLeft和scrollTop来监控到这一变化;而在标准模式下,除Safari之外的所有浏览器都会通过<html>元素来反映这一变化(Safari仍然基于<body>跟踪滚动位置)。

与resize事件类似,scroll事件也会在文档被滚动期间重复触发,所以有必要尽量保持事件处理程序的代码简单。

焦点事件

焦点事件会在页面获得或失去焦点时触发。利用这些事件并与document.hasFocus()方法及document.activeElement属性配合,可以知晓用户在页面上的行踪。有以下6个焦点事件。

blur:在元素失去焦点时触发。这个事件不会冒泡;所有浏览器都支持它。

DOMFocusIn:在元素获得焦点时触发。这个事件与HTML事件focus等价,但它冒泡。只有Opera支持这个事件。DOM3级事件废弃了DOMFocusIn,选择了focusin。

DomFocusOut:在元素失去焦点时触发。这个事件是HTML事件blur的通用版本。只有Opera支持这个事件。DOM3级事件废弃了DomFocusOut,选择了focusout。

focus:在元素获得焦点时触发。这个事件不会冒泡;所有浏览器都支持它。

focusin:在元素获得焦点时触发。这个事件与HTML事件focus等价,但它冒泡。

focusout:在元素失去焦点时触发。这个事件是HTML事件blur的通用版本。

这一类事件中最主要的两个是focus和blur,它们都是JavaScript早期就得到所有浏览器支持的事件。这些事件的最发问题是它们不冒泡。因此,IE的focusin和focusout与Opera的DOMFocusIn和DomFocusOut才会发生重叠。IE的方式最后被DOM3级事件采纳为标准方式。

当从页面中的一个元素移动到另一个元素,会依次触发下列事件:

(1) focusout在失去焦点的元素上触发;

(2) focusin在获得焦点的元素上触发;

(3) blur在失去焦点的元素上触发;

(4) DomFocusOut在数去焦点的元素上触发;

(5) focus在获得焦点的元素上触发;

(6) DOMFocusIn在获得焦点的元素上触发。

其中,blur、DomFocusOut和focusout的事件目标是失去焦点的元素;而focus、DOMFocusIn和focusin的事件目标是获得焦点的元素。

要确定浏览器是否支持这些事件,可以使用如下代码:

var isSupported =document.implementation.hasFeature("FocusEvent","3.0");

鼠标与滚轮事件

鼠标事件是Web开发中最常用的一类事件,毕竟鼠标还是最主要的定位设备。DOM3级事件中定义了9个鼠标事件:

click:在用户单击鼠标按钮(一般是左边的按钮)或按下回车键时触发。这一点对确保易访问性很重要,意味着onclick事件处理程序既可以通过键盘也可以通过鼠标执行。

dbclick:在用户双击主鼠标按钮(一般是左边的按钮)时触发。从技术上说,这个事件并不是DOM2级事件规范中规定的,但鉴于它得到了广泛支持,所以DOM3级事件将其纳入了标准。

mousedown:在用户按下了任意鼠标按钮时触发。不能通过键盘触发这个事件。

mouseenter:在鼠标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。DOM2级事件并没有定义这个事件,但DOM3级事件将它纳入了规范。

mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。DOM2级事件并没有定义这个事件,但DOM3级事件将它纳入了规范。

mousemove:当鼠标指针在元素内部移动时重复地触发。不能通过键盘触发这个事件。

mouseout:在鼠标指针位于一个元素上方,然后用户将其一如另一个元素时触发。又移入的另一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件。

mouseover:在鼠标指针位于一个元素外部,然后用户将其首次一如另一个元素边界之内时触发。不能通过键盘触发这个事件。

mouseup:在用户释放鼠标按钮时触发。不能通过键盘触发这个事件。

页面上的所有元素都支持鼠标事件。除了mouseenter和mouseleave,所有鼠标事件都会冒泡,也可以被取消,而取消鼠标事件将会影响浏览器的默认行为。取消鼠标事件的默认行为还会影响其他事件,因为鼠标事件与其他事件是密不可分的关系。

只有在一个元素上相继触发mousedown和mouseup事件,才会触发click事件;如果mousedown和mouseup中的一个被取消,就不会触发click事件。类似地,只有触发两次click事件才会触发依次dbclick事件。如果有代码阻止了连续两次触发click事件(可能是直接取消click事件,也可能通过mousedown或mouseup间接实现),那么就不会触发dbclick事件了。在onclick事件中出现了alert语句时也会阻止触发dbclick事件,如下面的dbclick事件处理程序用于不会得到执行:

<button onclick="alert('Click')" ondblclick="alert('DBClick')">点击</button>

这4个事件触发的顺序始终如下:

(1) mousedown

(2) mouseup

(3) click

(4) mousedown

(5) mouseup

(6) click

(7) dbclick

显然,click和dbclick事件都会依赖与其他事件的触发;然而mousedown和mouseup则不受其他事件的影响。

使用以下代码可以检测浏览器是否支持以上DOM2级事件(除dbclick、mouseenter和mouseleave之外):

var isSupported = document.implementation.hasFeature("MouseEvents","2.0");

要检测浏览器是否支持上面的所有事件,可以使用以下代码:

var isSupported =document.implementation.hasFeature("MouseEvent","3.0");

注意,DOM3级事件的feature名是"MouseEvent",而非"MouseEvents"

客户区坐标位置

鼠标事件都是在浏览器视口的特定位置上发生的。这个位置信息保存在事件对象的clientX和clientY属性中。所有浏览器都支持两个属性,它们的值表示事件发生时鼠标在视口中的水平和垂直坐标。

可以使用类似下列代码取得鼠标事件的客户坐标信息,可在页面中看到随着鼠标的移动,坐标的显示跟随变化:

<body style="background-color:antiqueWhite;height:1000px">

<label id="position"style="">(000,000)</label>

</body>

<script type="text/javascript">

var EventUtil={

addHandler:function(element,type,handler){

type=type.toLowerCase();

type=type.substring(0,2)=="on"?type:"on"+type;

if(element.addEventListener){

element.addEventListener(type.substring(2),handler,false);

}else if(element.attachEvent){

element.attachEvent(type,handler);

}else{

element[type]=handler;

}

},

removeHandler:function(element,type,handler){

type=type.toLowerCase();

type=type.substring(0,2)=="on"?type:"on"+type;

if(element.removeEventListener){

element.removeEventListener(type.substring(2),handler,false);

}else if(element.detachEvent){

element.detachEvent(type,handler);

}else{

element[type]=null;

}

}

};

var label =document.getElementById("position");

var showCoordinate= function(event){

var position="(" + event.clientX + " , " + event.clientY + ")";

label.innerText=position;

label.style.paddingTop=(event.clientY-20)+"px";

label.style.paddingLeft=(event.clientX-50)+"px";

}

EventUtil.addHandler(document,"onmousemove",showCoordinate);

</script>

页面坐标位置

通过客户区坐标能够知道鼠标是在视口中什么位置,而页面坐标通过事件对象的pageX和pageY属性,能够告诉你事件是在页面中什么位置发生的。换句话说,这两个属性表示鼠标光标在页面中的位置,因此坐标是从页面本身而非视口的左边和定边计算的。

在页面没有滚动的情况下,pageX和pageY的值与clientX和clientY的值相等。

IE8及更早版本不支持事件对象上的页面坐标,不过使用客户区坐标和滚动信息可以计算出来。这个时候需要用到document.body(混杂模式)或document.documentElement(标准模式)中的scrollLeft和scrollTop属性。

屏幕坐标位置

鼠标事件发生时,不仅会有相对于浏览器窗口的位置,还有一个相对于整个电脑屏幕的位置。而通过screenX和screenY属性就可以确定鼠标事件发生时鼠标指针相对于整个屏幕的坐标信息。

修改键

虽然鼠标事件主要是使用鼠标来触发的,但在按下鼠标时键盘上某些键的状态也可以影响到索要采取的操作。这些修改键就是Shfit、Ctrl、Alt和Meat(在Windows键盘中是Windows键,在苹果机中是Cmd键),它们经常被用来修改鼠标事件的行为。DOM为此规定了4个属性,表示这些修改键的状态:shfitKey、ctrlKey、altKey和metaKey。这些属性中包含的都是布尔值,如果相应的键被按下了,则值为true,否则为false。当某个鼠标事件发生时,通过检测这几个属性就可以确定用户是否同时按下了其中的键。如:

EventUtil.addHandler(btn,"click",function(event){

var keys=new Array();

if(event.shiftKey){

keys.push("shfit");

}

if(event.ctrlKey){

keys.push("ctrl");

}

if(event.altKey){

keys.push("alt");

}

if(event.metaKey){

keys.push("meta");

}

console.log("keys:" + keys.join(","));

});

相关元素

在发生mouseover和mouseout事件时,还会涉及更多的元素。这两个事件都会涉及把鼠标指针从一个元素的边界之内移动到另一个元素的边界之内。对mouseover事件而言,事件的主要目标是获得光标的元素,而相关元素就是哪个失去光标的元素。类似地,对mouseout事件而言,事件的主要目标是失去光标的元素,而相关元素则是光标的元素。

鼠标按钮

只有在主鼠标按钮被单击(或键盘回车被按下)时才会触发click事件,因此检测按钮的信息并不是必要的。但对于mousedown和mouseup事件来说,则在其event对象存在一个button属性,表示按下或释放的按钮。DOM的button属性可能有如下3个值:0表示主鼠标按钮,1表示中间的鼠标滚轮,2表示次鼠标按钮。在常规的设置中,主鼠标按钮就是鼠标左键,而次鼠标按钮就是鼠标右键。

更多的事件信息

"DOM2级事件"规范在event对象中还提供了detail属性,用于给出有关事件的更多信息。对于鼠标事件来说,detail中包含了一个数值,表示在给定位置上发生了多少次单击。在同一个像素上相继地发生一次mousedown和一次mouseup事件算作一次单击。details属性从1开始计数,每次单击发生后都会递增。如果鼠标在mousedown和mouseup之间移动了位置,则details会被重置为0。

鼠标滚轮事件

IE6.0首先实现了mousewheel事件。此后,Opera、Chrome和Safari也都实现了这个事件。当用户通过鼠标滚轮与页面交互、在垂直方向上滚动页面时(无论是向上还是向下),就会触发mousewheel事件。这个事件可以在任何元素上面触发,最终会冒泡到document(IE8)或window(IE9、Opera、Chrome及Safari)对象。与mousewheel事件对应的event对象除包含鼠标事件的所有标准信息外,还包含一个特殊的wheelDelta属性。当用户向前滚动鼠标滚轮时,wheelDelta是120的倍数;当向后时,是-120的倍数。

键盘与文本事件

用户在使用键盘时会触发键盘事件。"DOM2"级事件最初规定了键盘事件,但在最终定稿之前又删除了相应的内容。结果,对键盘事件的支持主要遵循的是DOM0级。

"DOM3级事件"为键盘事件制定了规范,有3个键盘事件:

keydown:当用户按下键盘上的任意键时触发,而且如果按住不放的话,会重复触发此事件。

keypress:当用户按下键盘上的字符键时触发,而且按住不放的话,会重复触发此事件。按下Esc键也会触发这个事件。

keyup:当用户释放键盘上的键时触发。

虽然所有元素都支持以上3个事件,但只有在用户通过文本框输入文本时才最常用到。

只有一个文本事件:textInput。这个事件是对keypress的补充,用以是在将文本显示给用户之前更容易拦截文本。在文本插入文本框之前会触发textInput事件。

在用户按了一下键盘上的字符键时,首先会触发keydown事件,然后紧跟着是keypress事件,最后会触发keyup事件。其中keydown和keypress都是在文本框发生变化之前被触发的;而keyup事件则是在文本框已经发生变化之后被触发的。如果用户按下了一个字符键不放,就会重复触发keydown和keypress事件,知道用户松开该键为止。

如果用户按下的是一个非字符键,那么首先会触发keydown事件,然后就是keyup事件。如果按住这个非字符键不放,那么就会一直重复触发keydown事件,直到用户松开这个键,此时会触发keyup事件。

键盘事件与鼠标事件一样,都支持相同的修改键。而且,键盘事件的事件对象中也有shfitKey、ctrlKey、altKey和metaKey属性。IE8及低版本不支持metaKey。

键码

在发生keydown和keyup事件时,event对象的keyCode属性中会包含一个代码,与键盘上一个特定的键对应。对数字字母字符键,keyCode属性的值与ASCII码中对应小写字母或数字的编码相同。因此,数字键7的keyCode值为55,而字母A键的keyCode值为65——与Shfit的状态无关。DOM和IE的event对象都支持keyCode属性。无论keydown或keyup事件都会存在一些特殊情况。在Firefox和Opera中,按分号键时keyCode值为59,也就是ASCII分号中的编码;但IE和Safari返回186,即键盘中按键的键码。

字符编码

发生keypress事件意味着按下的键会影响到屏幕中文本的显示。在所有浏览器中,按下能够插入或删除的键都会触发keypress事件;按下其他键能够触发此事件因浏览器而异。

IE9、Firefox、Chrome和Safari的event对象都支持一个charCode属性,这个属性只有在发生keypress事件时才包含值,而且这个值是按下的那个键所代表字符的ASCII编码。此时的keyCode通常等于0或者也有可能所按键的键码。IE8及之前版本和Opera则是在keyCode中保存字符的ASCII编码。要想以跨浏览器的方式取得字符编码,首先必须检测charCode属性是否可用,如果不可用则使用keyCode,如下所示:

var EventUtil={

//……

getCharCode:function(event){

if(typeof event.charCode == "number"){

return event.charCode;

}else{

return event.keyCode;

}

}

//……

};

这个方法首先检测charCode属性是否包含数值(在不支持这个属性的浏览器中,值为undefined),如果是返回该值,否则返回keyCode属性值。在取得了字符编码之后,就可以使用String.fromCharCode()将其转换成实际的字符。

DOM3级变化

尽管所有浏览器都实现了某种形式的键盘事件,DOM3级事件还是做出了一些改变。比如,DOM3级事件中的键盘事件,不再包含charCode属性,而是包含两个新属性:key和char。

其中,key属性是为了取代keyCode而新增的,它的值是一个字符串。在按下某个字符键时,key的值就是相应的文本字符(如"k"或"M");在按下非字符键时,key的值是相应键的名(如"Shift"或"Down")。而char属性在按下字符键时的行为与key相同,但在按下非字符键时值为null。

IE9支持key属性,但不支持char属性。Safari5和Chrome支持名为keyIdentifier的属性,在按下非字符键(例如Shfit)的情况下与key的值相同。对于字符键,keyIdentifier返回一个格式类似"U+0000"的字符串,表示Unicode值。

textInput事件

"DOM3级事件"规范中引入了一个新事件,名叫textInput。根据规范,当用户在可编辑区域中输入字符时,就会触发这个事件。这个用于替代keypress的textInput事件的行为稍微有不同。区别之一就是任何可以获得焦点的元素都可以触发keypress时间爱你,但只有可编辑区才能触发textInput事件。区别之二是textInput事件只会在用户按下能够输入实际字符的键时才会被触发,而keypress事件则在按下哪些能够影响文本显示的键时也会触发(例如退格键)。

由于textInput事件主要考虑的是字符,因此它的event对象中还包含一个data属性,这个属性的值就是用户输入的字符(而非字符编码)。换句话说,用户在没有按上档键的情况下按下了S键,data的值就是"s",而如果在按住上档键时按下该键,data的值就是"S"。

复合事件

复合事件是DOM3级事件中新添加的一类事件,用于处理IME的输入序列。IME(Input Method Editor,输入法编辑器)可以让用户输入在物理键盘上找不到的字符。例如,使用拉丁文键盘的用户通过IME照样能输入日文字符。IME通常需要同时按住多个见,但最终只输入一个字符。复合事件就是针对检测和处理这种输入而设计的。有以下三种复合事件。

compositionstart:在IME的文本复合系统打开时触发,表示要开始输入了。

compositionupdate:在向输入字段中插入新字符时触发。

compositionend:在IME的文本复合系统关闭时触发,表示返回正常键盘输入状态。

复合事件与文本事件在很多方面都很相似。在触发复合事件时,目标是接收文本的输入字段。但它比文本事件的事件对象多一个属性data,其中包含以下几个值中的一个:

如果在compositionstart事件发生时访问,包含正在编辑的文本(例如,已经选中的需要马上替换的文本);

如果在compositionupdate事件发生时访问,包含正插入的新字符;

如果在compositionend事件发生时访问,包含此次输入会话中插入的所有字符。

到2016年,大多数的主流浏览器(Safari除外)都已支持复合事件,要确定浏览器是否支持复合事件,可以使用以下代码:

var isSupported=document.implementation.hasFeature("CompositionEvent","3.0");

变动事件

DOM2级的变动事件能在DOM中的某一部分发生变化时给出提示。变动事件是为XML或HTML DOM设计的,并不特定于某种语言。DOM2级定义了如下变动事件。

DOMSubtreeModified:在DOM结构中发生任何变化时触发。这个事件在其他任何事件触发后都会触发。

DOMNodeInserted:在一个节点作为子节点被插入到另一个节点中时触发。

DOMNodeRemoved:在节点从其父节点中被移除时触发。

DOMNodeInsertedIntoDocument:在一个节点被直接插入文档或通过子树直接插入文档之后触发。这个事件在DOMNodeInserted之后触发。

DOMNodeRemovedFromDocument:在一个节点被直接从文档中移除或通过子树间接从文档中移除之前触发。这个事件在DOMNodeRemoved之后触发。

DOMAttrModified:在特性被修改之后触发。

DOMCharacterDataModified:在文本节点的值发生变化时触发。

使用下列代码可以检测出浏览器是否支持变动事件:

var isSupported=document.implementation.hasFeature("MutationEvents","2.0");

删除节点

在使用removeChild()或replaceChild()从DOM中删除节点时,首先会触发DOMNodeRemoved事件。这个事件的目标(event.target)是被删除的节点,而event.relatedNode属性中包含着对目标节点父节点的引用。在这个事件触发时,节点尚未从其父节点删除,因此其parentNode属性仍然指向父节点(与event.relatedNode相同)。这个事件会冒泡,因而可以在DOM的任何层次上面处理它。

如果被移除的子节点包含子节点,那么在其所有子节点及这个被移除的节点上会相继触发DOMNodeRemovedFromDocument事件。这个事件不会冒泡,所以只有直接指定给其中一个子节点的事件处理程序才会被调用。这个事件的目标是相应的子节点或者那个被移除的节点,除此之外event对象中不包含其他信息。

紧随其后触发的是DOMSubtreeModified事件。这个事件的目标是移除节点的父节点;此时的event对象也不会提供与事件相关的其他信息。

插入节点

在使用appendChild()、replaceChild()或insertBefore()向DOM中插入节点时,相继触发的事件为:DOMNodeInserted(冒泡)、DOMNodeInsertedIntoDocument(不冒泡)、DomSubtreeModified。

HTML5事件

DOM规范中没有涵盖所有浏览器支持的所有事件。很多浏览器出于不同的目的——满足用户需求或解决特殊问题,还实现了一些自定义的事件。HTML5详尽列出了浏览器应该支持的所有事件。

contextmenu事件

表示合适应该显示上下文菜单,以便开发人员取消默认的上下文菜单而提供自定义的菜单。该事件冒泡。在所有浏览器中都可以取消这个事件:在兼容DOM的浏览器中,使用event.preventDefault();在IE中,将event.returnValue的值设置为false。

Beforeunload事件

之所以有发生在window对象上的beforeunload事件,是为了让开发人员有可能在页面卸载前阻止这一操作。这个事件会在浏览器卸载页面之前触发,可以通过它来取消卸载并继续使用原有页面。但是不能彻底取消这个事件,因为那就相当于让用户无法离开当前页面了。为此,这个事件的意图是将控制权交给用户,显示的消息会告知用户页面将被卸载,询问用户是否真的要关闭页面,还是希望继续留下来。类似事件的应用场景很多,比如有些电脑程序退出时会询问是否退出,网页游戏在关闭网页时会询问是否再多玩一会……

DOMContentLoaded事件

如前所述,window的load事件会在页面中的一切都加载完毕时触发,但这个过程可能会因为要加载的外部资源过多而颇费周折。而DOMContentLoaded事件则在形成完整的DOM树之后就会触发,不理会图像、JavaScript文件、CSS文件或其他资源是否已经下载完毕。与load事件不同,DOMContentLoaded支持在页面下载的早起添加事件处理程序,这也就意味着用户能够尽早地与页面进行交互。

要处理DOMContentLoaded事件,可以为document或window添加相应的事件处理程序(尽管这个事件会冒泡到window,但它的实际目标实际上是document)。DOMContentLoaded事件对象不会提供任何额外的信息。IE9+、Firefox、Chrome、Safari3.0+和Opera9+都支持DOMContentLoaded事件,通常这个事件既可以添加事件处理程序,也可以执行其他DOM操作。这个事件始终都会在load事件之前触发。

内存和性能

为页面添加大量的处理程序将降低页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪时间。

事件委托

对"事件处理程序过多"问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click事件会一直冒泡到document层次。也就是说,可以为整个页面指定一个onclick事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。


最适合采用事件委托技术的事件包括click、mousedown、mouseup、keydown、keyup和keypress。虽然mouseover和mouseout事件也冒泡,但要适当处理他们并不容易,而且经常需要计算元素的位置(因为当鼠标从一个元素移到其子节点时,或者当鼠标移出该元素时,都会触发mouseout事件)。

移除事件处理程序

每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的JavaScript代码之间就会建立一个连接。当事件处理程序不再使用时,应该在适当的时机将其去除。

模拟事件

事件,就是网页中某个特别值得关注的瞬间。事件经常由用户操作或通过其他浏览器功能来触发。但很少有人知道,可以使用JavaScript在任意时刻来触发特定的事件,而此时的事件就如同浏览器创建的事件一样。也就是说,这些事件该冒泡还会冒泡,而且按照能够导致浏览器执行已经指定的处理他们的事件处理程序。在测试Web应用程序,模拟触发事件是一种极其有用的技术。DOM2级规范为此规定了模拟特定事件的方式。IE9,Opera、Firefox、Chrome和Safari都支持这种方式。IE有它自己模拟事件的方式。

DOM中的事件模拟

可以在document对象上使用createEvent()方法创建event对象。这个方法接收一个参数,即表示要创建的事件类型的字符串。在DOM2级中,所有这些字符串都使用英文复数形式,而在DOM3级中都变成了单数。这个字符串可以是下列几个字符串之一:UIEvents、MouseEvents、MutationEvents和HTMLEvents。

创建完成事件对象后,可以调用事件对象的initXXX()方法,为该事件对象指定具体的属性,然后调用HTML对象的dispatchEvent()方法完成事件的模拟,如:

var btn = document.getElementById("myBtn");

var event = document.createEvent("MouseEvents");

event.initMouseEvent("click",true,true,document.defaultView,0,0,0,0,0,false,false,false,false,0,null);

btn.dispatchEvent(event);

者:京东科技 孙凯

一、前言

相信很多前端开发者在做项目时同时也都做过页面性能优化,这不单是前端的必备职业技能,也是考验一个前端基础是否扎实的考点,而性能指标也通常是每一个开发者的绩效之一。尤其马上接近年关,页面白屏时间是否过长、首屏加载速度是否达标、动画是否能流畅运行,诸如此类关于性能更具体的指标和感受,很可能也是决定着年底你能拿多少年终奖回家过年的晴雨表

关于性能优化,我们一般从以下四个方面考虑:

  1. 开发时性能优化
  2. 编译时性能优化
  3. 加载时性能优化
  4. 运行时性能优化

而本文将从第三个方面展开,讲一讲哪些因素将影响到页面加载总时长,谈到总时长,那总是避免不了要谈及 window.onload,这不但是本文的重点,也是常见页面性能监控工具中必要的API之一,如果你对自己页面加载的总时长不满意,欢迎读完本文后在评论区交流。

二、关于 window.onload

这个挂载到 window 上的方法,是我刚接触前端时就掌握的技能,我记得尤为深刻,当时老师说,“对于初学者,只要在这个方法里写逻辑,一定没错儿,它是整个文档加载完毕后执行的生命周期函数”,于是从那之后,几乎所有的练习demo,我都写在这里,也确实没出过错。

MDN 上,关于 onload 的解释是这样的:load 事件在整个页面及所有依赖资源如样式表和图片都已完成加载时触发。它与 DOMContentLoaded 不同,后者只要页面 DOM 加载完成就触发,无需等待依赖资源的加载。该事件不可取消,也不会冒泡。

后来随着前端知识的不断扩充,这个方法后来因为有了“更先进”的 DOMContentLoaded,在我的代码里而逐渐被替代了,目前除了一些极其特殊的情况,否则我几乎很难用到 window.onload 这个API,直到认识到它影响到页面加载的整体时长指标,我才又一次拾起来它。

三、哪些因素会影响 window.onload

本章节主要会通过几个常用的业务场景展开描述,但是有个前提,就是如何准确记录各种类型资源加载耗时对页面整体加载的影响,为此,有必要先介绍一下前提。

为了准确描述资源加载耗时,我在本地环境启动了一个用于资源请求的 node 服务,所有的资源都会从这个服务中获取,之所以不用远程服务器资源的有主要原因是,使用本地服务的资源可以在访问的资源链接中设置延迟时间,如访问脚本资源 http://localhost:3010/index.js?delay=300,因链接中存在 delay=300,即可使资源在300毫秒后返回,这样即可准确控制每个资源加载的时间。

以下是 node 资源请求服务延迟相关代码,仅仅是一个中间件:

const express = require("express")
const app = express()

app.use(function (req, res, next) {
    Number(req.query.delay) > 0
        ? setTimeout(next, req.query.delay)
        : next()
})


场景一: 使用 async 异步加载脚本场景对 onload 的影响
示例代码:

  <!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>test</title>

      <!-- 请求时长为1秒的js资源 -->
      <script src="http://localhost:3010/index.js?delay=1000" async></script>
  </head>
  <body>
  </body>
  </html>

浏览器表现如下:

通过上图可以看到,瀑布图中深蓝色竖线表示触发了 DOMContentLoaded 事件,而红色竖线表示触发了 window.onload 事件(下文中无特殊情况,不会再进行特殊标识),由图可以得知使用了 async 属性进行脚本的异步加载,仍会影响页面加载总体时长。


场景二:使用 defer 异步加载脚本场景对 onload 的影响
示例代码:

  <!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>test</title>

      <!-- 请求时长为1秒的js资源 -->
      <script src="http://localhost:3010/index.js?delay=1000" defer></script>
  </head>
  <body>
  </body>
  </html>

浏览器表现如下:


由图可以得知使用了 defer 属性进行脚本的异步加载,除了正常的在 DOMContentLoaded 之后触发脚本执行,也影响页面加载总体时长。


场景三:异步脚本中再次加载脚本,也就是常见的动态加载脚本、样式资源的情况
html 代码保持不变,index.js内示例代码:

const script = document.createElement('script')

// 请求时长为0.6秒的js资源
script.src = 'http://localhost:3010/index2.js?delay=600'
script.onload = () => {
    console.log('js 2 异步加载完毕')
}
document.body.appendChild(script)

结果如下:


从瀑布图可以看出,资源的连续加载,导致了onload事件整体延后了,这也是我们再页面中非常常见的一种操作,通常懒加载一些不重要或者首屏外的资源,其实这样也会导致页面整体指标的下降。


不过值得强调的一点是,这里有个有意思的地方,如果我们把上述代码进行改造,删除最后一行的 document.body.appendChild(script),发现 index2 的资源请求并没有发出,也就是说,脚本元素不向页面中插入,脚本的请求是不会发出的,但是也会有反例,这个我们下面再说。

在本示例中,后来我又把脚本请求换成了 css 请求,结果是一致的。

场景四:图片的懒加载/预加载
html 保持不变,index.js 用于加载图片,内容如下:

const img = document.createElement('img')

// 请求时长为0.5秒的图片资源
img.src = 'http://localhost:3010/index.png?delay=500'
document.body.appendChild(img)

结果示意:


表现是与场景三一样的,这个不再多说,但是有意思的来了,不一样的是,经过测试发现,哪怕删除最后一行代码:document.body.appendChild(img)不向页面中插入元素,图片也会发出请求,也同样延长了页面加载时长,所以部分同学就要注意了,这是一把双刃剑:当你真的需要懒加载图片时,可以少写最后一行插入元素的代码了,但是如果大量的图片加载请求发出,哪怕不向页面插入图片,也真的会拖慢页面的时长。

趁着这个场景,再多说一句,一些埋点数据的上报,也正是借着图片有不需要插入dom即可发送请求的特性,实现成功上传的。


场景五:普通接口请求
html 保持不变,index.js 内容如下:

// 请求时长为500毫秒的请求接口
fetch('http://localhost:3010/api?delay=500')

结果如下图:


可以发现普通接口请求的发出,并不会影响页面加载,但是我们再把场景弄复杂一些,见场景六。


场景六:同时加载样式、脚本,脚本加载完成后,内部http接口请求,等请求结果返回后,再发出图片请求或修改dom,这也是更贴近生产环境的真实场景
html 代码:

<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test</title>

    <!-- 请求时长为1.2秒的css -->
    <link rel="stylesheet" href="http://localhost:3010/index.css?delay=1200">

    <!-- 请求时长为0.4秒的js -->
    <script src="http://localhost:3010/index.js?delay=400" async></script>
</head>
<body>
</body>
</html>

index.js 代码:

async function getImage () {
    // 请求时长为0.5秒的接口请求
    await fetch('http://localhost:3010/api?delay=500')

    const img = document.createElement('img')
    // 请求时长为0.5秒的图片资源
    img.src = 'http://localhost:3010/index.png?delay=500'
    document.body.appendChild(img)

}

getImage()

结果图如下:

如图所示,结合场景五记的结果,虽然普通的 api 请求并不会影响页面加载时长,但是因为api请求过后,重新请求了图片资源(或大量操作 dom),依然会导致页面加载时间变长。这也是我们日常开发中最常见的场景,页面加载了js,js发出网络请求,用于获取页面渲染数据,页面渲染时加载图片或进行dom操作。


场景七:页面多媒体资源的加载
示例代码:

<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test</title>
</head>
<body>
    <video src="http://localhost:3010/video.mp4?delay=500" controls></video>
</body>
</html>

结果如图:

对于视频这种多媒体资源的加载比较有意思,video 标签对于资源的加载是默认开启 preload 的,所以资源会默认进行网络请求(如需关闭,要把 preload 设置为 none ),可以看到红色竖线基本处于图中绿色条和蓝色条中间(实际上更偏右一些),图片绿色部分代表资源等待时长,蓝色部分代表资源真正的加载时长,且蓝色加载条在onload的竖线右侧,这说明多媒体的资源确实影响了 onload 时长,但是又没完全影响,因为设置了500ms的延迟返回资源,所以 onload 也被延迟了500ms左右,但一旦视频真正开始下载,这段时长已经不记录在 onload 的时长中了。


其实这种行为也算合理,毕竟多媒体资源通常很大,占用的带宽也多,如果一直延迟 onload,意味着很多依赖 onload 的事件都无法及时触发。

接下来我们把这种情况再复杂一些,贴近实际的生产场景,通常video元素是包含封面图 poster 属性的,我们设置一张延迟1秒的封面图,看看会发生什么,结果如下:


不出意外,果然封面图影响了整体的加载时长,魔鬼都在细节中,封面图也需要注意优化压缩


场景八:异步脚本和样式资源一同请求
示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test</title>

    <!-- 请求时长为1秒的css -->
    <link rel="stylesheet" href="http://localhost:3010/index.css?delay=1000">

    <!-- 请求时长为0.5秒的js -->
    <script src="http://localhost:3010/index.js?delay=500" async></script>
</head>
<body>
</body>
</html>

浏览器表现如下:


可以看出 css 资源虽然没有阻塞脚本的加载,但是却延迟了整体页面加载时长,其中原因是css资源的加载会影响 render tree 的生成,导致页面迟迟不能完成渲染。
如果尝试把 async 换成 defer,或者干脆使用同步的方式加载脚本,结果也是一样,因结果相同,本处不再举例。


场景九:样式资源先请求,再执行内联脚本逻辑,最后加载异步脚本
我们把场景八的代码做一个改造,在样式标签和异步脚本标签之间,加上一个只包含空格的内联脚本,让我们看看会发生什么,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <script>
        console.log('页面js 开始执行')
    </script>

    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test</title>

    <!-- 请求时长为1秒的css -->
    <link rel="stylesheet" href="http://localhost:3010/index.css?delay=2000">

    <!-- 此标签仅有一个空格 -->
    <script> </script>

    <!-- 请求时长为0.5秒的js -->
    <script src="http://localhost:3010/index.js?delay=500" async></script>
</head>
<body>
</body>
</html>

index.js 中的内容如下:

console.log("脚本 js 开始执行");

结果如下,这是一张 GIF,加载可能有点慢:


这个结果非常有意思,他到底发生了什么呢?

  1. 脚本请求是0.5秒的延迟,样式请求是2秒
  2. 脚本资源是 async 的请求,异步发出,应该什么时候加载完什么时候执行
  3. 但是图中的结果却是等待样式资源加载完毕后才执行

答案就在那个仅有一个空格的脚本标签中,经反复测试,如果把标签换成注释,也会出现一样的现象,如果是一个完全空的标签,或者根本没有这个脚本标签,那下方的index.js 通过 async 异步加载,并不会违反直觉,加载完毕后直接执行了,所以这是为什么呢?

这其实是因为样式资源下方的 script 虽然仅有一个空格,但是被浏览器认为了它内部可能是包含逻辑,一定概率会存在样式的修改、更新 dom 结构等操作,因为样式资源没有加载完(被延迟了2秒),导致同步 js (只有一个空格的脚本)的执行被阻塞了,众所周知页面的渲染和运行是单线程的,既然前面已经有了一个未执行完成的 js,所以也导致了后面异步加载的 js 需要在队列中等待。这也就是为什么 async 虽然异步加载了,但是没有在加载后立即执行的原因。


场景十:字体资源的加载
示例代码:

<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test</title>
    <style>
        @font-face {
            font-family: font-custom;
            src: url('http://localhost:3010/font.ttf?delay=500');
        }

        body {
            font-family: font-custom;
        }
    </style>
</head>
<body></body>
</html>

结果如下:


可以看到,此情况下字体的加载是对 onload 有影响的,然后我们又测试了一下只声明字体、不使用的情况,也就是删除上面代码中 body 设置的字体,发现这种情况下,字体是不会发出请求的,仅仅是造成了代码的冗余。


四、总结

前面列举了大量的案例,接下来我们做个总结,实质性影响 onload 其实就是几个方面。

  1. 图片资源的影响毋庸置疑,无论是在页面中直接加载,还是通过 js 懒加载,只要加载过程是在 onload 之前,都会导致页面 onload 时长增加。
  2. 多媒体资源的等待时长会被记入 onload,但是实际加载过程不会。
  3. 字体资源的加载会影响 onload。
  4. 网络接口请求,不会影响 onload,但需要注意的是接口返回后,如果此时页面还未 onload,又进行了图片或者dom操作,是会导致 onload 延后的。
  5. 样式不会影响脚本的加载和解析,只会阻塞脚本的执行。
  6. 异步脚本请求不会影响页面解析,但是脚本的执行同样影响 onload。

五、优化举措

  1. 图片或其他资源的预加载可以通过 preload 或 prefetch 请求,这两种方式都不会影响 onload 时长。
  2. 一定注意压缩图片,页面中图片的加载速度可能对整体时长有决定性影响。
  3. 尽量不要做串行请求,没有依赖关系的情况下,推荐并行。
  4. 中文字体包非常大,可以使用 字蛛 压缩、或用图片代替。
  5. 静态资源上 cdn 很重要,压缩也很重要。
  6. 删除你认为可有可无的代码,没准哪一行代码就会影响加载速度,并且可能很难排查。
  7. 视频资源如果在首屏以外,不要开启预加载,合理使用视频的 preload 属性。
  8. async 和 defer 记得用,很好用。
  9. 非必要的内容,可以在 onload 之后执行,是时候重新拾起来这个 api 了。