内容


使用 Dojo 的 Ajax 应用开发进阶教程,第 4 部分

DOM 查询与操作

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 使用 Dojo 的 Ajax 应用开发进阶教程,第 4 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:使用 Dojo 的 Ajax 应用开发进阶教程,第 4 部分

敬请期待该系列的后续内容。

DOM 的全称是文档对象模型(Document Object Model)。它是 HTML 和 XML 文档的 API。它定义了文档的逻辑结构,以及对文档进行访问和操作的方式。通过 DOM,开发人员可以在文档中自由导航, 也可以添加、更新和删除其中的元素和内容。基本上文档中的任何内容都是可以通过 DOM 进行访问和操作的。本文详细介绍了如何使用 DOM 基本 API 和 Dojo 来进行 DOM 查询和操作。使用的 Dojo 版本是 1.4。下面首先介绍 DOM 的基本概念。

DOM 基本概念

DOM 是给脚本语言(如 JavaScript 和 VBScript 等)来使用的 API。在互联网的早期,HTML 页面都是静态的。开发人员没有办法对页面进行动态修改。DOM 的出现解决了这个问题。DOM 给出了一种描述 HTML 文档结构的方式,并且允许开发人员通过 DOM 提供的 API 来对文档结构进行修改。DOM 目前是 W3C 的推荐规范。主流的浏览器都实现或部分实现该规范。下面首先介绍 DOM 规范的版本历史。

DOM 规范的版本历史

DOM 从出现之后,经过了不断的发展变化,以及 W3C 组织的标准化工作,因此目前的版本比较多,具体如下所示:

  • DOM 级别 0:1996 年,Netscape 公司的 Netscape Navigator 2.0 浏览器中率先引入了 JavaScript 这一脚本语言。开发人员可以利用 JavaScript 来操作页面上的元素。此时的 DOM 称为 DOM 级别 0。它只支持对页面中的表单、链接和图像进行操作。
  • 中间 DOM:中间 DOM(Intermediate DOM)指的是 DOM 级别 0 和 DOM 级别 1 之间的一个中间版本。在这个版本中,可以通过 JavaScript 来改变页面的样式表。另外,页面上更多的元素可以通过 DOM 来进行操作。
  • DOM 级别 1:DOM 级别 1 是由 W3C 制定的 DOM 规范标准,在 1998 年发布。DOM 级别 1 的规范定义了访问和操作 HTML 页面中元素的基本方式。
  • DOM 级别 2:DOM 级别 2 在 DOM 级别 1 的基础上增加了 getElementById()方法、DOM 遍历和范围、名称空间和 CSS 的支持。
  • DOM 级别 3:DOM 级别 3 在 DOM 级别 2 的基础上增加了 adoptNode()textContent等方法和属性、文档保存和加载、文档验证和 XPath 等。

本文中将重点介绍 DOM 级别 1 和级别 2 的部分。这些部分的内容目前在不同浏览器之间的兼容性较好,而且也很常用。下面重点介绍 DOM 规范中的基本元素。

DOM 基本元素

对于 HTML 文档中的基本元素,DOM 都有一个抽象的接口与它对应。

  • 文档(Document):文档接口用来表示整个 HTML 文档。对文档中其它元素和内容的访问和操作,都是从这个接口出发的。
  • 文档片段(DocumentFragment):文档片段用来表示整个文档树中的一个部分。
  • 节点(Node):节点接口用来表示 HTML 文档树中的一个节点。这是一个抽象的接口,在文档树中具体存在的都是该接口的子类型,如元素、属性和文本节点等。
  • 节点列表(NodeList):节点列表表示的是节点的一个有序集合。它的作用类似于 Java 中的 java.util.List接口。可以通过节点在集合中的序号来获取集合中的某个节点。
  • 命名节点映射表(NamedNodeMap):命名节点映射表表示的是可以根据名称来进行存取的节点集合。它的作用类似于 Java 中的 java.util.Map接口。
  • 元素(Element):元素是节点的一种子类型,可以包含子节点和属性。
  • 属性(Attr):属性用来描述元素的特征。它并不是文档树的一部分。
  • 文本(Text):文本表示元素和属性的文本内容。
  • DOM 异常(DOMException):DOM 异常用来表示 DOM 操作无法执行时的错误情况。DOM 异常中定义了一系列的出错条件与错误代码。
  • DOM 实现(DOMImplementation):DOM 实现表示与 DOM 接口对应的具体实现。

这里需要注意的是节点列表中的节点是动态的,它反映的是最新的文档结构。比如通过 DOM API 获得了某个元素的子节点列表,如果其中的某个子节点被删除,此节点就不会出现在之前的节点列表中。

在介绍完 DOM 的基本概念之后,下面介绍如何使用 DOM 对当前文档树进行查询。

DOM 查询

通过 DOM 提供的 API 来对当前文档树进行查询,是操作文档的前提。由于文档树结构可能很复杂,查询到所需节点的操作有可能会比较繁琐。这里介绍两种方法来进行查询,一种是利用 DOM 规范中定义的基本 API,另外一种是使用 Dojo。下面先从基本 API 开始。

使用基本 API

使用 DOM 规范中提供的 API,就可以对文档进行查询,以及在文档中自由导航。下面给出一些常用的方法和属性。

首先介绍的是两个用来在文档树中快速查找元素的方法:getElementById()getElementsByTagName()

文档接口的 getElementById(elementId)方法是在 DOM 级别 2 中引入的。该方法的作用是在文档中查找标识符为 elementId的元素。如果有,则返回该元素;否则返回 null。对 HTML 文档来说,元素的标识符是通过属性 id来指定的。如 document.getElementById("mySpan")在当前文档中查找标识符为 mySpan的元素。

文档和元素接口的 getElementsByTagName(tagname)方法用来查找标签名为 tagname的子元素。该方法的返回结果是节点列表,其中子元素的排列顺序是树遍历时的先序顺序。通过指定 tagname的值为 *,可以匹配所有标签。如 document.getElementsByTagName("div")查找当前文档中所有的 div元素。

下面介绍在查找到单个节点之后,如何查找其相邻节点。

在文档树中,每个节点的具体类型不尽相同。在节点接口中定义了属性 nodeType用来获取当前节点的具体类型。该属性的值是一系列预定义的常量值。属性 nodeNamenodeValue的值也与节点的具体类型相关。如对于元素节点来说,nodeName的值是标签名称,nodeValue的值是 null;对于属性节点来说,nodeNamenodeValue的值分别是属性的名称和值;对于文本节点来说,nodeName的值是 #textnodeValue的值是文本的内容。

在访问文档树的时候,一个常见的需求是访问当前节点的父节点、兄弟节点和子节点。节点接口中提供了相应的属性用来获取这些节点。

  • parentNode:获取当前节点的父节点。除了文档、文档片段和属性之外的其它节点都可以拥有父节点。
  • childNodes:获取当前节点的子节点,是一个节点列表。
  • hasChildNodes():该方法用来判断当前节点是否有子节点。
  • firstChild:获取当前节点的第一个子节点。如果没有则返回 null
  • lastChild:获取当前节点的最后一个子节点。如果没有则返回 null
  • previousSibling:获取出现在当前节点正前面的兄弟节点。如果没有则返回 null
  • nextSibling:获取出现在当前节点正后面的兄弟节点。如果没有则返回 null

节点接口还提供了 attributes属性用来获取节点的属性。对于元素节点,返回的是一个命名节点映射表;对于其它类型的节点,返回的是 null。通过属性 ownerDocument可以获取节点所在的文档。

上面介绍的这些基本 API 是由浏览器来实现的。下面介绍 Dojo 提供的 dojo.query。

使用 dojo.query

使用上面提到的 DOM 规范定义的基本 API,可以完成对 HTML 文档的查询。不过基本 API 的主要问题在于所提供的方法粒度较细。即便是满足一些简单的查询需求,也需要相当多的代码量。比如查找某个 div元素下面所有的 span元素,就需要用到 getElementById()getElementsByTagName()两个方法。而对 DOM 进行查询又是十分常用的操作,因此开发人员需要更加方便的进行 DOM 查询的方法。Dojo 中提供了 dojo.query 库,用来方便的进行 DOM 查询。dojo.query 的基本用法是使用 CSS 3 的选择器语法来选择 HTML 文档中的节点。对于复杂的查询条件,可以用复杂的 CSS 选择器来描述。使用 dojo.query 可以极大的降低代码量。比如上面提到的例子,用 dojo.query 的话只需要一行代码就足够了:dojo.query("#myDiv span")。另外 dojo.query 使用的是 CSS 的选择器语法,这对于开发人员来说并不陌生。代码清单 1中给出了一些常用的 dojo.query 的用法。

清单 1. 常用的 dojo.query 用法
 dojo.query("#header > h1")   //ID 为 header 的元素的直接子节点中的 h3 元素
 dojo.query("span[title^='test']")  // 属性 title 以字符串 test 开头的 span 元素
 dojo.query("div[id$='widget']") // 属性 id 以字符串 widget 结尾的 div 元素
 dojo.query("input[name*='value']")  // 属性 name 包含子串 value 的 input 元素
 dojo.query("#myDiv, .error") // 组合查询,结果中包含 ID 为 myDiv 的元素和 CSS 类为 error 的元素
 dojo.query(".message.info") // 同时包含了 CSS 类 message 和 info 的元素,注意两个类之间不包含空格
 dojo.query("tr:nth-child(even)") // 出现在父节点的偶数位置的 tr 元素
 dojo.query("input[type=checkbox]:checked") // 所有选中状态的复选框
 dojo.query(".message:not(:nth-child(odd))") // 嵌套子查询,选中包含 CSS 类 message,
                                              //并且不出现在父节点的奇数位置的元素

dojo.query方法除了第一个必须的参数用来表示所用的选择器语法之外,还有一个可选的参数用来指定查询的范围,可以是一个 ID 或是元素。如果传入该参数,则查询结果中只包含该元素的子节点。默认的查询范围是整个文档树。如 dojo.query("span.info", "myDiv")只在 ID 为 myDiv的元素的子节点中查询包含 CSS 类 info的 span 元素。熟练使用 dojo.query 的前提条件是对 CSS 3 规范定义的选择器语法比较熟悉。关于 CSS 3 选择器语法的更多信息,请见 参考资料

dojo.query 的另外一个强大功能是可以对选择出来的节点进行统一处理。通过方法级联还可以写出非常简洁的代码。下面的章节中将会详细介绍 dojo.query 的这一能力。

在介绍完使用基本 API 和 dojo.query进行 DOM 查询之后,下面介绍如何进行 DOM 操作。

DOM 操作

在通过上面介绍的基本 API 或是 dojo.query 查询到所需的节点之后,下面就可以对这些节点进行操作了。查询是为操作服务的。对 DOM 的操作包括对节点的创建、插入、更新和删除操作。下面将具体介绍如何使用基本 API 和 Dojo 来完成 DOM 操作。

使用基本 API

创建新的节点的统一入口是定义在文档接口中的一系列方法。这些方法都以 create开头。常用的方法有 createElement(tagName)用来创建一个标签名为 tagName的元素;createTextNode(data)用来创建一个内容为 data的文本节点;createAttribute(name)用来创建一个名称为 name的属性节点;createDocumentFragment()用来创建一个文档片段。

创建出新的节点之后,就需要将其插入到当前文档树中。节点接口定义了两个方法用来完成插入的操作。

  • appendChild(newChild):把节点 newChild添加到当前节点的子节点列表中。
  • insertBefore(newChild, refChild):与 appendChild()类似的是都是把节点 newChild添加到当前节点的子节点列表中,不同的是可以通过参数 refChild来指定位置。节点 newChild出现在节点 refChild的正前面。

节点接口的 replaceChild(newChild, oldChild)方法用来将当前节点的子节点 oldChild替换成新的节点 newChild。方法 removeChild(oldChild)用来删除当前节点的子节点 oldChild

对于元素节点来说,可以对其属性进行操作。方法 setAttribute(name, value)用来设置名为 name的属性的值为 value。方法 removeAttribute(name)用来删除名为 name的属性。

如果一个节点已经在文档树中存在,通过上面提到的 appendChild()insertBefore()replaceChild()方法改变其在文档树中的位置的时候,该节点会首先被从文档树中删除,然后再被插入到新的位置中。在插入文档片段的时候,文档片段本身并不会被插入,只有其子节点被插入到文档树中。

使用 Dojo

Dojo 也提供了一系列的 API 用来执行 DOM 操作。下面介绍常用的方法。

dojo.place(node, refNode, position)方法用来插入节点到文档树中的指定位置。该方法的参数 node用来指定待插入元素的 ID 或引用;refNode用来指定插入元素时的参照元素;position用来指定相对于参照元素的位置,可选的值有 beforeafterreplaceonlyfirstlast,分别表示在参照元素之前、之后、替换掉参照元素、替换掉参照元素的全部子节点、作为参照元素的第一个子元素,以及作为参照元素的最后一个子元素。也可以传入表示在参照元素的子节点中的序号位置。last是默认值,其作用相当于之前介绍的 appendChild()方法。如果该方法的第一个参数是以“<”开头的字符串,则创建一个以该字符串为内容的文档片段并插入此片段。

Dojo 提供了 3 个与元素的属性相关的方法。dojo.attr(node, name, value)用来获取或设置元素的属性。该方法的参数 node用来指定元素的 ID 或是引用;name用来指定要获取或设置的属性的名称,也可以是一个包含“属性 / 值”名值对的 JSON 对象;value用来指定要设置的属性的值。传入两个参数可以是获取单个属性的值,也可以是设置一组属性的值。如 dojo.attr(node, "title")用来获取属性 title的值,dojo.attr(node, {"title" : "My Title", "tabIndex" : 1})用来同时设置属性 titletabIndex的值。传入三个参数用来设置单个属性的值,如 dojo.attr(node, "name", "username")用来设置属性 name的值。在设置属性的时候,可以传入方法作为参数用来绑定事件处理。dojo.hasAttr(node, name)用来判断元素是否有名为 name的属性。dojo.removeAttr(node, name)用来删除元素的名为 name的属性。

dojo.create(tag, attrs, refNode, pos)方法用来创建新元素,并且可以指定元素的属性和在文档树中的位置。该方法可以有 4 个参数,只有第一个表示标签名的参数 tag是必须的。第二个参数 attrs指定元素的属性,实现时使用 dojo.attr()方法。最后两个参数指定新创建的元素在文档树中的位置,实现时使用 dojo.place()方法。

前面在介绍 dojo.query 的时候提到可以对选择出来的节点进行处理,下面进行具体介绍。dojo.query()方法返回的结果是 dojo.NodeList对象。dojo.NodeList继承自 JavaScript 中的数组类型,并添加了很多实用的方法,可以很方便的对选择出来的节点集合进行操作。其中的很多方法的返回结果也是 dojo.NodeList对象。这样多个方法的调用就可以级联起来,使得代码更加简单。在这一点上,dojo.query 的用法与 jQuery 比较类似。具体的级联用法见 dojo.query 级联一节。

dojo.NodeList中包含了与数组元素处理、DOM 操作、CSS 样式处理和事件绑定相关的很多方法,下面具体介绍其中的实用方法,如下所示。

  • forEach()map()filter()slice()splice()indexOf()lastIndexOf()every()some():这些是对节点数组本身进行操作的方法。dojo.NodeList的这些方法与操作数组的对应方法的含义相同,只是操作的对象被隐式指定为当前的节点数组。
  • attr()removeAttr():这两个是用来操作元素属性的方法,可以为节点数组中每个元素设置属性值或删除属性值。如 dojo.query("a").attr("target", "_blank")查找页面中所有的 a元素,并把其属性 target的值设成 _blank
  • style()addClass()removeClass()toggleClass():这些方法用来设置节点数组中每个元素的样式和 CSS 类。如 dojo.query("p").style("fontSize", "1.2em")把页面上所有的 p元素的字体大小设成 1.2em
  • append()prepend()after()before():这四个方法为节点数组中的每个元素添加内容,只是新添加内容的位置不同,分别位于节点的最后一个子节点、第一个子节点、之后和之前。这四个方法的参数可以是 HTML 字符串、DOM 节点引用和 dojo.NodeList对象。如 dojo.query("p").after("<span>Hello</span>")在每个 p 元素之后添加一个新的 span 元素。
  • appendTo()prependTo()insertBefore()insertAfter():这四个方法与上面四个方法是分别对应的,不同的是其参数是一个 dojo.query 查询字符串,节点数组中的元素被添加到由该查询指定的节点的对应位置上。可以看成是上面四个方法的逆操作。如 dojo.query("span.message").appendTo("#main")把包含 CSS 类 message的 span 元素添加为 ID 为 main的元素的最后一个子节点。
  • wrap()wrapAll()wrapInner():这三个方法用来包装节点数组中的元素。wrap()wrapInner()都是对节点中的每个元素添加包装,不同的是前者包装的是元素本身,而后者包装的是元素的子节点。wrapAll()是包装的节点数组中的全部元素。代码清单 2中给出了这三个方法的用法。
  • children()parent()next()prev():这四个方法用来查询节点数组中元素的子节点、父节点、后面和前面的相邻节点。这些方法都接受一个查询条件作为参数来进一步过滤结果。如 dojo.query("#myDiv").chidren(".message")查询 ID 为 myDiv的元素的包含 CSS 类 message的子节点。
清单 2. wrap()、wrapAll() 和 wrapInner() 的用法
 // 原始的 HTML 文档片段
 <div id="myDiv"> 
    <span class="item"> 
        <span class="title">Item 1</span> 
    </span> 
    <span class="item"> 
        <span class="title">Item 2</span> 
    </span> 
 </div> 

 // 执行 dojo.query(".item").wrap("<div class='item-container'></div>") 之后的结果
 <div id="myDiv"> 
    <div class="item-container"> 
        <span class="item"> 
            <span class="title">Item 1</span> 
        </span> 
    </div> 
    <div class="item-container"> 
        <span class="item"> 
            <span class="title">Item 1</span> 
        </span> 
    </div> 
 </div> 

 // 执行 dojo.query(".item").wrapAll("<div class='items'></div>") 之后的结果
 <div id="myDiv"> 
    <div class="items"> 
        <span class="item"> 
            <span class="title">Item 1</span> 
        </span> 
        <span class="item"> 
            <span class="title">Item 1</span> 
        </span> 
    </div>   
 </div> 

 // 执行 dojo.query(".item").wrapInner("<div class='item-inner'></div>") 之后的结果
 <div id="myDiv"> 
    <span class="item"> 
        <div class="item-inner"> 
            <span class="title">Item 1</span> 
        </div> 
    </span> 
    <span class="item"> 
        <div class="item-inner"> 
            <span class="title">Item 1</span> 
        </div> 
    </span> 
 </div>

dojo.query 级联

dojo.query方法返回的是 dojo.NodeList对象,而 dojo.NodeList对象的绝大多数方法返回的也是 dojo.NodeList对象。这样的话,对 dojo.NodeList的多个方法可以级联起来,使得写出来的代码更加简洁。在使用级联的时候需要注意 dojo.NodeList中包含的节点的变化,以免在错误的节点上面进行操作。使用 end()方法可以取消上一次对 dojo.NodeList的操作所造成的节点数组的改变。代码清单 3给出了 dojo.query 级联的示例。

清单 3. dojo.query 级联示例
 // 原始的 HTML 片段
 <div> 
    <div class="item"> 
        <div>Item 1</div> 
        <div>Item 2</div> 
    </div> 
 </div> 

 //JavaScript 代码
 dojo.query(".item").children().addClass("subItem").end() 
    .parent().addClass("itemContainer"); 

 // 更新之后的 HTML 片段
 <div class="itemContainer"> 
    <div class="item"> 
        <div class="subItem">Item 1</div> 
        <div class="subItem">Item 2</div> 
    </div> 
 </div>

代码清单 3所示,dojo.query(".item")的节点数组中包含的是包含 CSS 类 item的元素,调用 children()之后,节点数组变为上述元素的子元素,即两个 div元素;addClass()对这两个 div元素进行操作;接下来的 end()方法则把节点数组还原成 children()被调用之前的状态;接下来的 parent()选择的是包含 CSS 类 item的元素的父元素,addClass()对此父元素进行操作。可以看到,通过 end()方法的使用可以在一条语句中执行非常复杂的操作。不过从代码的可读性来说,一条语句中最好不要包含多个 end()

在介绍完使用 DOM 基本 API 和 Dojo 进行 DOM 操作之后,下面介绍在 Ajax 应用中使用 DOM 的相关内容。

在 Ajax 应用中使用 DOM

DOM 查询和操作在 Ajax 应用中是非常基本的。通过 DOM 操作,可以动态的对页面进行局部修改。这种“局部刷新”的用户体验,也是 Ajax 应用相对于传统 Web 应用的重要优势之一。一般来说,对页面的局部修改由用户的操作来触发。用户通过鼠标和键盘触发相应的浏览器事件,在事件的响应方法中进行 DOM 查询和操作。另外一种可能的触发条件是浏览器中的定时器机制。一些局部修改可以完全在浏览器端来实现,而另外一些局部修改则需要服务器端的支持。一般来说,在 Ajax 应用中使用 DOM 有下面三种实现模式。

  • 服务器端返回数据,浏览器端使用 DOM 操作:在这种模式下,服务器端返回的只是数据本身,并不包含展示相关的内容。浏览器端通过 XMLHTTPRequest 请求获取到数据之后,通过 DOM 操作来生成所需的页面片段,并添加到当前页面中。
  • 服务器端返回 HTML 片段,浏览器端简单显示:在这种模式下,服务器端通过模板技术,如 JSP™、Apache Velocity、等生成 HTML 片段,返回给浏览器。浏览器只需要用获取的 HTML 片段更新当前页面即可。
  • 服务器端返回数据,浏览器端使用模板:在这种模式下,服务器端返回的只是数据。浏览器端不是通过 DOM 操作来生成 HTML 片段,而是通过模板来进行生成。

这三种模式的区别在于两点:服务器端返回数据还是展示,浏览器端使用 DOM 操作还是模板。对于第一点,服务器端返回数据的好处是传输量较小、和客户端的耦合较松散以及较容易支持除浏览器之外的其它客户端。返回数据的格式常见的有 XML 和 JSON。不足之处在于在浏览器端有比较多的逻辑来生成 HTML 片段。对于第二点,DOM 操作的好处是简单易用,使用起来比较直接。不足之处在于代码编写比较复杂和冗长。而使用模板的话,所生成的 HTML 片段的结构可以从模板中很直观的看到,修改起来比较方便。但是也增加了额外的复杂度。代码清单 4给出了服务器端返回数据,浏览器端使用 DOM 操作的示例。

清单 4. 服务器端返回数据,浏览器端使用 DOM 操作
 dojo.xhrGet({ 
    url : "/posts", 
    load : function(data) { 
        var container = dojo.byId("posts"); 
        for (var i = 0, n = data.length; i < n; i++) { 
            var post = data[i]; 
            var postNode = dojo.create("div", { 
                className : "post"
            }, container); 
            dojo.create("div", { 
                className : "title", 
                innerHTML : post.title 
            }, postNode); 
            dojo.create("div", { 
                className : "content", 
                innerHTML : post.content 
            }, postNode); 
        } 
    }, 
    error : function() { 
        dojo.html.set(dojo.byId("posts"), "获取文章出错。"); 
    } 
 });

代码清单 4所示,服务器端返回的是 JSON 格式的数据,在浏览器端使用 dojo.create()来执行 DOM 操作。代码清单 5给出了服务器端返回数据,在浏览器端使用模板技术进行 DOM 操作的示例。

清单 5. 服务器端返回数据,浏览器端使用模板
 var template = "<div class="post"><div class="title">${title}</div>" 
    + "<div class="content">${content}</div></div>"; 

 dojo.xhrGet({ 
    url : "/posts", 
    load : function(data) { 
        var container = dojo.byId("posts"); 
        for (var i = 0, n = data.length; i < n; i++) { 
            var node = dojo.create("div", { 
                innerHTML : dojo.string.substitute(template, data[i]); 
            }); 
            container.appendChild(node.firstChild); 
        } 
    }, 
    error : function() { 
        dojo.html.set(dojo.byId("posts"), "获取文章出错。"); 
    } 
 });

代码清单 5所示,template中包含的就是 HTML 模板,从服务器端获得数据之后,通过 dojo.string.substitute()把数据应用在模板上,从而得到所需的 HTML 片段内容。

在介绍与在 Ajax 应用中使用 DOM 相关的内容之后,下面介绍与 DOM 查询和操作相关的一些高级话题。

高级话题

下面讨论几个与 DOM 查询和操作相关的高级话题。首先从 DOM 操作的性能开始。

性能

在 Ajax 应用中,性能是一个很重要的问题。由于 DOM 操作 Ajax 应用中非常普遍,提升 DOM 操作的性能对于整体的性能有很大影响。下面介绍一些好的实践。

  • 使用文档片段:文档片段是一个轻量级的文档对象,可以用来包含其它节点。当文档片段被插入到文档树中的时候,其本身并不会被插入,而只有其子节点被插入。一个常见的提高 DOM 操作性能的做法是利用文档片段来插入新创建的节点。首先创建一个文档片段,再把新创建的节点插入到文档片段中,再把该文档片段插入到文档树中。这样做的好处是可以减少页面的重新排列(reflow)。每次对文档树的 DOM 操作都会导致页面重新排列,从而影响 Web 应用的性能。有两种情况下的 DOM 操作不会导致页面重新排列:一种是对不可见元素(CSS 样式 display的值是 none)的操作,另外一种是不在当前文档树中的元素。由于文档片段不在当前文档树中,对它的修改并不会造成页面的重新排列。
  • 使用 innerHTML:这种做法是通过字符串拼接来构造 HTML 文档,再通过设置元素的 innerHTML来修改其内容。使用 innerHTML比一般的 DOM 操作要快。
  • 使用 cloneNode():当需要创建多个结构相同的元素时,比较好的办法是首先创建出一个元素作为模板,然后用 cloneNode()方法复制出其它的元素。这样比逐个创建每个元素速度要快。需要注意的是,通过 cloneNode()复制出来的元素会丢失原来绑定在其上的事件处理方法,需要重新进行事件绑定。

代码清单 6中给出了使用文档片段和 cloneNode()来提高 DOM 操作性能的示例。

清单 6. 高效 DOM 操作示例
 var df = document.createDocumentFragment(); 
 for (var i = 0; i < 10; i++) { 
    dojo.create("div", { 
        innerHTML : "node " + i 
    }, df); 
 } 
 var node = dojo.byId("myDiv"); 
 for (var i = 0; i < 10; i++) { 
    node.appendChild(df.cloneNode(true)); 
 }

浏览器兼容性

由于 DOM 规范的版本较多,时间跨度长,不同浏览器对 DOM 规范的支持程度也不尽相同。目前来说,主流浏览器对 DOM 规范级别 1 的全部以及级别 2 的核心部分,都有着不错的支持。在 Ajax 应用中,应该尽可能的使用这部分 DOM API。使用 JavaScript 库也能减少兼容性问题。关于 DOM 的浏览器兼容性问题的细节,见 参考资料

dojo.NodeList 插件

前面提到 dojo.NodeList提供了很多方法用来对查询到的节点数组进行操作。开发人员可以通过扩展 dojo.NodeList的方式来提供更加丰富的功能。这种扩展方式类似于 jQuery 中的插件机制。下面通过开发一个插件来进行说明。该插件实现的功能是点击标题栏可以控制内容的展开和收缩。代码清单 7给出了示例插件的 HTML 和 JavaScript 代码。

清单 7. dojo.NodeList 插件示例
 //HTML 代码片段    
 <div> 
    <div class="toggler">Header 1</div> 
    <div>Body 1</div> 
 </div>    
    
 //JavaScript 代码    
 dojo.NodeList.prototype.toggler = function(options) { 
    var opts = dojo.mixin({}, dojo.NodeList.prototype.toggler.defaults, options); 
    var collapsedOnLoad = opts.collapsedOnLoad; 
    
    return this.forEach(function(node) { 
        dojo.connect(node, "onclick", function() { 
            var collapsed = dojo.attr(node, "collapsed") == "true"; 
            dojo.query(node).next().style("display", collapsed ? "" : "none"); 
            dojo.attr(node, "collapsed", (!collapsed).toString()); 
        }); 
            
        if (collapsedOnLoad) { 
            dojo.attr(node, "collapsed", "true"); 
            dojo.query(node).next().style("display", "none"); 
        } 
    }); 
 }; 
    
 dojo.NodeList.prototype.toggler.defaults = { 
    collapsedOnLoad : true 
 }; 
    
 dojo.addOnLoad(function() { 
    dojo.query(".toggler").toggler(); 
 });

代码清单 7中,首先为 dojo.NodeList添加新的方法 toggler()。该方法可以对节点数组中的每个节点添加行为,使得该节点可以控制其相邻的下一个节点是否显示。具体的做法是通过 dojo.connect进行事件的绑定,当点击该节点的时候,根据节点的自定义属性 collapsed的值来确定其下一个节点的 CSS 样式 display的值。在使用的时候,只需要通过 dojo.query()查询到所需的节点,再调用此方法即可。

dojo.behavior

dojo.behavior允许以声明的方式为页面上的特定元素添加行为。进行声明的时候,只需要说明元素所满足的模式,以及针对这些元素所应用的行为即可。声明模式的时候使用的是与 dojo.query相同的 CSS 3 选择器语法。对于每种模式,可以声明多种不同的行为。对于每种行为,需要声明其触发的条件,以及对应的动作。触发的条件一般有两种:一种是找到匹配模式的元素,用 found来声明;另外一种则是元素上的各种事件,如 onclickonmouseoveronmouseout等。第一种是默认的触发条件。对应的动作一般有两种:一种是调用 JavaScript 方法,另外一种是用 dojo.publish来发布某种主题的通知。在使用 dojo.behavior的时候,首先通过 dojo.behavior.add()来添加声明,再通过 dojo.behavior.apply()来应用这些声明。这些声明的应用是增量式的,同样的声明对于同样的节点不会重复应用。新添加的节点会应用当前所有的行为声明。在页面加载完成之后,dojo.behavior.apply()会被自动调用。

下面通过一个具体的实例来进行说明。页面中文本的截断是一个很常见的操作。当元素的大小不足以全部显示其文本的时候,文本的一部分会被截断。一种比较好的做法是在被截断的文本后面加上 ...来提醒用户。代码清单 8中给出了用 dojo.behavior实现文本截断的代码。

清单 8. dojo.behavior 示例
 //HTML 代码
 <span class="label" maxLength="5">This label is very long.</span>  
    
 //JavaScript 代码    
 dojo.behavior.add({ 
    ".label[maxLength]" : { 
        found :  function(node) { 
            var text = node.innerHTML, 
                maxLength = parseInt(dojo.attr(node, "maxLength")), 
                truncatedText = text.length > maxLength ? 
                    text.substring(0, maxLength) + "..." : text; 
            dojo.attr(node, "title", text); 
            node.innerHTML = truncatedText; 
        } 
    } 
 });

代码清单 8中的行为声明的含义是如果遇到包含 CSS 类 label和属性 maxLength的元素,需要检查其包含文本的长度是否超过属性 maxLength指定的长度。如果超过的话则进行截断。

总结

DOM 查询和操作在 Ajax 应用开发中十分常用。简洁高效的操作 DOM,是开发一个良好 Ajax 应用的基础。本文首先介绍了 DOM 的基本概念,接着介绍了如何分别利用 DOM 规范定义的基本 API 和 Dojo 来进行 DOM 查询和操作。最后讨论了 DOM 操作的性能、dojo.NodeList 插件和 dojo.behavior 等高级话题。通过这些内容的介绍,可以对 Ajax 应用中 DOM 查询和操作有更深入的了解。

声明

本人所发表的内容仅为个人观点,不代表 IBM 公司立场、战略和观点。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=517757
ArticleTitle=使用 Dojo 的 Ajax 应用开发进阶教程,第 4 部分: DOM 查询与操作
publish-date=09092010