首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

5.2 遍历DOM

可以把XML的DOM表达方式看作是一棵导航树。一切术语都跟家谱术语(父、子、兄弟)类似。而与家谱的不同之处在于,XML文档从一个独立的根节点(称作文档元素,document element)开始,它包含指向子节点的指针。每一个子节点都包含指针指向它的父节点、相邻节点和子节点。

DOM还使用了一些特殊的术语来描述XML树里的对象种类。DOM树中的每个对象都是一个节点(node),每个节点可以有一个类型(type),比如元素、文本或者文档。要进一步学习下去,我们必须了解DOM文档是如何表现和如何定位的。请参考以下简单的HTML片段,让我们来研究研究它的DOM结构是如何工作的:

<p><strong>Hello</strong> how are you doing?</p>

这个片段的每一部分被分解为独立的DOM节点指针,指向其亲戚(父、子、兄弟)。使用图谱来表示的话,它应该如图5-1所示:片断的每部分(圆角框描述元素,直角框描述文本节点)和它的引用。

图5-1 节点间的关系

每个独立的DOM节点都包含指向它的相关节点的一系列指针。你需要使用这些指针来学习如何遍历DOM。所有可用的指针如图5-2所示。DOM节点指针的各个属性,分别是指向另一个DOM元素的指针(如果不存在则为null)。

图5-2 使用指针遍历DOM树

只需使用不同的指针,就可以定位页面上的任何元素或者文本块。要理解它在实际环境中是如何工作的,最好的方式就是实践一下。代码清单5-1是一个简单的HTML页。

代码清单5-1 简单HTML页面,或者说是简单的XML文档

  <html>

  <head>

    <title>Introduction to the DOM</title>

  </head>

  <body>

    <h1>Introduction to the DOM</h1>

    <p class="test">There are a number of reasons why the

DOM is awesome, here are some:</p>

    <ul>

      <li id="everywhere">It can be found everywhere.</li>

      <li class="test">It's easy to use.</li>

      <li class="test">It can help you to find what you want, really quickly.</li>

    </ul>

  </body>

  </html>

在这个文档中,根元素是<html>元素。在JavaScript中访问这个根元素是轻而易举的:

document.documentElement

根节点与其他DOM节点一样,同样拥有所有用于定位的指针。使用这些指针你就可以浏览整个文档,定位到任何一个你要找的元素。比如,要获取<h1>元素,你可能会使用如下方法:

//不起作用

document.documentElement.firstChild.nextSibling.firstChild

这是我们遇到的第一个难题:DOM指针不仅可以指向元素,也可以指向文本节点。在这里,代码并不会真正指向<h1>元素,而是指向了<title>元素。为什么会发生这样的事?原因在于XML存在一个有争议的地方:空格。或许你已经注意到,在<html>和<head>元素之间有一个行结束符,它被认为是一个空格,这就意味着第一个节点是文本节点,而不是<head>元素。从中我们可以了解到以下3点:

l    当试图只使用指针来遍历DOM时,精细编写的HTML标记居然会产生混乱。

l    仅使用DOM指针来定位文档可能会过于烦琐和不切实际。

l    通常,你不需要直接存取文本节点,只存取包含文本节点的元素就行了。

这让我们遇到了难题:还有其他更好的方式来寻找文档中的节点吗?当然有!在你的工具箱中创建一些辅助函数,你就可以有效提升现有的方法,并且让DOM的定位更为方便。

5.2.1 处理DOM中的空格

回到我们的HTML文档例子。之前,你试图定位到一个<h1>元素去,但由于捣乱的文本节点使得困难重重。如果只是一个比较独立的元素还好说,但是要继续找<h1>元素的下一个元素呢?你仍然会遭遇这个麻烦的空格bug,使你必须用.nextSibling.nextSibling来跳过<h1>和<p>之间的行结束符,空格一个都不能少。代码清单5-2所示是一个用以处理空格bug的补救办法。这个特别的技术删除所有空格——当然只是DOM文档中的文本节点,让DOM的遍历更容易。这样做不仅不会对你的HTML渲染产生副作用,还会让定位DOM变得更容易。但是需要注意的是,这个函数的结果并不是持久的,它需要在HTML文档的每一次加载时都重新执行一遍。

代码清单5-2 XML文档空格bug的补救方案

function cleanWhitespace( element ) {

  // 如果不提供参数,则处理整个HTML文档

  element = element || document;

  // 使用第一个子节点作为开始指针

  var cur = element.firstChild;

  // 一直到没有子节点为止

  while ( cur != null ) {

    // 如果节点是文本节点,并且只包含空格

    if ( cur.nodeType == 3 && ! /\S/.test(cur.nodeValue) ) {

      // 删除这个文本节点

      element.removeChild( cur );

      // 否则,它就是一个元素

    } else if ( cur.nodeType == 1 ) {

      // 递归整个文档

      cleanWhitespace( cur );

    }

    cur = cur.nextSibling; // 遍历子节点

  }

}

假设你要在前面例子中使用这个函数来查找位于<h1>元素后的元素。那么代码应该类似这样:

cleanWhitespace();

// 查找H1元素

document.documentElement

.firstChild // 查找Head元素

.nextSibling // 查找<body>元素

.firstChild // 得到H1元素

.nextSibling // 得到相邻的段落(p)

该技术有优点也有缺点。最大的优点在于,你可以保证DOM文档的遍历在一定程度上的稳定性。但明显性能太差,想想必须遍历每个DOM元素和文本节点,目的只是为了找出包含空格的文本节点。假设你有一个包含大量内容的文档,它可能会严重降低网站的加载速度。此外,每次为文档注入新的HTML,你都需要重新扫描DOM中的新内容,确保没有增加新的有空格填充的文本节点。

此外此函数重要的方面是节点类型的使用。节点的类型可以由检查它的nodeType属性来确定。可能会出现好几种值,但你经常会碰到的是以下3个:

l    元素(nodeType=1):匹配XML文件中的大部分元素。比如,<li>、<a>、<p>和<body>元素都有一个值为1的nodeType。

l    文本(nodeType=3):匹配文档内的所有文本块。当使用previousSibling和nextSibling来遍历DOM结构时,你会经常碰到元素内和元素间的文本块。

l    文档(nodeType=9):匹配文档的根元素。比如,在HTML文档内,它是<html>元素。

此外,你可以用常量来表明不同的DOM节点类型(但只是对非IE浏览器有用)。与其去记住1、3或9,还不如直接直观地使用document.ELEMENT_NODE、document.TEXT_NODE或者document.DOCUMENT_NODE。因为经常清空DOM的空格会让人感到厌烦,所以你应该探索其他遍历DOM结构更有效的方法。

5.2.2 简单的DOM遍历

可以使用纯粹的DOM遍历规则(每个遍历方向都有指针)来开发一些更适合你的HTMLDOM文档遍历函数。大部分Web开发者在大多数情况下仅仅需要遍历DOM元素而非相邻的文本节点,该规则就是基于这样的事实而制定的。以下一系列的辅助函数可以帮助你,它们能够取代标准的previousSibling、nextSibling、firstChild、lastChild和parentNode。代码清单5-3展示的函数,返回的是当前元素的前一个元素,如果前一个元素不存在则是null,类似于元素的previousSibling属性。

代码清单5-3 查找相关元素的前一个兄弟元素的函数

function prev( elem ) {

  do {

    elem = elem.previousSibling;

  } while ( elem && elem.nodeType != 1 );

  return elem;

}

代码清单5-4展示的函数,返回的是当前元素的下一个元素,如果下一个元素不存在则是null,类似于元素的nextSibling属性。

代码清单5-4 查找相关元素的下一个兄弟元素的函数

function next( elem ) {

  do {

    elem = elem.nextSibling;

  } while ( elem && elem.nodeType != 1 );

  return elem;

}

代码清单5-5展示的函数,返回的是当前元素的第一个子元素,类似于firstChild元素属性。

代码清单5-5 查找元素第一个子元素的函数

function first( elem ) {

  elem = elem.firstChild;

  return elem && elem.nodeType != 1 ?

next ( elem ) : elem;

}

代码清单5-6展示的函数,返回的是当前元素的最后一个子元素,类似lastChild元素属性。

代码清单5-6 查找元素最后一个子元素的函数

function last( elem ) {

  elem = elem.lastChild;

  return elem && elem.nodeType != 1 ?

  prev ( elem ) : elem;

}

代码清单5-7展示的函数,返回当前元素的父元素,类似parentNode元素属性。你可以一次用一个数字来操纵多个父元素,例如parent(elem,2)就等同于parent(parent(elem))。

代码清单5-7 查找元素父元素的函数

function parent( elem, num ) {

num = num || 1;

for ( var i = 0; i < num; i++ )

if ( elem != null ) elem = elem.parentNode;

return elem;

}

使用这些新函数你就可以迅速遍历DOM文档了,而且不必再为元素间的文本操心。比如,需要查找<h1>元素的下一个元素,现在可以这么做了:

// 查找<h1>元素的下一个元素

next( first( document.body ) )

在这段代码内有两件事值得注意。首先,此处有一个新的引用:document.body。所有的现代浏览器都在HTML DOM文档内通过body属性提供一个对<body>元素的引用。你可以用它来简化你的代码。另一件事情你可能已经注意到,那就是这些函数的书写方式十分违反直觉。一般来说,在遍历时会想:“从<body>元素开始,获取第一个元素,然后获取下一个元素”。但从我们实际编写方式来看,它是按相反的顺序。要解决这个问题,我们将讨论一些可以让自定义的遍历代码更清晰的方式。

5.2.3 绑定到每一个HTML元素

Firefox和Opera中存在一个强大的对象原型(object prototype)叫HTMLElement,它允许你为每一个HTML DOM元素绑定函数和数据。上一节所描述的函数显得特别愚钝,但它们应该可以更清晰的。一个完美的方案是,为HTMLElement的原型直接绑定函数,由此每个独立的HTML DOM元素也直接绑定了你的函数。为了能让上一节描述的函数继续有效,你必须做出3个改变:

(1)  你需要在函数的顶部增加一行代码,使用this来引用元素,而不是从参数的变量中获取它。

(2) 你需要删除不再需要的元素参数。

(3)  你需要把函数绑定到HTMLElement原型,由此DOM中的各个HTML元素可以使用该函数。

新的next函数如代码清单5-8所示。

代码清单5-8 为所有HTML DOM元素动态绑定新的DOM遍历函数

HTMLElement.prototype.next = function() {

  var elem = this;

  do {

    elem = elem.nextSibling;

  } while ( elem && elem.nodeType != 1 );

  return elem;

};

现在你可以这样来使用新的next函数(和经过同样调整后的其他函数)了:

// 一个简单的例子 —— 获取<p>元素

document.body.first().next()

这让你的代码更加清晰和易于理解。现在你可以按照自然想法的顺序来编写代码了,JavaScript总体上也更清晰了。如果你对这种编程风格感兴趣,我特别推荐尝试一下jQuery JavaScript库(http://jquery.com),它大量使用到这种技术。

注意 因为只有3个现代浏览器(Firefox、Safari和Opera)支持HTMLElement,要让它也能在IE中工作,必须给予特别的处理。有一个可以直接使用的库,由Jason Karl Davis(http://browser-land.org)编写,它为两个不支持的浏览器提供了访问HTMLElement的方法。可以从这找到该库的具体信息:http://www.browserland.org/scripts/htmlelement/.

5.2.4 标准的DOM方法

所有的现代DOM实现都包含一系列让编程更轻松的方法。结合一些自定义函数,遍历DOM可以是一种平滑的体验。那么让我们从JavaScript DOM的两个强大方法开始:

l    getElementById("everywhere"):该方法只能运行在document对象下,从所有中找出ID是everywhere的元素。这是一个非常强大的函数,并且是迅速的访问一个元素的最快方法。

l    getElementsByTagName("li"):该方法能运行在任何元素下,找出该元素下的所有标签名为li的后代元素,并返回一个NodeList(节点列表)。

警告 getElementById本来会按照你的预期工作:查找所有元素,揪出一个包含id的属性,且值为指定值的元素。但是,如果你加载一个远程XML文档,并使用getElementById(或是使用除了JavaScript之外的实现DOM的语言),它默认并不会使用id属性。这是由于设计上的问题,一个XML文档如需明确指出id属性是什么,通常使用XML的定义或配置。

警告 getElementsByTagName返回一个节点NodeList.该结构跟普通的JavaScript数组非常相似,但一个重要的不同之处在于:它并不支持.push()、.pop()、.shift()等JavaScript数组的常用方法。使用getElementsByTagName时请务必记住这点,这能把你从很多混乱中解救出来。

这两个方法在所有现代的浏览器都可用,对定位到指定元素有非常大的帮助。回头看看我们前面尝试查找<h1>元素的例子,现在可以这么做了:

document.getElementsByTagName("h1")[0]

这行代码保证有效并返回文档中的第一个<h1>元素。再回头看看例子文档,假设你要获取所有<li>元素并为之加上边框。

var li = document.getElementsByTagName("li");

for ( var j = 0; j < li.length; j++ ) {

  li[j].style.border = "1px solid #000";

}

最后,需要使第一个<li>元素的文本加粗,假设它已经有了一个关联它的id:

document.getElementById("everywhere").style.fontWeight = 'bold';

你可能已经注意到获取指定ID的单个元素需要冗长的代码,依靠标签名获取元素也如此。其实你可以封装一个函数来简化获取的过程:

function id(name) {

  return document.getElementById(name);

}

代码清单5-9展示了一个函数,它依靠HTML DOM内的文档标签来定位元素。该函数带一个或两个参数。如果只传入一个标签名作为参数,函数将会遍历整个文档。另外你可以传入一个DOM上下文元素作为第一个参数的辅助,则函数只遍历该参数元素下的标签。

代码清单5-9 依靠HTML DOM文档标签定位元素的函数

function tag(name, elem) {

  // 如果不提供上下文元素,则遍历整个文档

  return (elem || document).getElementsByTagName(name);

}

让我们重新回到寻找<h1>元素后的第一个元素的问题。十分幸运的是,现在的代码可以比之前任何一次的都要精炼:

// 查找<h1>元素后的第一个元素

next( tag("h1")[0] );

以上这些函数为你在DOM文档中获取元素提供了强大而迅速的解决方法。在进一步学习修改DOM的方法之前,首先需要大致了解脚本首次执行时DOM的加载问题。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论