第十二章:DOM2 和 DOM3(遍历)

DOM2 和 DOM3

遍历

  • DOM Level 2 Tranversal and Range 模块定义了2个用于辅助完成顺序遍历DOM结构的类型:NodeIteratorTreeWalker。这两个类型能够基于给定的起点对DOM结构执行深度优先的遍历操作。IE不支持DOM遍历(然而IE9+却支持,我遇到书中好多地方说IE不支持,难道这部分的知识的编写时期还没有出来IE9?)。使用下面代码可以检测浏览器对DOM2 级遍历能力的支持情况:
    var supportsTraversals = document.implementation.hasFeature("Traversal", "2.0");
    var supportsNodeIterator = (typeof document.createNodeIterator == "function");
    var supportsTreeWalker = (typeof document.createTreeWalker == "function");
    alert(supportsTraversals);//IE 8- false 
    alert(supportsNodeIterator);//IE 8- false
    alert(supportsTreeWalker);//IE 8- false

NodeIterator

  • 可以使用document.createNodeIterator()方法创建新实例,这个方法接收4个参数:
    1. root:搜索起点
    2. whatToShow:表示要访问哪些类型节点的数字代码
    3. filter:是一个NodeFilter对象,或者一个表示过滤哪些特殊节点的函数。
    4. entityReferenceExpansion:布尔值,表示是否要扩展实体引用。这个参数在HTML中没有用,因为其中的实体引用不能扩展。
  • 其中whatToShow是一个位掩码,通过应用一或多个过滤器来确定要访问哪些节点。这个参数的值以常量形式在NodeFilter(IE8-不支持)类型中定义,如下所示:
访问方式描述
NodeFilter.SHOW_ALL4294967295(32个1)显示所有类型的节点
NodeFilter.SHOW_ELEMENT1(第一个1)显示元素节点
NodeFilter.SHOW_ATTRIBUTE2(第二个个1)显示特性节点,由于DOM结构原因,实际上不能使用这个值,用了也没效果。
NodeFilter.SHOW_TEXT4显示文本节点
NodeFilter.SHOW_CDATA_SECTION8显示CDATA节点。对HTML无效(因为没有这样的节点,在XML中存在)
NodeFilter.SHOW_ENTITY_REFERENCE16显示实体引用节点。对HTML无效
NodeFilter.SHOW_ENTITY32显示实体节点。对HTML无效
NodeFilter.SHOW_PROCESSING_INSTRUCTION64显示处理指令节点。对HTML无效
NodeFilter.SHOW_COMMENT128显示注释节点
NodeFilter.SHOW_DOCUMENT256显示文档节点
NodeFilter.SHOW_DOCUMENT_TYPE512显示文档类型节点
NodeFilter.SHOW_DOCUMENT_FRAGMENT1024显示文档片段节点。对HTML无效
NodeFilter.SHOW_NOTATION2048显示符号节点。对HTML无效
  • 一般我们不会在意具体的值是多少,比如我们只显示元素和文本节点,可以使用:NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT 作为第二个参数。另外可以发现每个参量代表的具体的值是2^(nodeType - 1)
    console.log(NodeFilter.SHOW_ALL);//4294967295 32个1
    console.log(NodeFilter.SHOW_ELEMENT);//1    2^(1-1)
    console.log(NodeFilter.SHOW_ATTRIBUTE);//2    2^(2-1)
    console.log(NodeFilter.SHOW_TEXT);//4     2^(3-1)
    console.log(NodeFilter.SHOW_CDATA_SECTION);//8    2^(4-1)
    console.log(NodeFilter.SHOW_ENTITY_REFERENCE);//16    2^(5-1)
    console.log(NodeFilter.SHOW_ENTITY);//32    2^(6-1)
    console.log(NodeFilter.SHOW_PROCESSING_INSTRUCTION);//64
    console.log(NodeFilter.SHOW_COMMENT);//128
    console.log(NodeFilter.SHOW_DOCUMENT);//256
    console.log(NodeFilter.SHOW_DOCUMENT_TYPE);//512
    console.log(NodeFilter.SHOW_DOCUMENT_FRAGMENT);//1024
    console.log(NodeFilter.SHOW_NOTATION);//2048 2^(12-1)
  • filter参数可以是一个NodeFilter对象,也可以是一个函数。如果是前者,每个NodeFilter对象只有一个一个方法,即acceptNode(),我们需要设置这个方法。该方法有3个返回值(可以查看我上面的快捷链接的介绍):FILTER_ACCEPTFILTER_REJECTFILTER_SKIP(他们的值分别是1、2、3)。对于createNodeIterator()来说,如果应该返回给定的节点,则acceptNode()需要返回FILTER_ACCEPT否则就返回FILTER_SKIP(也可以使用FILTER_REJECT,效果一样)。见下面的例子:
    var filter = {
        acceptNode: function (node) {
            return node.tagName.toLowerCase() == "p" ?
                    NodeFilter.FILTER_ACCEPT:
                    NodeFilter.FILTER_SKIP;
        }
    };

    //几乎没有区别,也需要返回NodeFilter.FILTER_ACCEPT或者NodeFilter.FILTER_SKIP
    var filter2 =  function (node) {
        return node.tagName.toLowerCase() == "p" ?
                NodeFilter.FILTER_ACCEPT:
                NodeFilter.FILTER_SKIP;
    }
  • NodeIterator类型的主要两个方法是nextNode()和previousNode()。看看名字就知道是什么用处,直接上例子:
<!DOCTYPE html>
<html>
    <head>
        <title>NodeIterator Example</title>
        <script type="text/javascript">
            var filter = function (node) {
                return node.tagName.toLowerCase() == "li"?
                    NodeFilter.FILTER_ACCEPT:
                        NodeFilter.FILTER_SKIP;
            };
            function makeList() {
                var div = document.getElementById("div1");
                var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter, false);
                var output = document.getElementById("text1");
                output.value = "";
                //这种获取行为是动态的,nextNode()只会返回最新状态,而不是createNodeIterator时的状态
                var node = iterator.nextNode();
                while (node !== null) {
                    output.value += node.tagName + "\n";
                    node = iterator.nextNode();
                }
            }
        </script>
    </head>
    <body>
        <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>
        <textarea rows="10" cols="40" id="text1"></textarea><br />
        <input type="button" value="Make List" onclick="makeList()" />
        <input type="button" value="changeFilter" onclick="filter = null;" />
    </body>
</html>
  • 虽然说挺强大的,但是我觉得用处可能没有那么广,因为我们通常只会遍历一层子节点,所以通过childNodes,再通过nextSibling和previousSibling可能会更实用一点。这个方法更多用到’深入骨髓’的那种遍历,比如说我要获取文档下的a节点和div节点的集合。虽然他们各自可以通过getElementsByTagName获得,但是要按照顺序获得他们的集合,就没有那么容易写了,下面是我想到的最普通的深度优先遍历的写法。
<html>
<body>
<div id="test">
    <a href="#"></a>
    <div>
        <a href="#"></a>
    </div>
    <h1></h1>
    <h2></h2>
    <div>
        <div>
            <a href="#"></a>
        </div>
        <a href="#"></a>
    </div>
</div>
<input type="button" value="test" onclick="console.log(getElements(document.getElementById('test'),filter));">
<script>
    function filter(node) {
        //我随便写了一个过滤器。
        var name = node.tagName.toLowerCase();
        return name == "a" || name == "div";
    }
    //自定义的一种遍历,我决定用递归去实现。
    function getElements(root, filter) {
        var arr = [];//用数组保存,说明我这个方法的缺点是非动态的。
        if (root) {
            if (filter(root)) {
                arr.push(root);
            }
            var children = root.children;//这里用children不用childNodes是为了过滤文本节点。只是测试
            for (var i=0, len=children.length; i<len; i++) {
                arr.push.apply(arr, getElements(children[i], filter));
            }
        }
        return arr;
    }
</script>
</body>
</html>

TreeWalker

  • TreeWalkerNodeIterator的一个更高级的版本。除了包括nextNode()和previousNode()在内的相同功能外,这个类型还提供了用于不同方向上遍历DOM结构的方法。
    1. parentNode():遍历到当前节点的父节点。
    2. firstChild():遍历到当前节点的第一个子节点。
    3. lastChild():遍历到当前节点的最后一个子节点。
    4. nextSibling():遍历到当前节点的下一个兄弟节点。
    5. previousSibling():遍历到当前节点的上一个兄弟节点。
  • 创建TreeWalker对象要使用document.createTreeWalker()方法,这个方法同样接收4个参数。用法和document.createNodeIterator()类似。还记得前面说到的filter参数的返回值吗?前面只提到了FILTER_ACCEPTFILTER_SKIP,至于FILTER_REJECT的用法在createNodeIterator()中与FILTER_SKIP相同,但是在createTreeWalker()中,则会跳过相应节点及该节点的整个子树
  • TreeWalker类型还有一个属性,名叫currentNode。顾名思义,表示任何遍历方法在上一次遍历中返回的节点。通过设置这个属性也可以修改遍历继续进行的起点,例子如下:
    var node = walker.nextNode();
    alert(node === walker.currentNode);//true
    walker.currentNode = document.body;//修改起点
  • 这种修改遍历起点的能力引起了我的兴趣。假设我修改的节点并不是一开始root的子节点,那么是否还会遍历修改后的节点的兄弟节点?如果是子节点又会如何?接下来做一个实验:
<html>
<body>
<div id="father">
    <div id="test1">
        <div id="test11">
            <div id="test111"></div>
            <div id="test112"></div>
        </div>
        <div id="test12"></div>
    </div>
    <div id="test2">
        <div id="test21"></div>
        <div id="test22"></div>
    </div>
    <div id="test3">
        <div id="test31"></div>
        <div id="test32"></div>
    </div>
</div>
<div id="brother"></div>
<script>
    var node = document.getElementById("test1");
    var walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
    var currentNode = walker.nextNode();
    while (currentNode != null) {
        if (currentNode.tagName.toLowerCase() == "script") {
            console.log("script!");
        } else {
            console.log(currentNode.id);
        }
        if (currentNode.id == "test111") {
            walker.currentNode = document.getElementById("test2");
            currentNode = walker.currentNode;
            continue;
        }
        currentNode = walker.nextNode();
    }
</script>
</body>
</html>

运行结果如下:
-----------------------------------
test11
test111
test2
test21
test22
test3
test31
test32
brother
script!
  • 通过结果可以看出,即使一开始遍历的根节点是test1,在修改了currentNode后,会认为是从当前文档下已经遍历到修改后的节点,继续遍历会认为是当前文档下遍历的延续。不过这只是我的猜测,我又进行了下面的测试:
<html>
<body>
<div id="father">
    <div id="test1">
        <div id="test11">
            <div id="test111"></div>
            <div id="test112"></div>
        </div>
        <div id="test12"></div>
    </div>
    <div id="test2">
        <div id="test21"></div>
        <div id="test22"></div>
    </div>
    <div id="test3">
        <div id="test31"></div>
        <div id="test32"></div>
    </div>
</div>
<div id="brother"></div>
<script>
    var node = document.getElementById("test1");
    var walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
    var currentNode = walker.currentNode;//即root节点
    var flag = true;
    while (currentNode != null) {
        if (currentNode.tagName.toLowerCase() == "script") {
            console.log("script!");
        } else {
            console.log(currentNode.id);
        }
        if (currentNode.id == "test111" && flag) {
            walker.currentNode = document.getElementById("test2");
            currentNode = walker.currentNode;
            flag = false;
            continue;
        }
        if (flag) currentNode = walker.nextNode();
        else currentNode = walker.previousNode();
    }
</script>
</body>
</html>

结果如下:
--------------------------------------
 test1
 test11
 test111
 test2
 test12
 test112
 test111
 test11
 test1
  • 这个例子在遍历到test111节点后,立马将当前节点改为test2,且向前遍历,这个结果就很有意思了。按照我的猜测他应该会把father节点也一同遍历到,但结果并不是如此,难道只会遍历到第一个兄弟节点
    <div id="father">
    <div id="test0">
        <div id="test01"></div>
        <div id="test02"></div>
    </div>
    <div id="test1">
        <div id="test11">
            <div id="test111"></div>
            <div id="test112"></div>
        </div>
        <div id="test12"></div>
    </div>
    <div id="test2">
        <div id="test21"></div>
        <div id="test22"></div>
    </div>
    <div id="test3">
        <div id="test31"></div>
        <div id="test32"></div>
    </div>
</div>

但是结果没有变化。。。
-----------------------------------
test1
test11
test111
test2
test12
test112
test111
test11
test1
  • 后来我修改了一下函数,从test2开始遍历,搜素到test22就将test3设为当前节点,结果显示,最终只会向前遍历到test2节点。可见向前遍历只会遍历到一开始设置的root节点。那就有意思了,我在想如果我最开始的例子是先遍历test3再跳到test2向前或向后遍历,结果是不是截然相反
<html>
<body>
<div id="father">
    <div id="test0">
        <div id="test01"></div>
        <div id="test02"></div>
    </div>
    <div id="test1">
        <div id="test11">
            <div id="test111"></div>
            <div id="test112"></div>
        </div>
        <div id="test12"></div>
    </div>
    <div id="test2">
        <div id="test21"></div>
        <div id="test22"></div>
    </div>
    <div id="test3">
        <div id="test31"></div>
        <div id="test32"></div>
    </div>
</div>
<div id="brother"></div>
<script>
    var node = document.getElementById("test3");
    var walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
    var currentNode = walker.currentNode;//即root节点
    var flag = true;
    while (currentNode != null) {
        if (currentNode.tagName.toLowerCase() == "script") {
            console.log("script!");
        } else {
            console.log(currentNode.id);
        }
        if (currentNode.id == "test32" && flag) {
            walker.currentNode = document.getElementById("test2");
            currentNode = walker.currentNode;
            flag = false;
            continue;
        }
        if (flag) currentNode = walker.nextNode();
        else currentNode = walker.previousNode();
    }
</script>
</body>
</html>
  • 结果真如我所想,不仅连father输出来了,就连body head html都出来了(只不过没有id没有打印)。我再做一个向后的遍历看是不是到test3就结束了:
    var node = document.getElementById("test3");
    var walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
    var currentNode = walker.currentNode;//即root节点
    var flag = true;
    while (currentNode != null) {
        if (currentNode.tagName.toLowerCase() == "script") {
            console.log("script!");
        } else {
            console.log(currentNode.id);
        }
        if (currentNode.id == "test32" && flag) {
            walker.currentNode = document.getElementById("test2");
            currentNode = walker.currentNode;
            flag = false;
            continue;
        }
       currentNode = walker.nextNode();
    }
  • 结果也的确到test32就终止了。那我总结一下是这样的:当改变currentNode后遍历又遇到了之前createTreeWalker的root节点,则又会像一开始一样进行工作,否则就会以document为根节点反映一些结果。不过我的这个总结还需要靠一个例子去证明。因为之前只用到了nextNode()previousNode()。而没有用他的其他方法。所以我决定再做几个实验:

    1. 检验nextSibling()previousSibling()类似):设置test0 1 2 3一共4个兄弟节点,一开始用test2创建walker。遍历完test2跳到test0节点,然后去搜索nextSibling(),看是否能搜索到test3
    2. 检验parentNode():设置test0 1 2 3一共4个兄弟节点,一开始用test2创建walker。遍历完test2跳到test0节点,遍历完test0后回到test21请求parentNode()看是否会搜索到father
  • 代码我就不给了,第一个结果的确是到test2就终止了。第二个的结果也是如我所愿遍历到test2也终止了。可见我的猜想的确是正确的。不过前面还忘记说一件事。如果修改当前节点仍然是一开始指定的root的子节点,那么除了改变当前节点位置外对遍历结果不会有任何影响。这一点从刚才的实验也可以反映。因为你哪怕改到外面的节点去了,最终要是又回到了一开始设置的节点集里,又会回归“正常”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值