不知道起什么名好 發表於 2025-8-2 12:07:00

学习 TreeWalker api 并与普通遍历 DOM 方式进行比较

<h4 id="介绍-treewalker">介绍 TreeWalker</h4>
<p><code>TreeWalker</code> 是 <code>JavaScript</code> 中用于遍历 <code>DOM</code> 树的一个接口。允许你以灵活的方式在 <code>DOM</code> 树中进行前向和后向遍历,包括访问父节点、子节点和兄弟节点。适用于处理复杂的 <code>DOM</code> 操作:在遍历过程中进行添加、删除或修改节点的操作,并继续遍历。</p>
<p>与普通的 <code>for 循环 + querySelector</code> 相比灵活性更高。执行速度方面,在 <code>DOM</code> 超过一定复杂度的情况下,<code>TreeWalker</code> 更快,后面会举例说明。</p>
<h4 id="实践">实践</h4>
<h5 id="创建-treewalker">创建 TreeWalker</h5>
<p>可以使用 <code>document.createTreeWalker</code> 方法来创建一个 <code>TreeWalker</code> 对象。这个方法接受四个参数:</p>
<ul>
<li><code>root</code>:要遍历的根节点。</li>
<li><code>whatToShow</code>(可选):一个整数,表示要显示的节点类型。默认值是<code>NodeFilter.SHOW_ALL</code>,表示显示所有节点。</li>
<li><code>filter</code>(可选):一个 <code>NodeFilter</code> 对象,用于自定义过滤逻辑。</li>
<li><code>entityReferenceExpansion</code>(可选):一个布尔值,表示是否展开实体引用。这个参数在现代浏览器中通常被忽略,因为实体引用在 <code>HTML</code> 中很少使用</li>
</ul>
<pre><code class="language-js">const walker = document.createTreeWalker(
    document.body,//.root
    NodeFilter.SHOW_ELEMENT,// whatToShow(可选)
    null,// filter(可选)
    false //entityReferenceExpansion(可选)
)
</code></pre>
<p><code>NodeFilter.SHOW_ELEMENT</code> 表示显示元素节点。</p>
<h5 id="节点类型">节点类型</h5>
<p><code>NodeFilter</code> 有 12 种节点类型,和 <code>Node</code> 接口的节点类型一一对应;</p>
<table>
<thead>
<tr>
<th>NodeFilter</th>
<th>Node.prototype</th>
</tr>
</thead>
<tbody>
<tr>
<td>SHOW_ELEMENT:显示元素节点。</td>
<td>1: ELEMENT_NODE</td>
</tr>
<tr>
<td>SHOW_ATTRIBUTE:显示属性节点(在HTML 中不常用)。</td>
<td>2: ATTRIBUTE_NODE</td>
</tr>
<tr>
<td>SHOW_TEXT:显示文本节点。</td>
<td>3:TEXT_NODE</td>
</tr>
<tr>
<td>SHOW_CDATA_SECTION:显示CDATA 节点(在HTML 中不常用)。</td>
<td>4:CDATA_SECTION_NODE</td>
</tr>
<tr>
<td>SHOW_ENTITY_REFERENCE:显示实体引用节点(在HTML 中不常用)。</td>
<td>5: ENTITY_REFERENCE_NODE</td>
</tr>
<tr>
<td>SHOW_ENTITY:显示实体节点(在HTML 中不常用)。</td>
<td>6 : ENTITY_NODE</td>
</tr>
<tr>
<td>SHOW_PROCESSING_INSTRUCTION:显示处理指令节点。</td>
<td>7: PROCESSING_INSTRUCTION_NODE</td>
</tr>
<tr>
<td>SHOW_COMMENT:显示注释节点。</td>
<td>8:COMMENT_NODE</td>
</tr>
<tr>
<td>SHOW_DOCUMENT:显示文档节点。</td>
<td>9:DOCUMENT_NODE</td>
</tr>
<tr>
<td>SHOW_DOCUMENT_TYPE:显示文档类型节点。</td>
<td>10: DOCUMENT_TYPE_NODE</td>
</tr>
<tr>
<td>SHOW_DOCUMENT_FRAGMENT:显示文档片段节点。</td>
<td>11 : DOCUMENT_FRAGMENT_NODE</td>
</tr>
<tr>
<td>SHOW_NOTATION:显示符号节点(在HTML 中不常用)。</td>
<td>12 : NOTATION_NDE</td>
</tr>
</tbody>
</table>
<p><code>NodeFilter.SHOW_ALL</code> 表示显示所有类型节点,这和遍历节点的 <code>childNodes</code> 一样,<code>childNodes</code> 会把该节点下的所有类型的子节点遍历出来。而节点的 <code>children</code> 就只遍历元素节点。</p>
<h5 id="自定义过滤器">自定义过滤器</h5>
<p>可以通过传递一个 <code>NodeFilter</code> 对象来实现自定义的过滤逻辑。NodeFilter 对象有一个 acceptNode 方法,该方法返回一个常量来决定是否接受节点:</p>
<ul>
<li>NodeFilter.FILTER_ACCEPT:接受节点。</li>
<li>NodeFilter.FILTER_REJECT:拒绝节点及其子节点。</li>
<li>NodeFilter.FILTER_SKIP:跳过节点,但继续遍历其子节点。</li>
</ul>
<pre><code class="language-js">const filter = {
    acceptNode: function (node){
      if (node.tagName=== "DIV"){
            return NodeFilter.FILTER_ACCEPT;
      }else {
            return NodeFilter.FILTER_SKIP;
      }
    },
};

const walker = document.createTreeWalker(
    document.body,
    NodeFilter.SHOW_ELEMENT,
    filter,//只遍历标签名是div的元素
    false
);
let node;
while ((node = walker.nextNode())!== nu11){
console.log(node);
</code></pre>
<h5 id="遍历节点">遍历节点</h5>
<p><code>TreeWalker</code> 提供了多种方法来遍历节点:</p>
<p>nextNode():移动到下一个节点,如果没有更多节点则返回 null。</p>
<ul>
<li>previousNode():移动到上一个节点,如果没有更多节点则返回 null。</li>
<li>parentNode():移动到当前节点的父节点,如果没有父节点则返回 null。</li>
<li>firstChild():移动到当前节点的第一个子节点,如果没有子节点则返回 null。</li>
<li>lastChild():移动到当前节点的最后一个子节点,如果没有子节点则返回 null。</li>
<li>nextSibling():移动到当前节点的下一个兄弟节点,如果没有更多兄弟节点则返回 null。</li>
<li>previousSibling():移动到当前节点的上一个兄弟节点,如果没有更多兄弟节点则返回 null。</li>
</ul>
<p>需要注意的是,<strong><code>nextNode()</code>是深度优先遍历</strong>。</p>
<h5 id="当前节点">当前节点</h5>
<p>TreeWalker 对象有一个 currentNode 属性,表示当前遍历到的节点,这个属性是可读写的,可以通过这个属性来获取或设置当前节点。</p>
<pre><code class="language-js">console.log(walker.currentNode);// 输出当前节点
//设置当前节点
walker.currentNode = document.getElementById("id");
console.log(walker.currentNode);//输出新设置的当前节点
</code></pre>
<h5 id="实践并和-queryselector-比较">实践并和 querySelector() 比较</h5>
<p><code>querySelector()</code> 是一个选择器通过传入静态的 css 选择器获取元素。</p>
<p>而 TreeWalker 会创建一个对象,适应于进行复杂的 DOM 操作的场景,在遍历过程中支持添加、删除或修改节点,或者动态改变遍历方向,很灵活。</p>
<p>这两个本来就是适用于不同场景,获取元素基本上还是用querySelector(),不过涉及到复杂循环遍历时就可以考虑 TreeWalker 了。</p>
<p>这里我测试了一下,在怎样的复杂程度下,TreeWalker 遍历 会比用 for 循环 + querySelector() 遍历执行速度上更快。</p>
<p>经过不断测试,在循环嵌套遍历 1000 个元素时,并且对每个元素进行添加删除子元素的操作,此时使用 TreeWalker 遍历执行速度更快。这 1000 个数量并不是一个可以判定复杂程度确定的值,只是在当前浏览器下测试出来的一个大概的数量。<br>
因为这还与对元素操作复杂度有关,与浏览器执行性能也有关,随着浏览器不断更新迭代,后面应该只会越来越快。</p>
<p>下面整理下测试过程,在页面中创建了一个 id是root的元素</p>
<pre><code class="language-html">&lt;div id="root"&gt;&lt;/div&gt;
&lt;style&gt;
    #root&gt;div{margin-bottom: 20px;}
    .ColDiv{display: flex;gap: 10px;}
&lt;/style&gt;
</code></pre>
<p>然后给 root 创建1000个子元素,这里使用了三重 for 循环js</p>
<pre><code class="language-js">function createEl(el) {
var fragment = document.createDocumentFragment();
for (var i = 0; i &lt; 10; i++) {
    var divBox = document.createElement("div");
    var innerHTML = `Row${i}`;
    for (let j = 0; j &lt; 10; j++) {
      innerHTML += `&lt;div&gt;&lt;div class="ColDiv"&gt;Col${j}=&gt;`;
      for (let k = 0; k &lt; 10; k++) {
      innerHTML += `&lt;div&gt;children${k}&lt;/div&gt;`;
      }
      innerHTML += `&lt;/div&gt;`;
    }
    divBox.innerHTML = innerHTML;
    fragment.appendChild(divBox);
    el.appendChild(fragment);
}
}
createEl(document.getElementById("root"));
</code></pre>
<p>渲染到页面上就是这样,截图没有全部截完:<br>
<img src="https://img2024.cnblogs.com/blog/895887/202508/895887-20250802114722975-1335998151.png" alt="image" loading="lazy"></p>
<p>然后用<code>循环 + querySelector</code> 遍历 <code>root</code>,这里为了让遍历复杂一点,添加了一个逻辑:当遍历到子节点是 <code>children2</code> 时,<br>
给这个节点添加一个新的子节点,然后又删除它;最后计算执行时间;</p>
<pre><code class="language-js">const querySelectorTest = () =&gt; {
    let root = document.querySelector("#root");
    let children = root.children;
    let len = children.length;
    console.time("querySelector");
    const tempFn = (list) =&gt; {
      for (let i = 0; i &lt; list.length; i++) {
            let node = list;
            if (node.textContent==="children2") {
                //添加一个新的子节点
                const newDiv = document.createElement("div");
                newDiv.textContent = "New Item";
                node.appendChild(newDiv);
                console.log("Added new node:");
                //删除添加的子节点
                node.removeChild(newDiv);
            }
            if (node.children.length) {
                tempFn(node.children);
            }
      }
    }
    tempFn(children);
    console.timeEnd("querySelector");
}
</code></pre>
<p>然后同样的逻辑,用 <code>TreeWalker</code> 来遍历</p>
<pre><code class="language-js">const TreeWalkerTest = () =&gt; {
    const walker = document.createTreeWalker(
      document.getElementById("root"),
      NodeFilter.SHOW_ELEMENT,
      null,
      false
    );
    console.time("treeWalker");
    let node;
    while ((node = walker.nextNode()) !== null) {
      if (node.textContent === "children2") {
            //添加一个新的子节点
            const newDiv = document.createElement("div");
            newDiv.textContent = "New Item";
            node.appendChild(newDiv);
            //移动到新添加的节点
            let newNode = walker.nextNode();
            console.log("Added new node:");
            //删除一个节点前需要先移动到上一个节点 walker.previousNode(),这样才能顺利遍历下一个;
            walker.previousNode();
            newNode.parentNode.removeChild(newNode);
      }
    }
    console.timeEnd("treeWalker");
}
</code></pre>
<p>这里需要注意的是,删除一个节点前需要先移动到上一个节点 walker.previousNode(),这样才能顺利遍历下一个;</p>
<p>同时测试这两个函数</p>
<pre><code class="language-js">for (let i = 0; i &lt; 10; i++) {
TreeWalkerTest()
querySelectorTest()
}
</code></pre>
<p>结果如下:<br>
<img src="https://img2024.cnblogs.com/blog/895887/202508/895887-20250802114709301-1320609165.png" alt="image-1" loading="lazy"></p>
<p>可以看到多次运行测试函数,<code>TreeWalker</code> 执行速度大多数都更快;</p>
<p>然后修改 root 子元素数量试试,从1000改为100,测试函数的逻辑不变;</p>
<pre><code class="language-js">function createEl(el) {
var fragment = document.createDocumentFragment();
// for (var i = 0; i &lt; 10; i++) {
    var divBox = document.createElement("div");
    var innerHTML = `Row`;
    for (let j = 0; j &lt; 10; j++) {
      innerHTML += `&lt;div&gt;&lt;div class="ColDiv"&gt;Col${j}=&gt;`;
      for (let k = 0; k &lt; 10; k++) {
            innerHTML += `&lt;div&gt;children${k}&lt;/div&gt;`;
      }
      innerHTML += `&lt;/div&gt;`;
    }
    divBox.innerHTML = innerHTML;
    fragment.appendChild(divBox);
    el.appendChild(fragment);
// }
}
</code></pre>
<p>再来测试下:<br>
<img src="https://img2024.cnblogs.com/blog/895887/202508/895887-20250802114659630-278318889.png" alt="image-2" loading="lazy"></p>
<p><code>TreeWalker</code> 执行速度依然大多数都更快;</p>
<p>再来修改下测试函数的逻辑,只遍历,不进行添加删除节点的操作</p>
<pre><code class="language-js">const querySelectorTest = () =&gt; {
    let root = document.querySelector("#root");
    let children = root.children;
    let len = children.length;
    console.time("querySelector");
    const tempFn = (list) =&gt; {
      for (let i = 0; i &lt; list.length; i++) {
            let node = list;
            // if (node.textContent === "children2") {
            //         //添加一个新的子节点
            //         const newDiv = document.createElement("div");
            //         newDiv.textContent = "New Item";
            //         node.appendChild(newDiv);
            //         // console.log("Added new node:");
            //         //删除添加的子节点
            //         node.removeChild(newDiv);
            // }
            if (node.children.length) {
                  tempFn(node.children);
            }
      }
    }
    tempFn(children);
    console.timeEnd("querySelector");
}

const TreeWalkerTest = () =&gt; {
        const walker = document.createTreeWalker(
                document.getElementById("root"),
                NodeFilter.SHOW_ELEMENT,
                null,
                false
        );
        console.time("treeWalker");
        let node;
        while ((node = walker.nextNode()) !== null) {
                // if (node.textContent === "children2") {
                //         //添加一个新的子节点
                //         const newDiv = document.createElement("div");
                //         newDiv.textContent = "New Item";
                //         node.appendChild(newDiv);
                //         //移动到新添加的节点
                //         let newNode = walker.nextNode();
                //         // console.log("Added new node:");
                //         //删除一个节点
                //         walker.previousNode();
                //         newNode.parentNode.removeChild(newNode);
                // }
        }
        console.timeEnd("treeWalker");
}

for (let i = 0; i &lt; 10; i++) {
        TreeWalkerTest()
        querySelectorTest()
}
</code></pre>
<p>结果如下:<br>
<img src="https://img2024.cnblogs.com/blog/895887/202508/895887-20250802114634750-658360323.png" alt="image-3" loading="lazy"></p>
<p><code>TreeWalker</code> 执行速度还是大多数都更快;</p>
<p>但其实这里测试意义不大了,这个例子实际上是在测试 <code>while 循坏</code> 和 <code>for 循环+递归</code> 的差异了;单论循环而言, <code>while 循环</code>总是最快的;</p>
<p>那么接下来把 root 子节点打平,不再嵌套了,也就是遍历一维数组,然后把 querySelectorTest 的 for 循环改为 while 循环,再来试一下</p>
<pre><code class="language-js">function createEl(el, len) {
        var fragment = document.createDocumentFragment();
        for (var i = 0; i &lt; 10000; i++) {
                var divBox = document.createElement("div");
                divBox.innerHTML = "Row" + i;
                fragment.appendChild(divBox);
        }
        el.appendChild(fragment);
}
const TreeWalkerTest = () =&gt; {
        const walker = document.createTreeWalker(
                document.getElementById("root"),
                NodeFilter.SHOW_ELEMENT,
                null,
                false
        );
        console.time("treeWalker");
        let node;
        while ((node = walker.nextNode()) !== null) {}
        console.timeEnd("treeWalker");
};
const querySelectorTest = () =&gt; {
        let root = document.querySelector("#root");
        let children = root.children;
        let len = children.length;
        console.time("querySelector");
        let i = 0;
        while (i++ &lt; len) { }
        console.timeEnd("querySelector");
}
</code></pre>
<p>这样就是普通的两个 <code>while 循环</code>对比了,此时 <code>TreeWalker</code> 就没有优势了。</p>
<p><img src="https://img2024.cnblogs.com/blog/895887/202508/895887-20250802114620206-1609991253.png" alt="image-4" loading="lazy"></p>
<p>总结起来,在不复杂的场景下,遍历的元素数量不多或者嵌套层级不深,或者对遍历的元素没有进行复杂的<code>DOM</code>操作,使用普通 <code>for 循环,while 循环</code>操作元素始终比 <code>TreeWalker</code> 快,<br>
反之可以考虑使用 <code>TreeWalker</code>。</p><br><br>
来源:https://www.cnblogs.com/zsxblog/p/19018674
頁: [1]
查看完整版本: 学习 TreeWalker api 并与普通遍历 DOM 方式进行比较