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
用来获取当前节点的具体类型。该属性的值是一系列预定义的常量值。属性 nodeName
和 nodeValue
的值也与节点的具体类型相关。如对于元素节点来说,nodeName
的值是标签名称,nodeValue
的值是 null
;对于属性节点来说,nodeName
和 nodeValue
的值分别是属性的名称和值;对于文本节点来说,nodeName
的值是 #text
,nodeValue
的值是文本的内容。
在访问文档树的时候,一个常见的需求是访问当前节点的父节点、兄弟节点和子节点。节点接口中提供了相应的属性用来获取这些节点。
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
用来指定相对于参照元素的位置,可选的值有 before
、after
、replace
、only
、first
和 last
,分别表示在参照元素之前、之后、替换掉参照元素、替换掉参照元素的全部子节点、作为参照元素的第一个子元素,以及作为参照元素的最后一个子元素。也可以传入表示在参照元素的子节点中的序号位置。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})
用来同时设置属性 title
和 tabIndex
的值。传入三个参数用来设置单个属性的值,如 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
来声明;另外一种则是元素上的各种事件,如 onclick
、onmouseover
和 onmouseout
等。第一种是默认的触发条件。对应的动作一般有两种:一种是调用 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 公司立场、战略和观点。
回页首
下载
描述 |
名字 |
大小 |
下载方法 |
本文用到的 HTML 和 JavaScript 代码示例1 |
sample.zip |
4KB |
HTTP |
关于下载方法的信息
注意:
- 包含 dojo.query、dojo.NodeList 和 dojo.behavior 的示例代码。
参考资料
学习
讨论
关于作者
成富任职于 IBM 中国软件开发中心,目前在 Lotus 部门从事 IBM Mashup Center 的开发工作。他毕业于北京大学信息科学技术学院,获得计算机软件与理论专业硕士学位。他的个人网站是 http://www.。