遍历
要遍历文档的元素很简单,可以使用之前介绍的querySelectorAll()等方法,但这些方法都是元素Element层的,无法操作到Node层,比如文本节点或注释节点。“DOM2 级遍历和范围”模块定义了两个类型进行深度优先的遍历操作:NodeIterator 和 TreeWalker。(IE不支持)
NodeIterator
document.createNodeIterator()方法创建它的新实例,4个参数:
- root:想要作为搜索起点的树中的节点。
- whatToShow:表示要访问哪些节点的数字代码。
- filter:是一个 NodeFilter 对象,或者一个表示应该接受还是拒绝某种特定节点的函数。
- entityReferenceExpansion:布尔值,表示是否要扩展实体引用。这个参数在 HTML 页面中没有用,因为其中的实体引用不能扩展。
whatToShow 参数是一个位掩码,通过应用一或多个过滤器(filter)来确定要访问哪些节点。这个参数的值以常量形式在 NodeFilter 类型中定义(除了 NodeFilter.SHOW_ALL 之外,可以使用按位或操作符来组合多个选项):
- NodeFilter.SHOW_ALL:显示所有类型的节点。
- NodeFilter.SHOW_ELEMENT:显示元素节点。
- NodeFilter.SHOW_ATTRIBUTE:显示特性节点。由于 DOM 结构原因,实际上不能使用这个值。
- NodeFilter.SHOW_TEXT:显示文本节点。
- NodeFilter.SHOW_CDATA_SECTION:显示 CDATA 节点。对 HTML 页面没有用。
- NodeFilter.SHOW_ENTITY_REFERENCE:显示实体引用节点。对 HTML 页面没有用。
- NodeFilter.SHOW_ENTITYE:显示实体节点。对 HTML 页面没有用。
- NodeFilter.SHOW_PROCESSING_INSTRUCTION:显示处理指令节点。对 HTML 页面没有用。
- NodeFilter.SHOW_COMMENT:显示注释节点。
- NodeFilter.SHOW_DOCUMENT:显示文档节点。
- NodeFilter.SHOW_DOCUMENT_TYPE:显示文档类型节点。
- NodeFilter.SHOW_DOCUMENT_FRAGMENT:显示文档片段节点。对 HTML 页面没有用。
- NodeFilter.SHOW_NOTATION:显示符号节点。对 HTML 页面没有用。
可以通过 createNodeIterator()方法的 filter 参数来指定自定义的 NodeFilter 对象,或者 指定一个功能类似节点过滤器(node filter)的函数。每个 NodeFilter 对象只有一个方法,即 accept- Node();如果应该访问给定的节点,该方法返回 NodeFilter.FILTER_ACCEPT,如果不应该访问给 定的节点,该方法返回 NodeFilter.FILTER_SKIP。由于 NodeFilter 是一个抽象的类型,因此不能 直接创建它的实例。在必要时,只要创建一个包含 acceptNode()方法的对象,然后将这个对象传入 createNodeIterator()中即可。
var root = document.querySelector("body");
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);
var node = iterator.nextNode();
while(node != null){
console.log(node);
node = iterator.nextNode();
}
其中,NodeIterator 类型的两个主要方法是 nextNode()和 previousNode()。顾名思义,在深度优先 的 DOM 子树遍历中,nextNode()方法用于向前前进一步,而 previousNode()用于向后后退一步。 在刚刚创建的 NodeIterator 对象中,有一个内部指针指向根节点,因此第一次调用 nextNode()会返回根节点。当遍历到 DOM 子树的最后一个节点时,nextNode()返回 null。previousNode()方法的工作机制类似。
第三个参数也可以是一个与 acceptNode()方法类似的函数,常用的也是这种写法:
var filter = function (node) {
return node.tagName.toLowerCase() == "p" ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};
Firefox 3.5 之前的版本没有实现 createNodeIterator()方法,但却支持下一节要讨论的 createTreeWalker()方法。
TreeWalker
TreeWalker 是 NodeIterator 的一个更高级的版本。除了包括 nextNode()和 previousNode() 在内的相同的功能之外,这个类型还提供了下列用于在不同方向上遍历 DOM 结构的方法:
- parentNode():遍历到当前节点的父节点
- firstChild():遍历到当前节点的第一个子节点
- lastChild():遍历到当前节点的最后一个子节点
- nextSibling():遍历到当前节点的下一个同辈节点
- previousSibling():遍历到当前节点的上一个同辈节点
TreeWalker 类型还有一个属性,名叫 currentNode,表示任何遍历方法在上一次遍历中返回的节点。通过设置这个属性也可以修改遍历继续进行的起点。
filter 可以返回的值有所不同,除了 NodeFilter.FILTER_ACCEPT 和 NodeFilter. FILTER_SKIP 之外,还可以使用 NodeFilter.FILTER_REJECT。在使用 TreeWalker 对象时,NodeFilter.FILTER_SKIP 会跳过相应节点继续前进到子树中的下一个节点,而 NodeFilter.FILTER_REJECT 则会跳过相应节点及该节点的整个子树(跳过相应节点和该节点的整个子树之后继续前进遍历该节点后面的节点)。
而合理利用TreeWalker增加的方法,可以方便地遍历到想要的节点,以下不适用filter参数(设为null)也能筛选出li元素:
html:
<div class="div-cla">
<p class="p-cla">我是段落</p>
<ul class="ul-list">
<li class="li-cla">第一个li</li>
<li class="second-li">第二个li</li>
<li class="third-li">第三个li</li>
<li class="fourth-li">第四个li</li>
</ul>
</div>
js:
var root = document.querySelector(".div-cla");
var walker = document.createTreeWalker(root,NodeFilter.SHOW_ELEMENT,null,false);
walker.nextNode();
walker.nextNode();//定位到了ul
var li = walker.firstChild();
while (li != null){
//改变currentNode就跳过了第三个li
if(li.classList.contains("second-li")){
walker.currentNode = document.querySelector(".third-li");
}
console.log(li);
li = walker.nextSibling();
}
总结:对于NodeIterator和TreeWalker,由于 IE 中没有对 应的类型和方法,所以使用遍历的跨浏览器解决方案非常少见。
范围
用 DOM 范围实现简单选择
DOM2 级在 Document 类型中定义了 createRange()方法,创建范围并设置了其位置之后,还可以针对 范围的内容执行很多种操作,从而实现对底层 DOM 树的更精细的控制。
每个范围由一个 Range 类型的实例表示,下列属性提供了当前范 围在文档中的位置信息:
- startContainer:包含范围起点的节点(即选区中第一个节点的父节点)。
- endContainer:包含范围终点的节点(即选区中最后一个节点的父节点)。
- startOffset:范围在 startContainer 中起点的偏移量。如果 startContainer 是文本节点、注释节点或 CDATA 节点,那么 startOffset 就是范围起点之前跳过的字符数量。否则,startOffset 就是范围中第一个子节点的索引。
- endOffset:范围在 endContainer 中终点的偏移量(与 startOffset 遵循相同的取值规则)。
- commonAncestorContainer:startContainer 和 endContainer 共同的祖先节点在文档树中位置最深的那个。
html:
<div class="p-con">
<p>哈哈</p><p id="p1"><b>Hello</b>world!</p>
</div>
js:
var range1 = document.createRange(),
range2 = document.createRange(),
p1 = document.getElementById("p1");
range1.selectNode(p1);
range2.selectNodeContents(p1);
console.log(range1);
console.log(range1.startContainer);//div.p-con
console.log(range1.endContainer);//div.p-con
//打印2:即范围内第一个节点在startContainer中的索引
console.log(range1.startOffset);
//打印3:即范围内最后一个节点在endContainer中的索引加1
console.log(range1.endOffset);//3
console.log(range1.commonAncestorContainer);//div.p-con
console.log(range2);
console.log(range2.startContainer);//p#p1
console.log(range2.endContainer);//p#p1
console.log(range2.startOffset);//0
console.log(range2.endOffset);//2
console.log(range2.commonAncestorContainer);//p#p1
为了更精细地控制将哪些节点包含在范围中,还可以使用下列方法:
- setStartBefore(refNode):将范围的起点设置在 refNode 之前,因此 refNode 也就是范围 选区中的第一个子节点。同时会将 startContainer 属性设置为 refNode.parentNode,将startOffset 属性设置为 refNode 在其父节点的 childNodes 集合中的索引。
- setStartAfter(refNode):将范围的起点设置在 refNode 之后,因此 refNode 也就不在范 围之内了,其下一个同辈节点才是范围选区中的第一个子节点。同时会将 startContainer 属 性设置为 refNode.parentNode,将 startOffset 属性设置为 refNode 在其父节点的childNodes 集合中的索引加 1。
- setEndBefore(refNode):将范围的终点设置在 refNode 之前,因此 refNode 也就不在范围之内了,其上一个同辈节点才是范围选区中的最后一个子节点。同时会将 endContainer 属性设置为 refNode.parentNode,将 endOffset 属性设置为 refNode 在其父节点的 childNodes集合中的索引。
- setEndAfter(refNode):将范围的终点设置在 refNode 之后,因此 refNode 也就是范围选区中的最后一个子节点。同时会将 endContainer 属性设置为 refNode.parentNode,将endOffset 属性设置为 refNode 在其父节点的 childNodes 集合中的索引加 1。
用 DOM 范围实现复杂选择
要创建复杂的范围就得使用 setStart()和 setEnd()方法。这两个方法都接受两个参数:一个参 照节点和一个偏移量值。对 setStart()来说,参照节点会变成 startContainer,而偏移量值会变成 startOffset。对于 setEnd()来说,参照节点会变成 endContainer,而偏移量值会变成 endOffset。
这样可以实现和上面一样的效果:
var range1 = document.createRange();
var range2 = document.createRange();
var p1 = document.getElementById("p1"), p1Index = -1;
var i, len;
for (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.setEnd(p1, p1.childNodes.length);
console.log(range1);
console.log(range2);
显然使用setStart()和setEnd更直观更方便,以下进行精确控制找到llo wo:
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);
console.log(range);
操作 DOM 范围中的内容
在创建范围时 ,内部会为这个范围创建一个文档片段,范围所属的全部节点都被添加到了这个文档片段中。
创建了范围之后,就可以使用各种方法对范围的内容进行操作了(注意,表示范围的内部文档片段中的所有节点,都只是指向文档中相应节点的指针)。
- 从文档中删除范围所包含的内容
接上:
range.deleteContents();
- 提取内容:内容一样会被删除,但返回值代码片段,可用于放置在其他地方等等
接上:
var fragment = range.extractContents();
console.log(fragment);
p1.parentNode.appendChild(fragment);
- 创建副本:与extractContents的区别是,cloneContents返回的文档片段包含的是范围中节点的副本,而不是实际的节点
接上:
var copyFragment = range.cloneContents();
p1.parentNode.appendChild(copyFragment);
插入 DOM 范围中的内容
- 使用 insertNode() 方法可以向范围选区的开始处插入一个节点,插入的位置是范围选区的开始位置:
var span = document.createElement("span");
span.style.color = "red";
span.appendChild(document.createTextNode("Inserted text"));
range.insertNode(span);
- 环绕范围插入内容:surroundContents()方法接受一个参数,即环绕范围内容的节点,而在环绕范围插入内容时,后台会执行下列步骤:①提取出范围中的内容(类似执行 extractContent());②将给定节点插入到文档中原来范围所在的位置上;③将文档片段的内容添加到给定节点中:
接上:
//这句代码很重要
range.setEnd(helloNode,5);
var span = document.createElement("span");
span.style.backgroundColor = "red";
range.surroundContents(span);
效果图:
Snip20180313_13.png
所以环绕插入常用来突显某些文字.
注意点:代码中标注的这一句很重要,因为在环绕范围插入内容的时候,范围不能部分选中非文本节点并且只有一个边界(即是说选了部分该非文本节点和其他节点),否则会抛出错误.而标注的那一句把范围的开始和结束都限定在了<b></b>,所以没有问题.
折叠 DOM 范围
折叠范围:就是指范围中未选择文档的任何部分,在折叠范围时,其位置会落在文档中的两个部分之间,可能是范围选区的开始位置,也可能是结束位置。collapse()方法:一个参数,传true代表折叠到起点,false代表折叠到终点。
接上:
console.log(range.collapsed);//打印false
range.collapse(false);
console.log(range.collapsed);//打印true
检测某个范围是否处于折叠状态,可以帮我们确定范围中的两个节点是否紧密相邻:
html:
<p id="p2">Paragraph 2</p><p id="p3">Paragraph 3</p>
js:
var p2 = document.querySelector("#p2"),p3 = document.querySelector("#p3");
var range2 = document.createRange();
range2.setStartAfter(p2);
range2.setEndBefore(p3);
//打印false,因为p2后面p3前面什么都没有
console.log(range2.collapsed);
比较 DOM 范围
在有多个范围的情况下,可以使用 compareBoundaryPoints()方法来确定这些范围是否有公共的边界(起点或终点)。这个方法接受两个参数:表示比较方式的常量值和比较范围。表示比较方式的常量值如下所示:
- Range.START_TO_START(0):比较第一个范围和第二个范围的起点;
- Range.START_TO_END(1):比较第一个范围的起点和第二个范围的终点;
- Range.END_TO_END(2):比较第一个范围和第二个范围的终点;
- Range.END_TO_START(3):比较第一个范围的终点和第一个范围的起点。
compareBoundaryPoints()方法可能的返回值如下:如果第一个范围中的点位于第二个范围中的点之前,返回-1;如果两个点相等,返回 0;如果第一个范围中的点位于第二个范围中的点之后,返回 1。
复制 DOM 范围
可以使用 cloneRange()方法复制范围。这个方法会创建调用它的范围的一个副本。
var newRange = range.cloneRange();
新创建的范围与原来的范围包含相同的属性,而修改它的端点不会影响原来的范围。
清理 DOM 范围
在使用完范围之后,最好是调用 detach()方法,以便从创建范围的文档中分离出该范围。调用 detach()之后,就可以放心地解除对范围的引用,从而让垃圾回收机制回收其内存了。
range.detach();//分离范围
range = null;//垃圾回收
IE8 及更早版本中的范围
下次再继续补充。
网友评论