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

来源:互联网 发布:mac自带翻译软件 编辑:程序博客网 时间:2024/05/19 03:29

  • DOM2 和 DOM3
    • 遍历
      • NodeIterator
      • TreeWalker

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_ALL 4294967295(32个1) 显示所有类型的节点 NodeFilter.SHOW_ELEMENT 1(第一个1) 显示元素节点 NodeFilter.SHOW_ATTRIBUTE 2(第二个个1) 显示特性节点,由于DOM结构原因,实际上不能使用这个值,用了也没效果。 NodeFilter.SHOW_TEXT 4 显示文本节点 NodeFilter.SHOW_CDATA_SECTION 8 显示CDATA节点。对HTML无效(因为没有这样的节点,在XML中存在) NodeFilter.SHOW_ENTITY_REFERENCE 16 显示实体引用节点。对HTML无效 NodeFilter.SHOW_ENTITY 32 显示实体节点。对HTML无效 NodeFilter.SHOW_PROCESSING_INSTRUCTION 64 显示处理指令节点。对HTML无效 NodeFilter.SHOW_COMMENT 128 显示注释节点 NodeFilter.SHOW_DOCUMENT 256 显示文档节点 NodeFilter.SHOW_DOCUMENT_TYPE 512 显示文档类型节点 NodeFilter.SHOW_DOCUMENT_FRAGMENT 1024 显示文档片段节点。对HTML无效 NodeFilter.SHOW_NOTATION 2048 显示符号节点。对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>运行结果如下:-----------------------------------test11test111test2test21test22test3test31test32brotherscript!
  • 通过结果可以看出,即使一开始遍历的根节点是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>但是结果没有变化。。。-----------------------------------test1test11test111test2test12test112test111test11test1
  • 后来我修改了一下函数,从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的子节点,那么除了改变当前节点位置外对遍历结果不会有任何影响。这一点从刚才的实验也可以反映。因为你哪怕改到外面的节点去了,最终要是又回到了一开始设置的节点集里,又会回归“正常”。

0 0