DOM扩展

云海梦尘 2014-02-24

对DOM的两个主要的扩展是SelectorsAPI(选择API)和HTML5。这两个扩展都源自开发社区,而将某些常见做法及API标准化一直是众望所归。此外,还有一个不那么阴人瞩目的ElementTraversal(元素遍历)规范,为DOM添加了一些属性。虽然前述两个主要规范(特别是HTML5)已经涵盖了大量的DOM扩展,但专用扩展依然存在。

1、选择符API

SelectorsAPI是由W3C发起制定的一个标准,致力于让浏览器原生支持CSS查询。所有实现这一功能的JavaScript库都会写一个基础的CSS解析器,然后再使用已有的DOM方法查询文档并找到匹配的节点。尽管库开发人员在不知疲倦地改进这一过程的性能,但到头来都只能通过运行JavaScript代码来完成查询操作。而把这个功能变成原生API之后,解析和树查询操作可以在浏览器内部通过编译后的代码来完成,极大地改善了性能。

SelectorsAPILevel1的核心是两个方法:querySelector()和querySelectorAll()。在兼容的浏览器中,可以通过Document及Element类型的实例调用它们。目前已完全支持SelectorsAPILevel1的浏览器有IE8+/Firefox3.5+/Safari3.1+/Chrome/Opera10+。

1)querySelector()方法

querySelector()方法接受一个CSS选择符,反悔与该模式匹配的第一个元素,如果没有找到匹配的元素,返回null。

通过Document类型调用querySelector()方法时,会在文档元素的方位内查找匹配的元素。而通过Element类型调用querySelector()方法时,只会在该元素的范围内查找匹配的元素。

CSS选择符可以简单也可以复杂,视情况而定。如果传入了不被支持的选择符,querySlector()会抛出错误。

2)querySelectorAll()方法

querySelectorAll()方法接收的参数与querySelector()方法一样,都是一个CSS选择符,但返回的是所有匹配的元素而不仅仅是一个元素。这个方法返回的是一个NodeList的实例。

具体来说,返回的值实际上是带有所有属性和方法的NodeList,而其底层实现则类似于一组元素的快照,而非不断对文档进行搜索的动态查询。这样实现可以避免使用NodeList对象通常会引起的大多数性能的问题。

只要传给querySelectorAll()方法的CSS选择符有效,该方法都会返回一个NodeList对象,而不管找到多少匹配的元素。如果没有找到匹配的元素,NodeList就是空的。

与querySelector()类似,能够调用querySelectorAll()方法的类型包括Document、DocumentFragment和Element.要取得返回NodeList中的每一个元素,可以使用item()方法,也可以使用方括号语法。同样与querySelector()方法类似,如果传入了浏览器不支持的选择符或者选择符中有预付错误,querySelectorAll()会抛出错误。

3)matchesSelector()方法

SelectorsAPILevel2规范为Element类型新增了一个方法matchesSelector(),这个方法接受一个参数,即CSS选择符,如果调用元素与该选择符匹配,返回true;否则,返回false。

在取得某个元素引用的情况下,使用这个方法能够方便地检测它是否会被querySelector()或querySelectorAll()方法返回。

截至2011年年中,还没有浏览器支持matchesSelector()方法;不过,也有一些实验性的实现。IE9+通过msMatchesSelector()支持该方法,Firefox3.6+通过mozMatchesSelector()支持该方法,Safari5+和Chrome通过webkitMatchesSelector()支持该方法。因此,如果想使用这个方法,最好是编写一个包装函数。

function matchesSelector(element,selector){
    if(element.matchesSelector){
         return element.matchesSelector(selector);
    }else if(element.msMatchesSelector){
         return element.msMatchesSelector(selector);
    }else if(element.mozMatchesSelector){
         return element.mozMatchesSelector(selector);
    }else if(element.webkitMatchesSelector){
         return element.webkitMatchesSelector(selector);
    }else{
         throw new Error("Not supported");
    }
}

2、元素遍历

对于元素间的空格,IE9及之前版本不会返回文本节点,而其他所有浏览器都会返回文本节点。这样,就导致了在使用childNodes和firstChild等属性时的行为不一致。为了弥补这一差异,而同时又保持DOM规范不变,ElementTraversal规范新定义了一组属性。

ElementTraversalAPI为DOM元素添加了一下5个属性。

(1)childElementCount:返回子元素(不包括文本节点和注释)的个数。

(2)firstElementChild:指向第一个子元素;firstChild的元素版。

(3)lastElementChild:指向最后一个子元素;lastChild的元素版。

(4)previousElementSibling:指向前一个同辈元素;previousSibling的元素版。

(5)nextElementSibling:指向后一个同辈元素;nextSibling的元素版。

支持的浏览器为DOM元素添加了这些属性,利用这些元素不必担心空白文本节点,从而可以更方便地查找DOM元素了。支持ElementTraversal规范的浏览器有IE9+、Firefox3.5+、Safari4+、Chrome和Opera10+。

3、HTML5

HTML5规范则围绕如何使用新增标记定义了大量JavaScriptAPI。其中一些API与DOM重叠,定义了浏览器应该支持的DOM扩展。

1)与类相关的扩充

(1)getElementsByClassName()方法

可以通过document对象及所有HTML元素调用该方法。这个方法最早出现在JavaScript库中,是通过既有的DOM功能实现的,而原生的实现具有极大的性能优势。

getElementsByClassName()方法接收一个参数,即一个包含一或多个类名的字符串,返回带有制定类的所有元素的NodeList.传入多个类名时,类名的先后顺序不重要。

调用这个方法时,只有位于调用元素子树中的元素才会返回。在document对象上调用getElementsByClassName()始终会返回与类名匹配的所有元素,在元素上调用该方法就只会返回后代元素中匹配的元素。

使用这个方法可以更方便地为带有某些类的原价尿素添加事件处理程序,从而不必局限与使用ID或标签名。不过别忘了,因为返回的对象是NodeList,所以使用这个方法与使用getElementsByTagName()以及其他返回NodeList的DOM方法都具有同样的性能问题。

支持getElementsByClassName()方法的浏览器有IE9+、Firefox3+、Safari3.1+、Chrome和Opera9.5+。

(2)classList属性

在操作类名时,需要通过className属性添加、删除和替换类名。因为className中是一个字符串,所以即使只修改字符串一不烦,也必须每次都设置整个字符串的值。如:

<divclass="bduserdisabled">...</div>

这个<div>元素一共有三个类名。要从中删除一个类名,需要把这三个类名拆开,删除不想要的那个,然后再把其他类名拼成一个新字符串。请看虾米的例子。

//删除“user”类
//首先,取得类名字符串并拆分成数组
var className = div.className.split(/\s+/);
//找到要删除的类名
var pos = -1,i,len;
for(i=0,len=className.;ength;i<len;i++){
   if(className[i] == "user"){
      pos = i;
      break;
   }
}
//删除类名
className.splice(i,1);
//把剩下的类名拼成字符串并重新设置
div.className = className.join(" ");

为了从<div>元素的class属性中删除“user”,以上这些代码都是必需的。必须得通过类似的算法替换类名并确认元素中是否包含该类名。添加类名可以通过拼接字符串完成,但必须要通过检测确定不会多次添加相同的类名。很多JavaSctipt库都实现了这个方法,见简化这些操作。

HTML5新增了一种操作类名的方式,可以让操作更简单也更安全,那就是为所有元素添加classList属性。这个classList属性是新集合类型DOMTokenList的实例。与其他DOM集合类似,DOMTokenList有一个表示自己包含多少元素的length属性,而要取得每个元素可以使用item()方法,也可以使用方括号语法。此外,这个新类型还定义如下方法:

(1)add(value):将给定的字符串值添加到列表中。如果值已经不存在,就不添加了。

(2)contains(value):表示列表中是否存在给定的值,如果存在则返回,否则false。

(3)remove(value):从列表中删除给定的字符串。

(4)toggle(value):如果列表中已经存在给定的值,删除它;如果列表中没有给定的值,添加它。

这样,前面那么多行代码用下面这一行代码就可以代替了:

div.classList.remove("user");

以上代码能够确保其他类名不受此次修改的影响。其他方法也能极大减少类似基本操作的复杂性。

有了classList属性,除非需要全部删除所有类名,或者完全重写元素的class属性,否则也就用不到className属性了。

支持classList属性的浏览器有Firefox3.6+和Chrome。

2)焦点管理

HTML5也添加了辅助管理DOM焦点的功能。首先就是document.activeElement属性,这个属性始终会引用DOM中当前获得了焦点的元素。元素获得焦点的方式有页面加载、用户输入(通常是通过按Taba键)和在代码中调用focus()方法。

默认情况下,文档刚刚加载完成时,document.activeElement中保存的是document.body元素的引用。文档加载期间,document.activeElement的值为null。

另外就是新增了documemnt.hasFocus()方法,这个方法用于确定文档是否获得了焦点。

通过检测文档是否获得了焦点,可以知道用户是不是正在与页面交互。

查询文档获知哪个元素获得了焦点,以及确定文档是否获得了焦点,这两个功能最重要的用途是提高Web应用的无障碍性。无障碍Web应用的一个主要标志就是恰当的焦点管理。

实现了这两个属性的浏览器的包括IE4+、Firefox3+、Safari4+、Chrome和Opera8+.

3)HTMLDocument的变化

HTML5扩展了HTMLDocument,增加了新的功能。

(1)readyState属性

IE4最早为document对象引入了readyState属性。Document的readyState属性有两个可能的值:loading,正在加载文档;complete,已经加载完文档。

使用document.readyState的最恰当方式,就是通过它来实现一个指示文档已经加载完成的指示器。在这个属性得到广泛支持之前,要实现这样一个指示器,必须借助onload事件处理程序设置一个标签,表明文档已经加载完毕。

支持readyState属性的浏览器有IE4+、Firefox3.6+、Safari、Chrome和Opera9+.

(2)兼容模式

自从IE6开始区分渲染页面的模式是标准的还是混杂的,检测页面的兼容模式就称为浏览器的必要功能。IE为此给document添加了一个名为compatMode的属性,这个属性就是为了告诉开发人员浏览器采用了哪种渲染模式。在标准模式下,document.compatMode的值等于“CSS1Compat”,而在混杂模式下,document.compatMode的值等于“BackCompat”.

陆续实现这个属性的浏览器有Firefox、Safari3.1+、Opera和Chrome。

(3)head属性

作为对document.body引用文档的<body>元素的补充,HTML5新增了document.head属性,引用文档的<head>元素。要引用文档的<head>元素,可以结合使用这个属性和另一个后备方法。

var head = document.head || document.getElementsByTagName("head")[0];

如果可用,就使用document.head,否则仍然使用getElementsByTagName()方法。

实现document.head属性的浏览器包括Chrome和Safari5。

4)字符集属性

charset属性标识文档中实际使用的字符集,也可以用来指定新字符集。默认情况下,这个属性的值为“UTF-16”,但可以通过<meta>元素、响应头部或直接设置charset属性修改这个值。

另一个属性是defaultCharset,表示根据默认浏览器及操作系统的设置,当前文档默认的字符集应该是什么。如果文档没有使用默认的字符集,那么charset和defaultCharset属性的值可能会不一样。

通过这两个属性可以得到文档使用的字符编码的具体信息,也能对字符编码进行准确的控制。运行适当的情况下,可以保证用户正常查看页面或使用应用。

支持document.charset属性的浏览器有IE、Firefox、Safari、Opera和Chrome。支持document.defaultCharset属性的浏览器有IE、Safari、Chrome.

5)自定义数据属性

HTML5规定可以为元素添加非标准的属性,但要添加前缀data-,目的是为元素提供与渲染无关的信息,或者提供语义信息。这些属性可以任意添加,随便命名,只要以data-开头即可。

添加了自定义属性之后,可以通过元素的dataset属性来访问自定义属性的值。dataset属性的值是DOMStringMap的一个实例,也就是一个名值对儿的映射。在这个映射中,每个data-name形式的属性都会有一个对应的属性,只不过属性名没有data-前缀(如,自定义属性是data-myname,那映射中对应的属性就是myname)。

如果需要给元素添加一些不可见的数据以便进行其他处理,那就要用到自定义数据属性。在跟踪链接或混搭应用中,通过自定义数据属性能方便地知道点击来自页面中的哪个部分。

支持自定义数据属性的浏览器有Firefox6+和Chrome。

6)插入标记

使用插入标记的技术,直接插入HTML字符串不仅更简单,速度也更快。以下与插入标记相关的DOM扩展已经纳入了HTML5规范。

(1)innerHTML属性

在读模式下,innerHTML属性返回与调用元素的所有子节点(包括元素、注释和文本节点)对应的HTML标记。在写模式下,innerHTML会根据指定的值创建的DOM树,然后用这个DOM树完全替换调用元素原先的所有子节点。

但是,不同浏览器返回的文本格式会有所不同。IE和Opera会将所有标签转换为大写形式,而Safari、Chrome和Firefox则会原原本本地按照原先文档中(或指定这些标签时)的格式返回HTML,包括空格和缩进。

在写模式下,innerHTML的值会被解析为DOM子树,替换调用元素原来的所有子节点。因为它的值被认为是HTML,所以其中的所有标签都会按照浏览器处理HTML的标准方式转换为元素(转换结果因浏览器而异)。如果设置的值仅是文本而没有HTML标签,那么结果就是设置纯文本。

为innerHTML设置的包含HTML的字符串值与解析后innerHTML的值大不相同。如:

div.innerHTML = "Hello & welcome,<b>\"reader\"!</b>";

以上操作得到的结果如下:

<div id="content">Hello &amp; welcome, <b>&quot;reader&quot;!</b></div>

为innerHTML设置HTML字符串后,浏览器会将这个字符串解析为相应的DOM树。因此设置了innerHTML之后,再从中读取HTML字符串,会得到与设置时不一样的结果。原因在于返回的字符串是根据原始HTML字符串创建的DOM树经过序列化之后的结果。

使用innerHTML属性也有一些限制。比如,在大多数浏览器中,通过innerHTML插入<script>元素并不会执行其中的脚本。IE8及更早版本是唯一能在这种情况下执行脚本的浏览器,但必须满足一些条件。一是必须<script>元素指定defer属性,而是<script>元素必须位于“作用域的元素”(微软所谓)之后。<script>元素被认为是“无作用域的元素”,也就是在页面中看不到的元素,与<style>元素或注释类似。如果通过innerHTML插入的字符串开头就是一个“无作用域的元素”,那么IE会在解析这个字符串前先删除该元素。

在IE中最常用的方式是:

div.innerHTML = "<input type=\"hidden\"><script defer>alert('hi');<\/script>";

这种方式因为是用隐藏的<input>域而不影响页面布局,故是首选。

大多数浏览器都支持以直观的方式通过innerHTML插入<style>元素,但在IE8及更早版本中,<style>也是一个“没有作用域的元素”,因此也必须给它前置一个“有作用域的元素”。

并不是所用的元素都支持innerHTML属性。不支持innerHTML的元素有:<col>、<solgroup>、<frameset>、<head>、<html>、<style>、<table>、<tbody>、<thead>、<tfoot>、<tr>。此外,在IE8及更早版本中,<title>元素也没有innerHTML属性。

无论什么时候,只要使用innerHTML从外部插入HTML,都应该首先以可靠的方式处理HTML。IE8为此提供了window.toStaticHTML()方法,这个方法接收一个参数,即一个HTML字符串;返回一个经过乌海处理后的版本--从源HTML中删除所有脚本节点和事件处理程序属性。

(2)outerHTML属性

在读模式下,outerHTML返回调用它的元素及所有子节点的HTML标签。在写模式下,outerHTML会根据指定的HTML字符串创建新的DOM子树,然后用这个DOM子树完全替换调用元素。

使用outerHTML属性以下面这种方式设置值:

div.outerHTML="<p>Thisisaparagraph.</p>";

这行代码完成的操作与下面这些DOM脚本代码一样:

var p = document.createElement("p");
p.appendChild(document.createTextNode("This is a paragraph."));
div.parentNode.replaceChild(p,div);

结果,就是新创建的<p>元素会取代DOM树中的<div>元素。

支持outerHTML属性的浏览器有IE4+、Safari4+、Chrome和Opera8+。Firefox7及之前版本都不支持outerHTML属性。

(3)insertAdjacentHTML()方法

插入标记的最后一个新增方式是insertAdjacentHTML()方法。这个方法最早也是在IE中出现的,它接收两个参数:要插入的位置和要插入的HTML文本。第一个参数必须是下列值之一:

*"beforebegin",在当前元素之前插入一个紧邻的同辈元素;

*"afterbegin",在当前元素之下插入一个新的子元素或在第一个子元素之前再插入新的子元素;

*"beforeend",在当前元素之下插入一个新的子元素或在最后一个子元素之后再插入新的子元素;

*"afterend",在当前元素之后插入一个紧邻的同辈元素。

注意,这些值都必须是小写形式。第二个参数是一个HTML字符串,如果浏览器无法解析该字符串,就会抛出错误。

支持insertAdjacentHTML()方法的浏览器哟IE、Firefox8+、Safari、Opera和Chrome。

(4)内存与性能问题

使用以上方法替换子节点可能会导致浏览器的内存占用问题,尤其是在IE中,问题更加明显。在删除带有事件处理程序或引用了其他JavaScript对象子树时,就有可能导致内存占用问题。假设某个元素有一个事件处理程序(或者引用了一个JavaScript对象作为属性),在使用前述某个属性将该元素从文档树中删除后,元素与事件处理程序(或JavaScript)之间的绑定关系在内存中并没有一并删除。如果这种情况频繁出现,页面占用的内存数量就会明显增加。因此,在使用innerHTML、outerHTML属性和insertAdjacentHTML()方法时,最好先手工删除要被替换的元素的所有事件处理程序和JavaScript对象属性。

不过,使用这几个属性--特别是使用innerHTML,仍然还是可以为我们提供很多便利的。一般来说,在插入大量新HTML标记时,使用innerHTML属性与通过多次DOM操作先创建节点再指定它们之间的关系相比,效率要高得多。这是因为在设置innerHTML或outerHTML时,就会创建一个HTML解析器。这个解析器是在浏览器级别的代码基础上运行的,因此比执行JavaScript快得多。不可避免地,创建和销毁HTML解析器也会带来性能损失,所以最好能够将设置innerHTML或outerHTML的次数控制在合理的范围内。如:

for(var i=0,len=values.length;i<len;i++){
      ul.innerHTML += "<li>" + values[i] + "</li>";  //要避免这种频繁操作!
}

这种每次循环都设置一次innerHTML的做法效率很低。而且,每次循环还要从innerHTML中读取一次信息,就意味着每次循环要访问两次innerHTML。最好的做法是单独构建字符串,然后再一次性地将结果字符串赋值给innerHTML,像下面这样:

var itemHtml = "";
for(var i=0,len=values.length;i<len;i++){
    itemHtml += "<li>"+values[i]+"</li>";
}
ul.innerHTML = itemHtml;

这个例子的效率要高的多,因为它只对innerHTML执行了一次赋值操作。

7)scrollIntoView()方法

scrollIntoView()可以在所有HTML元素上调用,通过滚动浏览器窗口或某个容器元素,调用元素就可以出现在视口中。如果给这个方法传入true作为参数,或者不传入任何参数,那么窗口关东之后会让调用元素的顶部与视口顶部尽可能平齐。如果传入false作为参数,调用元素会尽可能全部出现在视口中。

当页面发生变化时,一般会用这个方法来吸引用户的注意力。实际上,为某个元素设置焦点也会导致浏览器滚动并显示出获得焦点的元素。

支持scrollIntoView()方法的浏览器有IE、Firefox、Safari和Opera。

相关推荐