DOM2 和 DOM3

2019-08-02  本文已影响0人  了凡和纤风

本章内容:DOM2 和 DOM3 的变化、操作样式和 DOMAPI、DOM遍历与范围

DOM1 级主要定义的是HTML 和 XML 文档的底层结构。DOM2 和 DOM3 级则在这个结构的基础上引入了更多的交互功能,也支持了更高级的XML特性。
DOM2 和 DOM3 的模块

一、DOM变化

DOM2级和3级的目的在于扩展DOM API,满足操作文档的需求,同时提供更好的错误处理及特性检测能力。

为了确定浏览器是否支持这些DOM模块,可以使用 hasFeature() 来检测它们(这个方法在上一章提到过。)

var supportsDOM2Core = document.implementation.hasFeature('Core', '2.0')
var supportsDOM3Core = document.implementation.hasFeature('Core', '3.0')
var supportsDOM2HTML = document.implementation.hasFeature('HTML', '2.0')
var supportsDOM2Views = document.implementation.hasFeature('Views', '2.0')
var supportsDOM2XML = document.implementation.hasFeature('XML', '2.0')

1.1、针对XML 命名空间的变化

有了XML命名空间,不同XML文档的元素就可以混合在一起,共同构成格式良好的文档。HTML不支持XML命名空间,但XHTML支持XML命名空间。以下给出实例中,皆为XHTML文档格式

命名空间要使用xmlns特性来制定,并应将其包含在<html>元素中。

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Example XHTML page</title>
  </head>
  <body>
    Hello World
  </body>
</html>

在这个例子中,所有的元素都默认被视为XHML 命名空间的元素。要明确地为XML命名空间创建前缀,可以使用xmlns 后跟冒号,再跟前缀。

<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <xhtml:head>
    <xhtml:itle>EX XHTML Page</xhtml:title>
  </xhtml:head>
  <xhtml:body>
    Hello World
  </xhtml:body>
</xhtml:html>

有时候为了避免不同语言间的冲突,也需要使用命名空间来限定特性。

<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <xhtml:head>
    <xhtml:title>EX XHTML Page</xhtml:head>
  </xhtml:head>
  <xhtml:body xhtml:class="home">
    Hello World
  </xhtml:body>
</xhtml:html>

在只基于一种语言编写XML文档的情况下,命名空间实际上也没有什么用。不过,在混合使用两种语言的情况下,命名空间的用处就非常大了。
比如下面这个混合了 XHML 和 SVG 语言的文档。

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Ex XHTML Page</title>
  </head>
  <body>
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1"
         viewBox="0 0 100 100" style="width: 100%;height: 100%">
      <react x="0" y="0" width="100" height="100" style="fill:red" />
    </svg>
  </body>
</html>

通过设置命名空间,将<svg>标识为了与包含文档无关的元素。<svg> 所有子元素以及所有的特性,都被认为是 http://www.w3.org/2000/svg 命名空间

1.1.1、Node类型的变化

在 DOM2 级中, Node 类型包含下列特定命名空间的属性。

当节点使用命名空间前缀时,其nodeName等于 prefix+ ":" + localName。

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Ex XHTML Page</title>
  </head>
  <body>
    <s:svg xmlns:s="http://www.w3.org/200/svg" version="1.1"
           viewBox="0 0 100 100" style="width: 100%; height: 100%">
      <s:react x="0" y="0" width="100" height="100" style="fill:red" />
    </s:svg>
  </body>
</html>

以上面这段代码为例。

DOM3 级在此基础上更进一步,又引入了下列与命名空间相关的方法。

针对前面的 代码调用 以上API

console.log(document.body.isDefaultNamespace('http://www.w3.org/1999/xhtml')) // true

var svg = document.getElementsByTagName('s:svg')[0] // 获取 svg的 引用
console.log(svg.lookupPrefix('http://www.w3.org/2000/svg')) // s
console.log(svg.lookupNamespaceURI('s')) // http://www.w3.org/2000/svg
1.1.2、Document 类型的变化

DOM2 级中的 Document 类型 包含下列与命名空间相关的方法:

// 创建一个 新的 SVG 元素
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')

// 创建一个属于 某个命名空间 的新特性。
var att = document.createAttributeNS('http://www.somewhere.com', 'radom')

// 取得所有 XHTML 元素
var eles = document.getElementsByTagNameNS('http://www.w3.org/1999/xhml', '*')

只有在文档中存在两个或多个命名空间时,这些与命名空间有关的方法才是必须的。

1.1.3、Element类型

DOM2级核心 中有关Element的变化,主要涉及操作特性。新增的方法如下。

1.1.4、NamedNodeMap 类型的变化

NameNodeMap 类型也新增了下列与命名空间有关的方法。由于特性是通过 NameNodeMap 表示的,因此这些方法多少情况下只针对特性使用。

由于一般都是通过 元素 访问特性,所以这些方法很少使用。

1.2、其他方面的变化

这些变化与 XML 命名空间无关,而是更倾向于确保 API 的可靠性及完整性。

1.2.1、DocumentType 类型的变化

DoucmentType 类型新增了 3个属性:publicIdststemIdinternalSubset。前两个属性表示的是文档类型声明中的两个信息段。

<!DOCTYPE HTML PUBLIC "-//W3C/DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
console.log(document.doctype.publicId) // -//W3C/DTD HTML 4.01//EN
console.log(document.doctype.systemId) // http://www.w3.org/TR/html4/strict.dtd

internalSubset 用于访问包含在文档类型中的额外定义。在HTML中极少用到,在XML中常用一些。

1.2.2、Document 类型的变化

Document类型变化中唯一于 命名空间无关的方法是 importNode()这个方法的用途是从一个文档中取得一个节点,然后将其导入到另一个文档,使其成为这个这个文档的一部分。需要注意的是,每个节点都有一个 ownerDocument 属性,表示所属文档。如果调 appendChild() 是传入的节点属于 不同文档(ownerDocument的值不一样),则会导致错误。在调用 importNode()时传入不同文档的节点则会返回一个新节点,这个新节点的所有权归当前文档所有。
importNode() 方法和 cloneNode() 方法非常相似,接受两个参数:要复制的节点、是否复制子节点,返回原来节点的副本。

var newNode = document.importNode(oldNode, true) // 深复制,复制子节点
document.body.appendChild(newNode)

同样这个方法在HTML中并不常用,在XML中使用较多。


“DOM2级视图” 模块添加了一个 名为 defaultView 的属性,其中保存着一个指针,指向拥有给定文档窗口(或框架)。IE不支持此属性,IE中 的等价属性时 prentWIndow(Opera也支持这个属性)。
确定文档的归属窗口

var parentWindow = document.defaultView || document.parentWindow

"DOM2级核心" 还为 docunebt.implementation 对象规定了两个新方法:

var doctype = document.implementation.createDocumentType('html', '-//W3C/DTD HTML 4.01//EN', 'http://www.w3.org/TR/html4/strict.dtd')

由于既有文档类型不能改变,因此这个方法只在创建新文档时有用

// 创建一个新的XML文档
var xmlDoc = document.implementation.createDocument('', 'root', null)

// 创建一个XHML 文档
// 这里的 doctype 引用 上面例子中 通过 createDocumentType创建的
var xhtmlDoc = document.implementation.createDocument('http://www.w3.org/1999//xhtml', 'html', doctype)

"DOM2级HTML" 模块 页为 document.implementation 新增了一个方法,createHTMLDocument(),改方法创建一个完整的 HTML 文档。接受一个参数:即新创建文档的标题(<title>中间的文本)。

var htmlDoc = document.implementation.createHTMLDocument('New Doc')

通过 createHTMLDocument() 创建的这个文档,是HTMLDocument类型的实例,具有该类型的所有属性和方法包括 title 和 body 属性。 这个方法存在兼容问题

1.2.3、Node类型的变化

添加了 isSupported() 方法。与 DOM1级的 hasFeature() 方法类似。isSupported() 方法用于确定当前节点具有什么能力。接受两个参数:特性名 和 特性版本号;返回一个布尔值。

if (document.body.isSupported('HTML', '2.0')) {
  // todo
}

与 hasFeature() 类似,建议在 确定某个特性是否可用时,最好还是使用能力检测


DOM3级 引入了 两个辅助比较节点的方法:isSameNode() 和 isEqualNode()。这两个方法都接受一个节点参数,

var div1 = document.createElement('div')
div.setAttribute('class', 'box')

var div2 = document.createElement('div')
dvi2.setAttribute('class', 'box')


console.log(div1.isSameNode(div1)) // true
console.log(div1.isEqualNode(div2)) // true
// 引用的不是同一个对象,不相同
console.log(div1.isSameNode(div2)) // false
1.2.4、框架的变化

DOM2级中有一个新属性:contentDocument,这个属性包含一个指针,指向表示框架内容的文档对象。IE8之前不支持 这个属性。但支持一个 contentWindow 的属性,该属性返回框架的 window 对象,而这个window 对象又有一个 document 属性。

// 访问内嵌框架的文档对象
var iframe = document.getElementById('myIframe')
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document

所有浏览器都支持 contentWindow属性

二、样式

"DOM2级样式"围绕样式机制提供了一套API。可以通过 hasFeature() 检测是否支持。

var supportsDOM2CSS = document.implementation.hasFeature('CSS', '2.0')
var supportsDOM2CSS2 = document.implementation.hasFeature('CSS2', '2.0')

2.1、访问元素的样式

任何支持style 特性的 HTML 元素在 JavaScript中都有对应的 style 属性。这个 style 对象是 CSSStyleDeclaration 的实例。包含HTML的style 特性指定的所有样式信息。
css 中的 '-' 在JavaScript中要变成 驼峰式,比如:background-color = backgroundColor

var myDiv = document.getElementById('div')

// 设置背景颜色
myDive.style.backgroundColor = 'cyan'

// 改变大小
myDiv.style.width = '200px'

// 指定边框
myDiv.style.border = '1px solid black'

同时通过 style 对象也可以获取 style特性中指定的 样式

// 获取背景色
console.log(myDiv.style.backgroundColor)
2.1.1、DOM样式属性和方法

"DOM2级样式"规范还未 style 对象定义了一些属性和方法。

通过 cssText属性可以访问style 特性中的代码。并且如果通过 cssText 修改 特性,会重写整个 style 特性的值。

// 设置
myDiv.style.cssText = 'width: 24px; height: 30px'
// 获取
console.log(myDive.style.cssText)

length属性 与 item()方法配套使用,以便在迭代元素中定义的css属性,在使用length 和 item() 时,style 对象实际上就相当于一个集合,可以使用方括号语法来代替item()

for (var i = 0, len = myDiv.style.length; i < len; i++) {
  console.log(myDiv.style[i]) // myDiv.style.item(i)
}

通过如上遍历可以取得所有的特性名,然后通过 getPropertyValue() 获取获取每一个特性值

var prop, value, i, len
for(i = 0, len = myDiv.length; i < len; i++) {
  prop = myDiv.style.item(i)
  value = myDiv.style.getPropertyValue(prop)
  console.log(prop + ": " + value )
}

getPropertyValue() 返回的始终是CSS属性值的字符串表示。
使用getPropertyCSSValue()方法,返回一个对象。这个对象又两个属性:

var i, prop, value, len
for(i = 0, len = myDiv.length; i < len; i++) {
  prop = myDiv.style.item(i)
  value = myDiv.style.getPropertyCSSValue(prop) // 获取 CSSValue 对象
  console.log(prop + ': ' + value.cssText + '(' + value.cssValueType + ')') 
}

要从元素的样式中移除某个 CSS 属性,需要使用 removePropertyValue() 方法。这个方法移除一个属性

myDiv.style.removeProperty('border')
2.1.2、计算的样式

"DOM2级样式"增强了 document.defaultView,提供了 getComputedStyle() 方法。这个方法接受两个参数:要取得计算样式的元素、一个伪元素字符串(例如 :after)如果不需要伪元素可以设置为 null。 返回一个 CSSStyleDeclaration 对象。

<html>
  <head>
    <title>EX</title>
    <style>
      #div {
        color: pink;
        width: 20px;
        height: 30px;
      }
    </style>
  </head>
  <body>
    <div id="div" style="color: pink">
  </body>
</html>
var myDiv = document.getElementById('div')
var computedStyle = document.defaulltView.getComputedStyle(myDiv, null)
console.log(computedStyle .color) // rgb(255, 192, 203)
console.log(computedStyle .width) // 20px
console.log(computedStyle .height) // 30px

IE不支持 getComputedStyle() 方法。 在IE中,每个具有 style 属性的元素还有一个 currentStyle 属性。这个属性是CSSStyleDeclaration的实例,包含当前元素全部计算后的样式。

var computedStyle = myDiv.currentStyle

console.log(computedStyle.width) // 20px

在所有浏览器中,所有计算后的样式都是只读的;不能修改计算和样式对象中的css属性。

2.2、操作样式表

CSSStyleSheet类型表示的是样式表,是一套只读的接口,CSSStyleSheet 继承自 StyleSheet,从 StyleSheet接口继承而来的属性如下。

除了disable 属性之外,其他属性都是只读的。在支持以上所有这些属性的基础上,CSSStyleSheet还支持下列的属性和方法

应用于文档的所有样式表是通过 document.styleSheets 集合来表示的`

var sheet = null
for (var i = 0, len=document.styleSheets.length; i < len; i++){ // 遍历 样式表
  sheet = document.styleSheets[i]
  console.log(sheet.href) // 访问 href 属性
}

不同浏览器的 document.styleSheets 返回的样式表也不同。所有浏览器都会包含<style>元素 和 rel 特性被设置为 ”stylesheet“的<link>元素引入的样式表。IE 和 Opera也包含 rel 被设置为 "alternate stylesheet"的<link>元素的引入的样式表。

也可以直接通过<link> 或 <style> 元素取得 CSSStyleSheet。DOM规定了一个包含 CSSStyleSheet 对象的属性,名叫 sheet;IE支持的是 styleSheet 属性。

// 在不同浏览器中取得 样式对象
function getStyleSheet(element) {
  return element.sheet || element.styleSheet
}

// 取得第一个 <link> 元素引入的样式表
var link = document.getElementsByTagNames('link').item(0)
var sheet = getStyleSheet(link)

这里的 getStyleShee() 返回的样式表对象 于 document.styleSheets 集合中样式表对象相同。

2.2.1、CSS规则

CSSRule对象表示样式表中的每一条规则。实际上,CSSRule 是一个供其他多种类型继承的基类型,其中最常见的就是 CSSStyleSheet类型,表示样式信息。CSSStyleRule对象包含下列属性。

其中最常用的是cssText、selectorText、style
cssText 属性与 style.cssText 属性类似,但并不相同。前者包含选择符文本和围绕样式信息的花括号,后者只包含样式信息(类似于元素的 style.cssText)。此外,cssText是只读的,style.cssText 也可以被重写。

大多数情况下,使用style属性就可以满足所有操作样式规则的需求了。这个对象就像每个元素上的style属性一样,可以通过它读取和修改规则中的样式信息。

div.box {
  background-color: blue;
  width: 100px;
  height: 200px
}

假设上面这条规则位于页面中的第一个样式表中

var sheet = document.styleSheets[0]
var rules = sheet.cssRules || sheet.rules // 取得规则列表
var rule = rules[0] // 取得第一条 规则

console.log(rule.selectorText) // div.box
console.log(rule.style.cssText) // 完整的CSS代码
console.log(rule.style.backgroundColor) // blue || rgb(..)

也可以像下面这样来修改信息

var sheet = document.styleSheets[0]
var rules = sheet.cssRules || sheet.rules
var rule = rules[0]
rule.style.backgroundColor = 'red'

需要注意的是,以这种方法修改会影响页面中适用该规则的所有元素

2.2.2、创建规则

inertRule()
要向现有样式表中 添加新规则,需要使用 inertRule()方法,这个方法接受两个参数;规则文本、表示在哪里插入的规则的索引:

sheet.insertRule('body { background-color: silver}', 0)

插入的规则将成为样式表中的第一条规则——规则的次序在确定层叠之后应用到文档的规则时至关重要。Firefox、Safari、Opera 和 Chrome 都支持 这个方法。
IE8及更早版本中,类似的支持 addRule() 方法;

sheet.addRule('body', 'background-color: silver', 0) // 仅对IE有效

IE中说明,关于这个方法最多可以使用添加 4095条样式规则。超出这个上限的调用将会导致错误。

要跨越浏览器项样式表中插入规则,可以使用下面的函数。接收四个参数:要向其中添加规则的样式表、选择符文本、css样式信息、插入位置

function insertRule(sheet, selectorText, cssText, position) {
  if (sheet.insertRule) {
    sheet.insertRule(selectorText + '{' + cssText + '}', position)
  } else if (sheet.addRUle) { // IE
    sheet.addRule(selectorText, cssText, position)
  }
 }

insertRule(document.styleSheets[0], 'body', 'background-color: cyan', 0)

如果要添加较多的规则,这样的方法就会比较繁琐。因此,在需要大量添加规则的时候,推荐使用前面提到的 动态加载样式表的技术。

2.2.3、删除规则

从样式表中删除规则
deleteRule() 接受一个参数:要删除的规则的位置
IE支持removeRule()接受一个参数:要删除的位置。

function deleteRule(sheet, index) {
  if (sheet.deleteRule) {
    sheet.deleteRule(index)
  } else if(sheet.removeRule) { // IE
    sheet.removeRule(index)
  }
}

deleteRule(document.styleSheets[0], 0)

与添加规则相似,删除规则也不是实际Web开发中常见的做法。

2.3、元素大小

最初DOM中没有规定如何确定页面中元素的大小。IE为此率先引入了一些属性,以便开发人员使用。目前所有主流浏览器都已经支持这些属性。

2.3.1、偏移量

元素的可见大小由其高度、宽度决定,包括所有内边距、滚动条和边框大小(注意,不包括外边距)。通过下列4给属性可以取得元素的偏移量

其中,offsetLeft 和 offsetTop属性与包含元素有关,包含元素的引用保存在 offsetParent 属性中。offsetParent 和 parentNode的值不一样相等(父元素,不一定就是包含引用指向的元素)

偏移量图.png

要想知道某个元素在页面上的偏移量,将这个元素的offsetLeft 和 offsetTop 与其 offsetParent的相同属性相加,如此循环直至根元素就可以得到一个基本准确的值了。

// 到屏幕左边的偏移值
function getElementLeft(element) {
  var actualLeft = element.offsetLeft
  var current = element.offsetParent

  while(current !== null) {
    actualLeft += current.offsetLeft
    current = current.offsetParent
  }

return actualLeft
}

// 到屏幕顶部的偏移值
function getElementTop(element) {
  if (element.offsetParent) {
    return arguments.callee(current) +  element.offsetTop
  }
  return  element.offsetTop
}

对于简单的CSS布局的页面,这两个函数可以得到非常精确的结构。

2.3.2、客户区大小

元素的客户区大小,指的是元素内容及其内边距所占据的空间大小。有关客户区大小的属于又两个:

function getViewport() {
  if (document.compatMode == 'BackCompat') { // 混杂模式
    return {
      width: document.body.clientWidth,
      height: document.body.clientHeight
    }
  } else {
    return {
      width: document.documentElement.clientWidth,
      height: document.documentELement.clientHeight
    }
  }
}
2.3.3、滚动大小

滚动大小,指的是包含滚动内容的元素的大小。

scrollWidth 和 scrollHeight 主要用于确定元素内容的实际大小。对于不包含滚动条的页面而言,scrollWidth 和 scrollHeight 与 clientWidth 和 clientHeight 之间的关系并不十分清晰。在这种情况下,基于 document.documentElement 查看这些属性会在不同浏览器间发现一些不一致问题。

滚动大小

在确定文档的总高度时(包括基于视口的最小高度时),必须取得 scrollWidth / clientWidth 和 scrollHeight / clientHeight 中的最大值,才能保证在跨浏览器的环境下得到精确的结果。

var docHeight = Math.max(document.documentElement.scrollHeight, document.documentElement.clientHieght)
var docWidth = Math.max(document.documentElement.scrollWidth, document.documentElement.clientWidth)

对于运行在混杂模式下的IE,则需要用 document.body 代替 document.documentElement


通过 scrollLeft 和 scrollTop 属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位置。
下面这个函数会检测元素是否位于顶部,如果不是就将其回滚到顶部

function scrollToTop(element) {
  if (element.scrollTop != 0) element.scrollTop = 0
}
2.3.4、确定元素大小

浏览器为每个元素提供了一个 getBoundingClientRect()方法。这个方法返回一个矩形对象,包含四个属性:left、top、right、bottom这些属性给出了元素在页面中相对于视口的位置。
浏览器的实现稍微有点不同。IE8及更早版本认为文档的左上角坐标是(2, 2),而其他浏览器包括 IE9 则将传统的(0, 0)作为起点坐标。在IE8及更早版本中,会返回(2, 2), 而其他浏览器返回(0, 0)

// 元素距离当前视口的精确距离
function getBoundingClientRect(element) {
   // 是否为初始化(是否定义过),避免多次执行
  if (typeof arguments.callee.offset != 'number') {
    var scrollTop = document.documentElement.scrollTop // 文档滚动的高度

    // 创建元素 测试左上角坐标
    var temp = document.createElement('div')
    temp.style.cssText = 'position:absolute; left: 0; top: 0;'
    document.body.appendChild(temp)

    // 计算 文档左上角的坐标
    arguments.callee.offset = -temp.getBoundingClientRect().top - scrollTop
    // 移除元素
    document.body.removeChild(temp)
    // 回收
    temp = null
  }

  var rect = element.getBoundingClientRect() // 距离当前视口的距离
  var offset = arguments.callee.offset // 文档左上角坐标的值

  return {
    left: rect.left + offset,
    right: rect.right + offset,
    top: rect.top + offset,
    bottom: rect.bottom + offset
  }
}

对于不支持 getBoundingClientReact()的浏览器,可以使用其他手段取得相同信息。

// 对于不支持 getBoundingClientRect() 的浏览器
function getBoundingClientRect(element) {

  var scrollTop = document.documentElement.scrollTop
  var scrollLeft = document.documentElement.scrollLeft

  if (element.getBoundingClientRect) {
    // 是否为初始化(是否定义过),避免多次执行
    if (typeof arguments.callee.offset != 'number') {
      var scrollTop = document.documentElement.scrollTop // 文档滚动的高度

      // 创建元素 测试左上角坐标
      var temp = document.createElement('div')
      temp.style.cssText = 'position:absolute; left: 0; top: 0;'
      document.body.appendChild(temp)

      // 计算 文档左上角的坐标
      arguments.callee.offset = -temp.getBoundingClientRect().top - scrollTop
      // 移除元素
      document.body.removeChild(temp)
      // 回收
      temp = null
    }

    var rect = element.getBoundingClientRect() // 距离当前视口的距离
    var offset = arguments.callee.offset // 文档左上角坐标的值

    return {
      left: rect.left + offset,
      right: rect.right + offset,
      top: rect.top + offset,
      bottom: rect.bottom + offset
    }
  } else {
    var actualLeft = getElementLeft(element) // 获取到屏幕左边的偏移值
    var actualTop = getElementTop(element) // 获取到屏幕上边的偏移值

    // 偏移值 - 滚动的距离 = 当前视口的值
    return {
      left: actualLeft - scrollLeft,
      right: actualLeft + element.offsetWidth - scrollLeft,
      top: actualTop - scrollTop,
      botoom: actualTop + element.offsetHeight - scrollTop
    }
  }
}

由于这里使用了 arguements.callee,所以这个模式不能再严格模式下使用

三、遍历

"DOM2级遍历和范围"模块定义了凉饿用于辅助完成顺序遍历DOM结构的类型:NodeIterator 和 TreeWalker。这两个类型能够基于给定的起点对DOM结构执行深度优化(depth-first)的遍历操作。
可以通过下列代码检测浏览器对DOM2级遍历能力的支持

var supportsTraversals = document.implementation.hasFeature('Traversal', '2.0')
var supportsNodeIterator = (typeof document.createNodeIterator == 'function')
var supportsTreeWalker = (typeof document.createTreeWalker == 'function')

DOM遍历是深度优化的DOM结构遍历,也就是说,移动的方向至少有两个(取决于使用的遍历类型)。遍历以给定节点为根,不可能向上超出DOM树的根节点。


DOM树

如上图的DOM树,从document依序向前。从文档最后的文本节点开始,遍历可以反向移动到DOM树的顶端。NodeIterator 和 TreeWalker 都以这种方式执行遍历。

3.1、NodeIterator

NodeIterator类型是两者中比较简单的第一个,可以使用document,createNodeIterator() 方法创建它的实例。这个方法接受以下4个参数:

whatToShow参数是一个位掩码,通过应该一或多个过滤器(filter) 来确定要访问哪些节点。这个参数的值以常量形式在 NodeFilter 类型中定义,如下(忽略了对HTML没用的常量):

filter参数指定自定义的NodeFilter对象,或者指定一个功能类似节点过滤器(node filter)的函数。每个NodeFilter对象只有一个方法,即accept-Node()

由于NodeFilter是一个抽象的类型,因此不能直接创建它的实例。在必要时,只要创建一个包含 acceptNode() 方法对象,然后将这个对象传入 createNodeIterator() 中即可
创建一个 值显示 <p> 元素的节点迭代器

var filter = {
  acceptNode: function(node) {
    return node.tagName.toLowerCase() == 'p' ? 
    NodeFilter.FILTER_ACCEPT :
    NodeFilter.FILTER_SKIP
  }
}
  
var iterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, filter, false)

第三个参数也可以是一个与acceptNode() 方法类似的函数

var filter = function() {
  return node.tagName.toLowerCase() == 'p' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
}

一般来说,后者比较常用。如果不知道过滤器,那么应该把第三个参数设置为 null

NodeIterator 类型的两个主要方法 nextNode()、previousNode(),分别表示向前一步和向后一步。
在刚刚创建的 NodeIterator 对象中,有一个内部指针指向根节点,因此第一个调用 nextNode() 会返回根节点。当遍历到DOM子数的最后一个节点时,nextNode() 返回 null
。previousNode()的工作机制类似,在 previousNode()返回根节点之后,再次调用返回null

<div id="div">
  <p><b>Hello World</b></p>
  <ul>
    <li>List Item 1</li>
    <li>List Item 2</li>
    <li>List Item 3</li>
  </ul>
</div>

遍历如上结构

var div = document.getElementById('div')
var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, null, false)

var node = iterator.nextNode()
while (node != null) {
  console.log(node.tagName)
  node = iterator.nextNode()
}
/*
DIV
P
B
UL
LI
LI
LI
*/

如果只想返回某一个元素,比如LI,可以为他定义一个过滤器:

var filter = function(node) {
  return node.tagName.toLowerCase() == 'li' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
}

var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter , false)

// todo

由于 nextNode() 和 previousNode() 方法都基于 NodeIterator 在 DOM 结构中的内部指针工作,所以DOM结构的变化会反映在遍历的结果中。

3.2、TreeWalker

TreeWalker 时 NodeIterator 的一个更高级版本。除了包括 nextNode() 和 previousNode() 在内的相同功能之外,还提供了下列的方法:

创建 TreeWalker 对象要使用 document.createTreeWalker() 方法,这个方法接受4个参数(与 createNodeIterator()相同 ):

var div = document.getElementById('div')

var filter = function(node) {
  return node.tagName.toLowerCase() == 'li' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
}

var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, filter, false)
var node =walker.nextNode()

while (node != null) {
  console.log(node.tagName)
  node = walker.nextNode()
}

由于这两个创建方法很相似,所以很容易用 TreeWalker 来代替 NodeIterator:

TreeWalker 真正强大的地方在于能够在DOM结构中沿任何方向移动。使用TreeWalker 遍历DOM树,即使不定义过滤器,也可以取得所有的 <li> 元素。

var div = document.getElementById('div')
var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, null, false)

walker.firstChild() // 转到 <p>
walker.nextSibling() // 转到 <ul>

var node = walker.firstChild(); // 第一个<li>
while(node !== null) {
  console.log(node.tagName)
  node = nextSibling() // 同级<li>
}

TreeWalker 类型还有一个属性,名叫 currentNode,表示任何遍历方法在 上一次遍历中返回的节点。通过设置这个属性也可以修改继续进行的起点

var node =walker.nextNode()
console.log(node == walker.currentNode) // true
walker.currentNode = document.body // 修改起点

四、范围

"DOM2级遍历和范围"模块定义了“范围”(range)接口。通过范围可以选择文档中的一个区域,而不必考虑节点的界限。

4.1、DOM中的范围

DOM2级在 Document 类型中定义了 createRange() 方法。使用 hasFeature() 或者直接检测该方法,都可以确定浏览器是否支持范围。

var supportsRange = document.implementation.hasFeature('Range', '2.0')
var alsoSupportsRange = (typeof document.createRange == 'function')

创建范围通过 createRange()方法

var range = document.createRange()

新创建的范围也直接与创建它的文档关联在一起,不能用于其他文档。
每一个范围由一个Range 实例表示,拥有以下属性和方法:

在把范围放到文档中特定的位置时,这些属性都会被赋值。

4.1.1、用DOM范围实现简单选择

要使用范围来选择文档中的一部分,最简单的方式就是 使用 selectNode()selectNodeContents()。这两个方法都接受一个参数:即一个DOM节点,然后使用该节点中的信息来填充范围。selectNode() 方法选择整个节点,包括器子节点; 而 selectNodeContents() 方法则只选择节点的 子节点。

<body>
  <p id="p1"><b>Hello</b> World</p>
</body>

根据如上代码来创建文档

var rang1 = document.createRange()
var rang2 = document.createRange()
var p1 = document.getElementById('p1')

rang1.select(p1) // 包含 p 元素
rang2.selectNodeContents(p1) // 不包含 p 元素
示例图

在调用selectNode() 时,startContainer、endContainer、commonAncestorContainer都等于 传入节点的父节点,而 startOffset 属性等于 给定节点在其父节点的 childNodes 集合中的索引(在这个例子中是1——因为兼容DOM的浏览器会将空格算作第一个文本节点),endOffset 等于 startOffset 加 1(因为只选择了一个节点)

在掉头 selectNodeContents() 时,startContainer、endContainer 和 commonAncestorContainer 等于传入的节点,而startOffset 属性始终等于0,因为范围从给定节点的第一个子节点开始。endOffset 等于子节点的数量,在这个例子中为2

为了 更精确地控制将哪些节点包含在范围中,还可以使用下列方法

调用这些方法的时候,所有属性都会自动为你设置号。要想创建复杂的范围选区,也可以直接指定这些属性的值。

4.1.2、用DOM 范围实现复杂选择

要创建复杂的范围就得使用 setStart()setEnd() 方法。这两个方法都接受两个参数:参照节点、偏移量。可以使用这两个方法来模仿 selectNode() 和 selectNodeContents()

var range1 = document.createRange()
var range2 = document.createRange()
var p1 = document.getElementById('p1')
var p1Index = -1

for (var i = 0, len = p1.parentNode.childNodes.length; i < len; i++) {
  if (p1.parentNode.childNodes[i] == p1) {
    p1Index = i
    break
  }
}

range1.setStart(p1.parentNode, p1Index)
range1.setEnd(p1.parentNode, p1Index + 1)
range2.setStart(p1, 0)
range2.setStart(p1, p1.childNodes.length)

模仿selectNode() 和 selectNodeContents() 并不是 setStart() 和 setEnd() 的主要用途,它们更胜一筹的地方在于能够选择节点的一部分。

假设你只想选择前面 HTML 实例代码从 "Hello" 的 "llo" 到 "world" 的 "o" ——很容易就能做到

// 获取所有节点的引用
var p1 = document.getElementById('p1')
var helloNode = p1.firstChild.firstChild
var worldNode = p1.lastChild

// 创建范围 指定起点和终点
var range = document.createRange()
range.setStart(helloNode, 2) // 设置偏移两个字符
range.setEnd(worldNode, 3) // 偏移三个字符(前面的空格)
图解

当然,仅仅是选择了文档中的某一部分用处并不是很大。但重要的是,选择之后才可以对选区进行操作

4.1.3、操作DOM 范围中的内容

在创建范围时,内部会为这个范围创建一个文档片段,范围所属的全部节点都被添加到了这个文档片段中。为了创建这个文档片段,范围内容的格式必须正确有效。
对于上年截取字符的代码,范围经过计算知道选区缺少了一个开始的<b>标签,就会在后台动态加入一个该标签,同时还会在前面加入一个表示结束的</b>标签以结束“He”。并且重构一个有效良好的DOM格式

llo</b> wo ====> <p><b>He</b><b>llo</b> world!</p>

在创建了范围之后,就可以使用各种方法对范围的内容进行操作了(注意,表示范围的内部文档片段中的所有节点,都只是指向文档中相应节点的指针)

deleteContents()
这个方法能够从文档中删除范围所包含的内容

var range = document.createRange()
range.setStart(helloNode, 2)
range.setEnd(worldNode, 3)
range.deleteContents()

此时页面会显示如下HTML代码

<p id="p1"><b>He</b>rld</p>
图解
由于范围选区在修改底层DOM 结构时能够保证格式良好,因此即使内容被删除了,最终的 DOM 结构依旧是 格式良好的。

extractContents() 也会从文档中移除范围选区。区别在于:extractContents() 会返回范围的文档片段。利用这个返回的值,可以将范围的内容插入到文档中的其他地方。

var fragment = range.extractContents()
p1.parentNode.appendChild(fragment)

此时的文档结构

<p id="p1"><b>He</b>rld</p>
<b>llo</b> Wo

还有一种做法,使用 cloneContents() 创建范围对象的一个副本,然后在文档的其他地方插入该副本

var fragment = range.cloneContents()
p1.parentNode.appendChild(fragment)

此时的文档结构

<p id="p1"><b>Hello</b> World</p>
<b>llo</b> Wo

需要注意的是:在调用上面介绍的方法之前,拆分的节点并不会产生格式良好的文档片段。换句话说,原始的HTML在DOM被修改之前会始终保持不变。

4.1.4、插入DOM范围中的内容

insertNode() 方法可以想范围选区的开始插入一个节点。
假如在前面示例的 HTML 代码中插入 <span style="color: red"> Insterted text <span>

var p1 = document.getElementById('p1')
var helloNode = p1.firstChild.firstChild
var worldNode = p1.lastChild
var range = document.createRange()

var span = document.createElement('span')
span.style.color = 'red'
span.appendChild(document.createTextNode('Insterted text'))
range.insertNode(span)

此时的文档结构

<p id="p1"><b>He<span style="color: red;">Insterted text</span>llo</b> World</p>

除了向范围内部 插入内容之外,还可以围绕范围插入内容,surroundContents() 方法。接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列步骤。

  1. 提取除范围中的内容(类是执行 extractContent())
  2. 将给定节点插入到文档中原来范围所在的位置上;
  3. 将文档片段的内容添加到给定节点中。

range.selectNode(helloNode) // <b>Hello</b>

var span = document.createElement("span")
span.style.backgroundColor = 'yellow'
range.surroundContents(span)

此时的文档变化

<b><span style="background-color: yellow;">Hello</span></b>

为了插入<span>,范围必须包含整个 DOM 选区( 不能仅仅包含选中的 DOM 节点 )

4.1.5、折叠DOM范围

所谓 折叠范围,就是指范围中未选择文档的任何部分。

collapse()方法来折叠范围,这个方法接受一个参数,一个布尔值,表示要折叠到范围的哪一端。参数 true 表示折叠到范围的起点,false表示折叠到范围的终点。要确定范围异界折叠完毕,可以检测 collapsed 属性。

range.collapse(true) // 折叠到起点
console.log(range.collapsed)

某个范围是否处于折叠状态,可以帮我嫩确定范围中的两个节点是否紧密相邻。

<p id="p1"> Paragraph 1</p><p id="p2">Paragraph 2</p>

如上结构:如果我们不知道其构成(比如说,这是动态生成的代码),那么可以像下面这样创建一个范围。

var p1 = document.getElementById('p1')
var p2 = document.getElementById('p2')
var range = document.createRange()

range.setStartAfter(p1)
range.setEndBefore(p2)
console.log(range.collapsed) // ,true

这个例子中,新创建的范围时折叠的 因为p1 的后面 和 p2的前面什么也没有。

4.1.6、比较DOM范围

在有多个DOM范围的情况下,可以使用 compareBoundaryPoints()来确定这些范围是否有公共的边界(起点或终点)。接受两个参数:表示比较方式的常量值、要比较的范围。
表示比较的常量值:

compareBoundaryPoints()方法可能的返回值如下:

<p id="p1"><b>Hello</b> World!</p>
var range1 = document.createRange()
var range2 = document.createRange()
var p1 = document.getElementById('p1')

range1.selectNodeContents(p1)
range2.selectnodeContents(p1)
range2.setEndBefore(p1.lastChild)

console.log(range1.compareBoundaryPoints(Range.START_TO_START, range2)) // 0
console.log(range1.compareBoundaryPoints(Range.END_TO_END, range2)) // 1
图示
4.1.7、复制 DOM 范围

cloneRange() 方法 可以复制范围。这个方法会创建调用它的范围的一个副本

var newRange = range.cloneRange()

要创建的范围于原来的范围包含相同的属性,而修改它的端点不会影响到原来的副本

4.1.8、清理DOM 范围

在使用完范围之后,最好是调用 detach() 方法,以便从创建范围的文档中分离出该范围。调用detach()之后,就可以方法的解除对范围的引用,从而让垃圾回收机制回收其内存了。

range.detach() // 从文档中分离
range = null // 解除引用

4.2、IE8及更早版本中的范围

虽然IE9支持DOM范围,但IE8及之前版本不支持DOM范围。IE8及早期版本支持一种类似的概念,即文本范围(text range)。文本范围是IE专有的特性,其他浏览器都不自持。
通过<body>、<button>、<input>、<textarea>等这几个元素,可以调用 createTextRange() 方法来创建文本范围

var range = document.body.createTextRange()
4.2.1、用IE范围实现简单的选择

findText()
选择页面中某一区域的最简单方式,就是使用范围的findText()方法。这个方法会找到第一次出现的给定文本,并将范围移过来以环绕该文本。如果没有找到文本,这个反法返回false;否则返回 true。
以如下html 为例

<p id="p1"><b>Hello</b> World!</p>
var range = document.body.createTextRange()
var found = range.findText("Hello")
console.log(found) // true——代表找到文本
console.log(found.text) // Hello

还可以为 findText() 传入另一个参数,表示向哪个方向继续搜索的数值。负值表示应该从当前位置向后搜索,而正值表示应该从当前位置向前搜索。

var found = range.findText('Hello')
var foundAgain = range.findText('Hello', 1)

IE中与DOM中的 selectNode() 方法最接近的方法是 moveToElementText(),这个方法接受一个 DOM 元素,并选择改元素的所有文本,包括HTML标签。

var range = document.body.createTextRange()
var p1 = document.getElementById('p1')
range.moveToElementText(p1)

在文本范围中包含HTML的情况下,可以使用 htmlText 属性取得范围的全部内容,包括 HTML 和文本。

console.log(range.htmlText) //  <P id=p1><B>Hello</B> World!</P>

IE的范围没有任何属性可以随着范围选区的变化而动态更新。不过,其 parentElement() 方法倒是与 DOM 的 commonAncestorContainer 属性类似

var ancestor = range.parentElement()

这样得到的父元素 始终都可以反映文本选区的父节点

4.2.2、使用IE范围实现复杂的选择

在IE中创建范围的方法,就是以特定的增向量向四周移动范围。IE提供了4个方法:move()、moveStart()、moveEnd() 和 expand()。这些方法都接受两个参数:移动单位、移动单位的数量
移动单位是下列一种字符串值

通过 moveStart()方法可以移动范围的起点,通过moveEnd()可以移动范围的终点,移动的幅度由单位数量指定

range.moveStart('word', 2) // 移动两个单词
range.moveEnd('character', 1) // 移动一个字符

使用expand()方法可以将范围规范化。expand() 方法的作用是将任何部分选择的文本全部选中。例如,当前选择的是一个单词中间的两个字符,调用expand("word")可以将整个单词都包含在范围之内。


move()方法则首先会折叠当前范围(让起点和终点相等),然后再将范围移动指定的单位数量。

range.move('character', 5)

调用 move() 之后,范围的起点和终点相同,因此必须再使用 moveStart() 或 moveEnd() 创建新的选区

4.2.3、操作IE范围中的内容

使用 text属性 或 pasteHTML() 方法。通过 text 属性可以取得范围中的内容文本;但是,也可以通过这个属性设置范围中的文本内容

var range = document.body.createTextRange()
range.findText('Hello')
range.text = 'Howdy'

此时的文档结构

<P id=p1><B>Howdy</B> World!</P>

要向 范围中插入 HTML代码,就得使用 pasterHTML() 方法,

var range = document.body.createTextRange()
range.findText('Hello')

range.pasteHTML('<em>Howdy</em>')

此时的文档结构

<P id=p1><B><EM>Howdy</EM></B> World!</P>

不过,在范围中包含HTML代码是,不应该使用pasteHTML(),因为这样很可能会导致不可预料的结果——很可能是格式不对的HTML

4.2.4、折叠IE范围

IE为范围提供的 collapse() 方法与相应的 DOM 方法用法一样:传入 true 把范围折叠到起点,传入false把范围折叠到终点

range.collapse(true) // 折叠到起点

可惜的是,没有对应的 collapsed 属性让我们知道范围是否折叠完毕。为此,需要使用到 boundingWidth 属性,该属性返回范围的宽度(以像素为单位)。如果 boundingWidth 属性等于 0,就说明范围已经折叠了。

var isCollapsed = (range.boundingWidth == 0)

此外还有 boundingHeight、boundingLeft、boundingTop 等属性,虽然他们都不像 boundingWidth 那么有用,但也可以提供一些有关范围位置的信息。

4.2.5、比较IE范围

IE中的 compareEndPoints() 方法与 DOM 范围的 compareBoundaryPoints() 方法类似。接受两个参数:比较的类型、比较的范围。
比较类型与DOM相似

与DOM类似,返回值有三种情况

var range1 = document.body.createTextRange()
var range2 = document.body.createTextRange()

range1.findText('Hello World')
range2.findText('Hello')

console.log(range1.compareEndPoints('StartToStart', range2))  // 0
console.log(range1.compareEndPoints('EndToEnd', range2)) // 1

IE中 还有两个方法,也是用于比较范围的:isEqual() 用于确定两个范围是否相等,inRange() 用于确定一个范围是否包含另一个范围。

var range1 = document.body.createTextRange()
var range2 = document.body.createTextRange()

range1.findText('Hello World')
range2.findText('Hello')

console.log(range1.isEqual(range2)) // false
console.log(range1.inRange(range2)) // true
4.5.6、复制IE范围

在IE中使用 duplicate() 方法可以复制文本范围,结果会创建原范围的一个副本

var newRange = range.duplicate()

新创建的范围会带有与原范围完全相同的属性

五、小结

"DOM2 级样式" 模块只要针对操作元素的样式信息而开发,其特征简要总结如下。

上一篇下一篇

猜你喜欢

热点阅读