遍历

  • DOM2 Traversal and Range模块定义了两个类型用于辅助顺序遍历DOM结构。这两个类型NodeIterator和Treewalker从某个起点开始执行对DOM结构的深度优先遍历。
  • DOM遍历是**对DOM结构的深度优先遍历,至少允许朝两个方向移动(取决于类型)**。遍历以给定节点为根,不能在 DOM中向上超越这个根节点。
  • 查看以下HTML:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!DOCTYPE html> 
    <html>
    <head>
    <title>Example</title>
    </head>
    <body>
    <p><b>Hello</b> world!</p>
    </body>
    </html>
    DOM树如下:

1)NodeIterator

  • NodeIterator类型是两个类型中比较简单的,可以通过document.createNodeIterator()方法创建其实例。方法接收四个参数:
    • root:作为遍历根节点的节点;
    • whatToShow:数值代码,表示应该访问哪些节点;
    • filter:NodeFilter对象或函数,表示是否接收或跳过特定节点;
    • entityReferenceExpansion,布尔值,表示是否扩展实体引用。这个参数在HTML文档中没有效果,因为实体引用永远不扩展。
  • whatToshow参数是一个位掩码,通过应用一个或多个过滤器来指定访问哪些节点。这个参数对应的常量是在NodeFilter类型中定义的。
    • 这些值除了 NodeFilter.SHOW_ALL 之外,都可以组合使用。
  • createNodeIterator()方法的filter参数可以用来指定自定义NodeFilter对象,或者一个作为节点过滤器的函数。

  • NodeFilter对象只有一个方法**acceptNode()**,如果给定节点应该访问就返回NodeFilter.FILTER_ACCEPT,否则返回NodeFilter.FILTER_SKIP。

  • NodeFilter 是一个抽象类型,所以不可能创建它的实例。只要创建一个包含 acceptNode()的对象,然后把它传给createNodeIterator()就可以了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /*
    以下代码定义了只接收<p>元素的节点过滤器对象:
    */
    let filter = {
    acceptNode(node) {
    return node.tagName.toLowerCase() == "p" ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
    }
    };
    /*
    filter 参数还可以是一个函数,与 acceptNode()的形式一样,如下面的例子所示:
    */
    let filter = function(node) {
    return node.tagName.toLowerCase() == "p" ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
    };
    let iterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, filter, false);
    1
    2
    3
    4
    /*
    创建一个简单的遍历所有节点的 NodeIterator
    */
    let iterator = document.createNodeIterator(document, NodeFilter.SHOW_ALL, null, false);
  • NodeIterator 的两个主要方法是nextNode()和 previousNode():

    • nextNode()方法在DOM子树中以深度优先方式进前一步;previousNode()是在遍历中后退一步
    • NodeIterator对象的时候,会有一个内部指针指向根节点,因此第一次调用nextNode ()返回的是根节点。。
    • 当遍历到达DOM树最后一个节点时,nextNode()返回null;previousNode()返回遍历的根节点后,再次调用也会返回null。
      1
      2
      3
      4
      5
      6
      7
      8
      <div id="div1"> 
      <p><b>Hello</b> world!</p>
      <ul>
      <li>List item 1</li>
      <li>List item 2</li>
      <li>List item 3</li>
      </ul>
      </div>
  • 假设想要遍历<div>元素内部所有元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let div = document.getElementById("div1"); 
    let iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, null, false);
    let node = iterator.nextNode();
    while (node !== null) {
    console.log(node.tagName); // 输出标签名
    node = iterator.nextNode();
    }
    /*
    DIV
    P
    B
    UL
    LI
    LI
    LI
    */
  • 如果只想遍历<li>元素,可以传入一个过滤器:

1
2
3
4
5
6
7
8
9
10
let div = document.getElementById("div1"); 
let filter = function(node) {
return node.tagName.toLowerCase() == "li" ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
};
let iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT,filter, false);
let node = iterator.nextNode();
while (node !== null) {
console.log(node.tagName); // 输出标签名
node = iterator.nextNode();
}

2)TreeWalker

  • Treewalker是NodeIterator的高级版。
  • 除了包含同样的nextNode().previousNode()方法,Treewalker还添加了如下在 DOM结构中向不同方向遍历的方法。
    • parentNode(),遍历到当前节点的父节点;
    • firstChild(),遍历到当前节点的第一个子节点;
    • lastChild(),遍历到当前节点的最后一个子节点;
    • nextSibling(),遍历到当前节点的下一个同胞节点;
    • previoussibling(),遍历到当前节点的上一个同胞节点。
  • 调用document.createTreewalker()方法来创建Treewalker对象。
    • 参数: 作为遍历起点的根节点、要查看的节点类型、节点过滤器、一个表示是否扩展实体引用的布尔值。
    • 节点过滤器:除了可以返回NodeFilter.FILTER_ACCEPT和NodeFilter.FILTER_SKIP,还可以返回NodeFilter.FILTER_REJECT。
  • 在使用NodeIterator时,NodeFilter.FILTER_SKIP和NodeFilter.FILTER_REJECT是一样的。
  • 在使用Treewalker时,NodeFilter.FILTER_SKIP表示跳过节点,访问子树中的下一个节点,而NodeFilter.FILTER_REJECT则表示跳过该节点以及该节点的整个子树

范围

1、DOM范围

  • DOM2在Document类型上定义了一个createRange()方法可用于创建一个DOM范围对象。
1
let range = document.createRange(); 
  • 新创建的氛围对象是与创建它的文档关联,不能在其他文档中使用。
  • 创建范围并指定它的位置后就可以对范围的内容执行一些操作,从而实现对底层DOM树更精细的控制。
  • 每个范围都是 Range类型的实例,拥有相应的属性和方法。

2、简单选择

  • selectNode()或 selectNodeContents()是通过范围选择文档中౼个部分最简单的方式。
    • 参数:接收一个节点,并将该节点的信息添加到调用它的氛围
    • 不同:
      • selectNode()选择整个节点,包括其后代节点。
      • selectNodeContents()只选择节点的后代。
        1
        2
        3
        4
        5
        6
        <!DOCTYPE html> 
        <html>
        <body>
        <p id="p1"><b>Hello</b> world!</p>
        </body>
        </html>
        1
        2
        3
        4
        5
        let range1 = document.createRange(), 
        range2 = document.createRange(),
        p1 = document.getElementById("p1");
        range1.selectNode(p1);
        range2.selectNodeContents(p1);
  • 调用selectNode()时,startContainer、endContainer和 commonAncestorContainer都等于传入节点的父节点。在这个例子中,这几个属性都等于document.body。
  • 选定节点或节点后代之后,还可以在范围上调用相应的方法,实现对范围中选区的更精细控制:

3、复杂选择

  • 要创建复杂的范围,需要使用setstart()和setEnd()方法。
    • 两个方法都接收两个参数:参照节点和偏移量。
    • 对setstart()来说,参照节点会成为startContainer,而偏移量会赋值给startoffset。
    • 对setEnd()而言,参照节点会成为endContainer,而偏移量会赋值给endoffset。
      1
      2
      3
      4
      5
      6
      <!DOCTYPE html> 
      <html>
      <body>
      <p id="p1"><b>Hello</b> world!</p>
      </body>
      </html>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      /*
      通过范围从中选择从"Hello"中的"1lo"到" world! 中的"o"的部分。
      */
      let p1 = document.getElementById("p1"),
      helloNode = p1.firstChild.firstChild,
      worldNode = p1.lastChild
      let range = document.createRange();
      range.setStart(helloNode, 2);
      range.setEnd(worldNode, 3);

4、操作范围

  • 创建范围之后,浏览器会在内部创建一个文档片段节点,用于包含范围选区中的节点。
  • 为操作范围的内容,选区中的内容必须格式完好。
  • 范围能够确定缺ܾ的开始和结束标签,从而可以重构出有效的 DOM 结构,以便后续操作
1
2
3
4
5
6
<!DOCTYPE html> 
<html>
<body>
<p id="p1"><b>Hello</b> world!</p>
</body>
</html>
  • 针对以上例子,范围发现选区中缺少一个开始的<b>标签,于是会在后台动态补上这个标签,同时还需要补上封闭”He”的结束标签</b>,结果会把 DOM 修改为这样:

    1
    <p><b>He</b><b>llo</b> world!</p> 
  • deleteContents()方法会从文档中删除范围包含的节点。

1
2
3
4
5
6
7
let p1 = document.getElementById("p1"),
helloNode = p1.firstChild.firstChild,
worldNode = p1.lastChild,
range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
range.deleteContents();
  • 执行上面的代码之后,页面中的 HTML 会变成这样:
    1
    <p><b>He</b>rld!</p> 
  • extractContents()也会从文档中移除范围选区,同时返回范围对应的文档片段。
  • 下面例子中提取了范围的文档片段,然后把它添加到文档<body>元素的最后。
    1
    2
    3
    4
    5
    6
    7
    8
    let p1 = document.getElementById("p1"), 
    helloNode = p1.firstChild.firstChild,
    worldNode = p1.lastChild,
    range = document.createRange();
    range.setStart(helloNode, 2);
    range.setEnd(worldNode, 3);
    let fragment = range.extractContents();
    p1.parentNode.appendChild(fragment);
    1
    2
    <p><b>He</b>rld!</p> 
    <b>llo</b> wo
  • 如果不想把范围从文档中移除,也可以使用cloneContents()创建一个副本,然后把这个副本插入到文档其他地方。

5、范围插入

  • insertNode():方法可以在范围选区的开始位置插人一个节点。
    1
    <p id="p1"><b>Hello</b> world!</p> 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let p1 = document.getElementById("p1"), 
    helloNode = p1.firstChild.firstChild,
    worldNode = p1.lastChild,
    range = document.createRange();
    range.setStart(helloNode, 2);
    range.setEnd(worldNode, 3);
    let span = document.createElement("span");
    span.style.color = "red";
    span.appendChild(document.createTextNode("Inserted text"));
    range.insertNode(span);
    1
    <p id="p1"><b>He<span style="color: red">Inserted text</span>llo</b> world</p>
  • surroundContents():方法插入包含范围的内容。
    • 接收一个参数:包含范围内容的节点。

6、范围折叠

  • 如果范围并没有选择文档的任何部分,则称为折叠( collapsed )。
  • 折叠范围有点类似文本框:如果文本框中有文本,那么可以用鼠标选中以高亮显示全部文本。这时候,如果再单击鼠标,则选区会被移除,光标会落在某两个字符中间。
  • 在折叠范围时,位置会被设置为范围与文档交界的地方,可能是范围选区的开始处,也可能是结尾处。
  • collapse()方法
    • 接收一个参数:布尔值,表示折叠到范围哪一段。
    • true表示折叠到起点;false表示折叠到终点。
      1
      2
      3
      4
      5
      6
      let p1 = document.getElementById("p1"), 
      p2 = document.getElementById("p2"),
      range = document.createRange();
      range.setStartAfter(p1);
      range.setStartBefore(p2);
      console.log(range.collapsed); // true

7、范围比较

  • compareBoundaryPoints()方法确定范围之间是否存在公共的边界。
    • 参数:接收两个参数,要比较的范围和以恶常量值,表示比较的方式。
      • Range.START_TO_START(0),比较两个范围的起点;
      • Range.START_TO_END(1),比较第一个范围的起点和第二个范围的终点;
      • Range.END_TO_END(2),比较两个范围的终点;
      • Range.END_TO_START (3),比较第一个范围的终点和第二个范围的起点。
    • 返回值:
      • 在第一个范围的边界点位于第二个范围的边界点之前时返回-1;
      • 在两个范围的边界点相等时返回0;
      • 在第一个范围的边界点位于第二个范围的边界点之后时返回1。
        1
        <p id="p1"><b>Hello</b> world!</p> 
        1
        2
        3
        4
        5
        6
        7
        8
        let range1 = document.createRange(); 
        let range2 = document.createRange();
        let 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

8、复制范围

  • cloneRange()方法可以复制范围,会创建调用它的范围的副本。
  • 新范围包含与原始范围一样的属性,修改其边界点不会影响原始范围。

9、清理

  • detach()方法把范围从创建它的文档中剥离。