找回密码
 立即注册
首页 业界区 业界 学习 TreeWalker api 并与普通遍历 DOM 方式进行比较 ...

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

向梦桐 10 小时前
介绍 TreeWalker

TreeWalker 是 JavaScript 中用于遍历 DOM 树的一个接口。允许你以灵活的方式在 DOM 树中进行前向和后向遍历,包括访问父节点、子节点和兄弟节点。适用于处理复杂的 DOM 操作:在遍历过程中进行添加、删除或修改节点的操作,并继续遍历。
与普通的 for 循环 + querySelector 相比灵活性更高。执行速度方面,在 DOM 超过一定复杂度的情况下,TreeWalker 更快,后面会举例说明。
实践

创建 TreeWalker

可以使用 document.createTreeWalker 方法来创建一个 TreeWalker 对象。这个方法接受四个参数:

  • root:要遍历的根节点。
  • whatToShow(可选):一个整数,表示要显示的节点类型。默认值是NodeFilter.SHOW_ALL,表示显示所有节点。
  • filter(可选):一个 NodeFilter 对象,用于自定义过滤逻辑。
  • entityReferenceExpansion(可选):一个布尔值,表示是否展开实体引用。这个参数在现代浏览器中通常被忽略,因为实体引用在 HTML 中很少使用
  1. const walker = document.createTreeWalker(
  2.     document.body,//.root
  3.     NodeFilter.SHOW_ELEMENT,// whatToShow(可选)
  4.     null,// filter(可选)
  5.     false //entityReferenceExpansion(可选)
  6. )
复制代码
NodeFilter.SHOW_ELEMENT 表示显示元素节点。
节点类型

NodeFilter 有 12 种节点类型,和 Node 接口的节点类型一一对应;
NodeFilterNode.prototypeSHOW_ELEMENT:显示元素节点。1: ELEMENT_NODESHOW_ATTRIBUTE:显示属性节点(在HTML 中不常用)。2: ATTRIBUTE_NODESHOW_TEXT:显示文本节点。3:TEXT_NODESHOW_CDATA_SECTION:显示CDATA 节点(在HTML 中不常用)。4:CDATA_SECTION_NODESHOW_ENTITY_REFERENCE:显示实体引用节点(在HTML 中不常用)。5: ENTITY_REFERENCE_NODESHOW_ENTITY:显示实体节点(在HTML 中不常用)。6 : ENTITY_NODESHOW_PROCESSING_INSTRUCTION:显示处理指令节点。7: PROCESSING_INSTRUCTION_NODESHOW_COMMENT:显示注释节点。8:COMMENT_NODESHOW_DOCUMENT:显示文档节点。9OCUMENT_NODESHOW_DOCUMENT_TYPE:显示文档类型节点。10: DOCUMENT_TYPE_NODESHOW_DOCUMENT_FRAGMENT:显示文档片段节点。11 : DOCUMENT_FRAGMENT_NODESHOW_NOTATION:显示符号节点(在HTML 中不常用)。12 : NOTATION_NDENodeFilter.SHOW_ALL 表示显示所有类型节点,这和遍历节点的 childNodes 一样,childNodes 会把该节点下的所有类型的子节点遍历出来。而节点的 children 就只遍历元素节点。
自定义过滤器

可以通过传递一个 NodeFilter 对象来实现自定义的过滤逻辑。NodeFilter 对象有一个 acceptNode 方法,该方法返回一个常量来决定是否接受节点:

  • NodeFilter.FILTER_ACCEPT:接受节点。
  • NodeFilter.FILTER_REJECT:拒绝节点及其子节点。
  • NodeFilter.FILTER_SKIP:跳过节点,但继续遍历其子节点。
  1. const filter = {
  2.     acceptNode: function (node){
  3.         if (node.tagName=== "DIV"){
  4.             return NodeFilter.FILTER_ACCEPT;
  5.         }else {
  6.             return NodeFilter.FILTER_SKIP;
  7.         }
  8.     },
  9. };
  10. const walker = document.createTreeWalker(
  11.     document.body,
  12.     NodeFilter.SHOW_ELEMENT,
  13.     filter,//只遍历标签名是div的元素
  14.     false
  15. );
  16. let node;
  17. while ((node = walker.nextNode())!== nu11){
  18. console.log(node);
复制代码
遍历节点

TreeWalker 提供了多种方法来遍历节点:
nextNode():移动到下一个节点,如果没有更多节点则返回 null。

  • previousNode():移动到上一个节点,如果没有更多节点则返回 null。
  • parentNode():移动到当前节点的父节点,如果没有父节点则返回 null。
  • firstChild():移动到当前节点的第一个子节点,如果没有子节点则返回 null。
  • lastChild():移动到当前节点的最后一个子节点,如果没有子节点则返回 null。
  • nextSibling():移动到当前节点的下一个兄弟节点,如果没有更多兄弟节点则返回 null。
  • previousSibling():移动到当前节点的上一个兄弟节点,如果没有更多兄弟节点则返回 null。
需要注意的是,nextNode()是深度优先遍历
当前节点

TreeWalker 对象有一个 currentNode 属性,表示当前遍历到的节点,这个属性是可读写的,可以通过这个属性来获取或设置当前节点。
  1. console.log(walker.currentNode);// 输出当前节点
  2. //设置当前节点
  3. walker.currentNode = document.getElementById("id");
  4. console.log(walker.currentNode);//输出新设置的当前节点
复制代码
实践并和 querySelector() 比较

querySelector() 是一个选择器通过传入静态的 css 选择器获取元素。
而 TreeWalker 会创建一个对象,适应于进行复杂的 DOM 操作的场景,在遍历过程中支持添加、删除或修改节点,或者动态改变遍历方向,很灵活。
这两个本来就是适用于不同场景,获取元素基本上还是用querySelector(),不过涉及到复杂循环遍历时就可以考虑 TreeWalker 了。
这里我测试了一下,在怎样的复杂程度下,TreeWalker 遍历 会比用 for 循环 + querySelector() 遍历执行速度上更快。
经过不断测试,在循环嵌套遍历 1000 个元素时,并且对每个元素进行添加删除子元素的操作,此时使用 TreeWalker 遍历执行速度更快。这 1000 个数量并不是一个可以判定复杂程度确定的值,只是在当前浏览器下测试出来的一个大概的数量。
因为这还与对元素操作复杂度有关,与浏览器执行性能也有关,随着浏览器不断更新迭代,后面应该只会越来越快。
下面整理下测试过程,在页面中创建了一个 id是root的元素
  1. [/code]然后给 root 创建1000个子元素,这里使用了三重 for 循环js
  2. [code]function createEl(el) {
  3.   var fragment = document.createDocumentFragment();
  4.   for (var i = 0; i < 10; i++) {
  5.     var divBox = document.createElement("div");
  6.     var innerHTML = `Row${i}`;
  7.     for (let j = 0; j < 10; j++) {
  8.       innerHTML += `Col${j}=>`;
  9.       for (let k = 0; k < 10; k++) {
  10.         innerHTML += `children${k}`;
  11.       }
  12.       innerHTML += ``;
  13.     }
  14.     divBox.innerHTML = innerHTML;
  15.     fragment.appendChild(divBox);
  16.     el.appendChild(fragment);
  17.   }
  18. }
  19. createEl(document.getElementById("root"));
复制代码
渲染到页面上就是这样,截图没有全部截完:
1.png

然后用循环 + querySelector 遍历 root,这里为了让遍历复杂一点,添加了一个逻辑:当遍历到子节点是 children2 时,
给这个节点添加一个新的子节点,然后又删除它;最后计算执行时间;
  1. const querySelectorTest = () => {
  2.     let root = document.querySelector("#root");
  3.     let children = root.children;
  4.     let len = children.length;
  5.     console.time("querySelector");
  6.     const tempFn = (list) => {
  7.         for (let i = 0; i < list.length; i++) {
  8.             let node = list[i];
  9.             if (node.textContent==="children2") {
  10.                 //添加一个新的子节点
  11.                 const newDiv = document.createElement("div");
  12.                 newDiv.textContent = "New Item";
  13.                 node.appendChild(newDiv);
  14.                 console.log("Added new node:");
  15.                 //删除添加的子节点
  16.                 node.removeChild(newDiv);
  17.             }
  18.             if (node.children.length) {
  19.                 tempFn(node.children);
  20.             }
  21.         }
  22.     }
  23.     tempFn(children);
  24.     console.timeEnd("querySelector");
  25. }
复制代码
然后同样的逻辑,用 TreeWalker 来遍历
  1. const TreeWalkerTest = () => {
  2.     const walker = document.createTreeWalker(
  3.         document.getElementById("root"),
  4.         NodeFilter.SHOW_ELEMENT,
  5.         null,
  6.         false
  7.     );
  8.     console.time("treeWalker");
  9.     let node;
  10.     while ((node = walker.nextNode()) !== null) {
  11.         if (node.textContent === "children2") {
  12.             //添加一个新的子节点
  13.             const newDiv = document.createElement("div");
  14.             newDiv.textContent = "New Item";
  15.             node.appendChild(newDiv);
  16.             //移动到新添加的节点
  17.             let newNode = walker.nextNode();
  18.             console.log("Added new node:");
  19.             //删除一个节点前需要先移动到上一个节点 walker.previousNode(),这样才能顺利遍历下一个;
  20.             walker.previousNode();
  21.             newNode.parentNode.removeChild(newNode);
  22.         }
  23.     }
  24.     console.timeEnd("treeWalker");
  25. }
复制代码
这里需要注意的是,删除一个节点前需要先移动到上一个节点 walker.previousNode(),这样才能顺利遍历下一个;
同时测试这两个函数
  1. for (let i = 0; i < 10; i++) {
  2.   TreeWalkerTest()
  3.   querySelectorTest()
  4. }
复制代码
结果如下:
2.png

可以看到多次运行测试函数,TreeWalker 执行速度大多数都更快;
然后修改 root 子元素数量试试,从1000改为100,测试函数的逻辑不变;
  1. function createEl(el) {
  2.   var fragment = document.createDocumentFragment();
  3.   // for (var i = 0; i < 10; i++) {
  4.     var divBox = document.createElement("div");
  5.     var innerHTML = `Row`;
  6.     for (let j = 0; j < 10; j++) {
  7.         innerHTML += `Col${j}=>`;
  8.         for (let k = 0; k < 10; k++) {
  9.             innerHTML += `children${k}`;
  10.         }
  11.         innerHTML += ``;
  12.     }
  13.     divBox.innerHTML = innerHTML;
  14.     fragment.appendChild(divBox);
  15.     el.appendChild(fragment);
  16.   // }
  17. }
复制代码
再来测试下:
3.png

TreeWalker 执行速度依然大多数都更快;
再来修改下测试函数的逻辑,只遍历,不进行添加删除节点的操作
  1. const querySelectorTest = () => {
  2.     let root = document.querySelector("#root");
  3.     let children = root.children;
  4.     let len = children.length;
  5.     console.time("querySelector");
  6.     const tempFn = (list) => {
  7.         for (let i = 0; i < list.length; i++) {
  8.             let node = list[i];
  9.             // if (node.textContent === "children2") {
  10.             //         //添加一个新的子节点
  11.             //         const newDiv = document.createElement("div");
  12.             //         newDiv.textContent = "New Item";
  13.             //         node.appendChild(newDiv);
  14.             //         // console.log("Added new node:");
  15.             //         //删除添加的子节点
  16.             //         node.removeChild(newDiv);
  17.             // }
  18.             if (node.children.length) {
  19.                     tempFn(node.children);
  20.             }
  21.         }
  22.     }
  23.     tempFn(children);
  24.     console.timeEnd("querySelector");
  25. }
  26. const TreeWalkerTest = () => {
  27.         const walker = document.createTreeWalker(
  28.                 document.getElementById("root"),
  29.                 NodeFilter.SHOW_ELEMENT,
  30.                 null,
  31.                 false
  32.         );
  33.         console.time("treeWalker");
  34.         let node;
  35.         while ((node = walker.nextNode()) !== null) {
  36.                 // if (node.textContent === "children2") {
  37.                 //         //添加一个新的子节点
  38.                 //         const newDiv = document.createElement("div");
  39.                 //         newDiv.textContent = "New Item";
  40.                 //         node.appendChild(newDiv);
  41.                 //         //移动到新添加的节点
  42.                 //         let newNode = walker.nextNode();
  43.                 //         // console.log("Added new node:");
  44.                 //         //删除一个节点
  45.                 //         walker.previousNode();
  46.                 //         newNode.parentNode.removeChild(newNode);
  47.                 // }
  48.         }
  49.         console.timeEnd("treeWalker");
  50. }
  51. for (let i = 0; i < 10; i++) {
  52.         TreeWalkerTest()
  53.         querySelectorTest()
  54. }
复制代码
结果如下:
4.png

TreeWalker 执行速度还是大多数都更快;
但其实这里测试意义不大了,这个例子实际上是在测试 while 循坏 和 for 循环+递归 的差异了;单论循环而言, while 循环总是最快的;
那么接下来把 root 子节点打平,不再嵌套了,也就是遍历一维数组,然后把 querySelectorTest 的 for 循环改为 while 循环,再来试一下
  1. function createEl(el, len) {
  2.         var fragment = document.createDocumentFragment();
  3.         for (var i = 0; i < 10000; i++) {
  4.                 var divBox = document.createElement("div");
  5.                 divBox.innerHTML = "Row" + i;
  6.                 fragment.appendChild(divBox);
  7.         }
  8.         el.appendChild(fragment);
  9. }
  10. const TreeWalkerTest = () => {
  11.         const walker = document.createTreeWalker(
  12.                 document.getElementById("root"),
  13.                 NodeFilter.SHOW_ELEMENT,
  14.                 null,
  15.                 false
  16.         );
  17.         console.time("treeWalker");
  18.         let node;
  19.         while ((node = walker.nextNode()) !== null) {}
  20.         console.timeEnd("treeWalker");
  21. };
  22. const querySelectorTest = () => {
  23.         let root = document.querySelector("#root");
  24.         let children = root.children;
  25.         let len = children.length;
  26.         console.time("querySelector");
  27.         let i = 0;
  28.         while (i++ < len) { }
  29.         console.timeEnd("querySelector");
  30. }
复制代码
这样就是普通的两个 while 循环对比了,此时 TreeWalker 就没有优势了。
5.png

总结起来,在不复杂的场景下,遍历的元素数量不多或者嵌套层级不深,或者对遍历的元素没有进行复杂的DOM操作,使用普通 for 循环,while 循环操作元素始终比 TreeWalker 快,
反之可以考虑使用 TreeWalker。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册