學(xué)習(xí) TreeWalker api 并與普通遍歷 DOM 方式進行比較
介紹 TreeWalker
TreeWalker 是 JavaScript 中用于遍歷 DOM 樹的一個接口。允許你以靈活的方式在 DOM 樹中進行前向和后向遍歷,包括訪問父節(jié)點、子節(jié)點和兄弟節(jié)點。適用于處理復(fù)雜的 DOM 操作:在遍歷過程中進行添加、刪除或修改節(jié)點的操作,并繼續(xù)遍歷。
與普通的 for 循環(huán) + querySelector 相比靈活性更高。執(zhí)行速度方面,在 DOM 超過一定復(fù)雜度的情況下,TreeWalker 更快,后面會舉例說明。
實踐
創(chuàng)建 TreeWalker
可以使用 document.createTreeWalker 方法來創(chuàng)建一個 TreeWalker 對象。這個方法接受四個參數(shù):
root:要遍歷的根節(jié)點。whatToShow(可選):一個整數(shù),表示要顯示的節(jié)點類型。默認值是NodeFilter.SHOW_ALL,表示顯示所有節(jié)點。filter(可選):一個NodeFilter對象,用于自定義過濾邏輯。entityReferenceExpansion(可選):一個布爾值,表示是否展開實體引用。這個參數(shù)在現(xiàn)代瀏覽器中通常被忽略,因為實體引用在HTML中很少使用
const walker = document.createTreeWalker(
document.body,//.root
NodeFilter.SHOW_ELEMENT,// whatToShow(可選)
null,// filter(可選)
false //entityReferenceExpansion(可選)
)
NodeFilter.SHOW_ELEMENT 表示顯示元素節(jié)點。
節(jié)點類型
NodeFilter 有 12 種節(jié)點類型,和 Node 接口的節(jié)點類型一一對應(yīng);
| NodeFilter | Node.prototype |
|---|---|
| SHOW_ELEMENT:顯示元素節(jié)點。 | 1: ELEMENT_NODE |
| SHOW_ATTRIBUTE:顯示屬性節(jié)點(在HTML 中不常用)。 | 2: ATTRIBUTE_NODE |
| SHOW_TEXT:顯示文本節(jié)點。 | 3:TEXT_NODE |
| SHOW_CDATA_SECTION:顯示CDATA 節(jié)點(在HTML 中不常用)。 | 4:CDATA_SECTION_NODE |
| SHOW_ENTITY_REFERENCE:顯示實體引用節(jié)點(在HTML 中不常用)。 | 5: ENTITY_REFERENCE_NODE |
| SHOW_ENTITY:顯示實體節(jié)點(在HTML 中不常用)。 | 6 : ENTITY_NODE |
| SHOW_PROCESSING_INSTRUCTION:顯示處理指令節(jié)點。 | 7: PROCESSING_INSTRUCTION_NODE |
| SHOW_COMMENT:顯示注釋節(jié)點。 | 8:COMMENT_NODE |
| SHOW_DOCUMENT:顯示文檔節(jié)點。 | 9:DOCUMENT_NODE |
| SHOW_DOCUMENT_TYPE:顯示文檔類型節(jié)點。 | 10: DOCUMENT_TYPE_NODE |
| SHOW_DOCUMENT_FRAGMENT:顯示文檔片段節(jié)點。 | 11 : DOCUMENT_FRAGMENT_NODE |
| SHOW_NOTATION:顯示符號節(jié)點(在HTML 中不常用)。 | 12 : NOTATION_NDE |
NodeFilter.SHOW_ALL 表示顯示所有類型節(jié)點,這和遍歷節(jié)點的 childNodes 一樣,childNodes 會把該節(jié)點下的所有類型的子節(jié)點遍歷出來。而節(jié)點的 children 就只遍歷元素節(jié)點。
自定義過濾器
可以通過傳遞一個 NodeFilter 對象來實現(xiàn)自定義的過濾邏輯。NodeFilter 對象有一個 acceptNode 方法,該方法返回一個常量來決定是否接受節(jié)點:
- NodeFilter.FILTER_ACCEPT:接受節(jié)點。
- NodeFilter.FILTER_REJECT:拒絕節(jié)點及其子節(jié)點。
- NodeFilter.FILTER_SKIP:跳過節(jié)點,但繼續(xù)遍歷其子節(jié)點。
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);
遍歷節(jié)點
TreeWalker 提供了多種方法來遍歷節(jié)點:
nextNode():移動到下一個節(jié)點,如果沒有更多節(jié)點則返回 null。
- previousNode():移動到上一個節(jié)點,如果沒有更多節(jié)點則返回 null。
- parentNode():移動到當(dāng)前節(jié)點的父節(jié)點,如果沒有父節(jié)點則返回 null。
- firstChild():移動到當(dāng)前節(jié)點的第一個子節(jié)點,如果沒有子節(jié)點則返回 null。
- lastChild():移動到當(dāng)前節(jié)點的最后一個子節(jié)點,如果沒有子節(jié)點則返回 null。
- nextSibling():移動到當(dāng)前節(jié)點的下一個兄弟節(jié)點,如果沒有更多兄弟節(jié)點則返回 null。
- previousSibling():移動到當(dāng)前節(jié)點的上一個兄弟節(jié)點,如果沒有更多兄弟節(jié)點則返回 null。
需要注意的是,nextNode()是深度優(yōu)先遍歷。
當(dāng)前節(jié)點
TreeWalker 對象有一個 currentNode 屬性,表示當(dāng)前遍歷到的節(jié)點,這個屬性是可讀寫的,可以通過這個屬性來獲取或設(shè)置當(dāng)前節(jié)點。
console.log(walker.currentNode);// 輸出當(dāng)前節(jié)點
//設(shè)置當(dāng)前節(jié)點
walker.currentNode = document.getElementById("id");
console.log(walker.currentNode);//輸出新設(shè)置的當(dāng)前節(jié)點
實踐并和 querySelector() 比較
querySelector() 是一個選擇器通過傳入靜態(tài)的 css 選擇器獲取元素。
而 TreeWalker 會創(chuàng)建一個對象,適應(yīng)于進行復(fù)雜的 DOM 操作的場景,在遍歷過程中支持添加、刪除或修改節(jié)點,或者動態(tài)改變遍歷方向,很靈活。
這兩個本來就是適用于不同場景,獲取元素基本上還是用querySelector(),不過涉及到復(fù)雜循環(huán)遍歷時就可以考慮 TreeWalker 了。
這里我測試了一下,在怎樣的復(fù)雜程度下,TreeWalker 遍歷 會比用 for 循環(huán) + querySelector() 遍歷執(zhí)行速度上更快。
經(jīng)過不斷測試,在循環(huán)嵌套遍歷 1000 個元素時,并且對每個元素進行添加刪除子元素的操作,此時使用 TreeWalker 遍歷執(zhí)行速度更快。這 1000 個數(shù)量并不是一個可以判定復(fù)雜程度確定的值,只是在當(dāng)前瀏覽器下測試出來的一個大概的數(shù)量。
因為這還與對元素操作復(fù)雜度有關(guān),與瀏覽器執(zhí)行性能也有關(guān),隨著瀏覽器不斷更新迭代,后面應(yīng)該只會越來越快。
下面整理下測試過程,在頁面中創(chuàng)建了一個 id是root的元素
<div id="root"></div>
<style>
#root>div{margin-bottom: 20px;}
.ColDiv{display: flex;gap: 10px;}
</style>
然后給 root 創(chuàng)建1000個子元素,這里使用了三重 for 循環(huán)js
function createEl(el) {
var fragment = document.createDocumentFragment();
for (var i = 0; i < 10; i++) {
var divBox = document.createElement("div");
var innerHTML = `Row${i}`;
for (let j = 0; j < 10; j++) {
innerHTML += `<div><div class="ColDiv">Col${j}=>`;
for (let k = 0; k < 10; k++) {
innerHTML += `<div>children${k}</div>`;
}
innerHTML += `</div>`;
}
divBox.innerHTML = innerHTML;
fragment.appendChild(divBox);
el.appendChild(fragment);
}
}
createEl(document.getElementById("root"));
渲染到頁面上就是這樣,截圖沒有全部截完:

然后用循環(huán) + querySelector 遍歷 root,這里為了讓遍歷復(fù)雜一點,添加了一個邏輯:當(dāng)遍歷到子節(jié)點是 children2 時,
給這個節(jié)點添加一個新的子節(jié)點,然后又刪除它;最后計算執(zhí)行時間;
const querySelectorTest = () => {
let root = document.querySelector("#root");
let children = root.children;
let len = children.length;
console.time("querySelector");
const tempFn = (list) => {
for (let i = 0; i < list.length; i++) {
let node = list[i];
if (node.textContent==="children2") {
//添加一個新的子節(jié)點
const newDiv = document.createElement("div");
newDiv.textContent = "New Item";
node.appendChild(newDiv);
console.log("Added new node:");
//刪除添加的子節(jié)點
node.removeChild(newDiv);
}
if (node.children.length) {
tempFn(node.children);
}
}
}
tempFn(children);
console.timeEnd("querySelector");
}
然后同樣的邏輯,用 TreeWalker 來遍歷
const TreeWalkerTest = () => {
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") {
//添加一個新的子節(jié)點
const newDiv = document.createElement("div");
newDiv.textContent = "New Item";
node.appendChild(newDiv);
//移動到新添加的節(jié)點
let newNode = walker.nextNode();
console.log("Added new node:");
//刪除一個節(jié)點前需要先移動到上一個節(jié)點 walker.previousNode(),這樣才能順利遍歷下一個;
walker.previousNode();
newNode.parentNode.removeChild(newNode);
}
}
console.timeEnd("treeWalker");
}
這里需要注意的是,刪除一個節(jié)點前需要先移動到上一個節(jié)點 walker.previousNode(),這樣才能順利遍歷下一個;
同時測試這兩個函數(shù)
for (let i = 0; i < 10; i++) {
TreeWalkerTest()
querySelectorTest()
}
結(jié)果如下:

可以看到多次運行測試函數(shù),TreeWalker 執(zhí)行速度大多數(shù)都更快;
然后修改 root 子元素數(shù)量試試,從1000改為100,測試函數(shù)的邏輯不變;
function createEl(el) {
var fragment = document.createDocumentFragment();
// for (var i = 0; i < 10; i++) {
var divBox = document.createElement("div");
var innerHTML = `Row`;
for (let j = 0; j < 10; j++) {
innerHTML += `<div><div class="ColDiv">Col${j}=>`;
for (let k = 0; k < 10; k++) {
innerHTML += `<div>children${k}</div>`;
}
innerHTML += `</div>`;
}
divBox.innerHTML = innerHTML;
fragment.appendChild(divBox);
el.appendChild(fragment);
// }
}
再來測試下:

TreeWalker 執(zhí)行速度依然大多數(shù)都更快;
再來修改下測試函數(shù)的邏輯,只遍歷,不進行添加刪除節(jié)點的操作
const querySelectorTest = () => {
let root = document.querySelector("#root");
let children = root.children;
let len = children.length;
console.time("querySelector");
const tempFn = (list) => {
for (let i = 0; i < list.length; i++) {
let node = list[i];
// if (node.textContent === "children2") {
// //添加一個新的子節(jié)點
// const newDiv = document.createElement("div");
// newDiv.textContent = "New Item";
// node.appendChild(newDiv);
// // console.log("Added new node:");
// //刪除添加的子節(jié)點
// node.removeChild(newDiv);
// }
if (node.children.length) {
tempFn(node.children);
}
}
}
tempFn(children);
console.timeEnd("querySelector");
}
const TreeWalkerTest = () => {
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") {
// //添加一個新的子節(jié)點
// const newDiv = document.createElement("div");
// newDiv.textContent = "New Item";
// node.appendChild(newDiv);
// //移動到新添加的節(jié)點
// let newNode = walker.nextNode();
// // console.log("Added new node:");
// //刪除一個節(jié)點
// walker.previousNode();
// newNode.parentNode.removeChild(newNode);
// }
}
console.timeEnd("treeWalker");
}
for (let i = 0; i < 10; i++) {
TreeWalkerTest()
querySelectorTest()
}
結(jié)果如下:

TreeWalker 執(zhí)行速度還是大多數(shù)都更快;
但其實這里測試意義不大了,這個例子實際上是在測試 while 循壞 和 for 循環(huán)+遞歸 的差異了;單論循環(huán)而言, while 循環(huán)總是最快的;
那么接下來把 root 子節(jié)點打平,不再嵌套了,也就是遍歷一維數(shù)組,然后把 querySelectorTest 的 for 循環(huán)改為 while 循環(huán),再來試一下
function createEl(el, len) {
var fragment = document.createDocumentFragment();
for (var i = 0; i < 10000; i++) {
var divBox = document.createElement("div");
divBox.innerHTML = "Row" + i;
fragment.appendChild(divBox);
}
el.appendChild(fragment);
}
const TreeWalkerTest = () => {
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 = () => {
let root = document.querySelector("#root");
let children = root.children;
let len = children.length;
console.time("querySelector");
let i = 0;
while (i++ < len) { }
console.timeEnd("querySelector");
}
這樣就是普通的兩個 while 循環(huán)對比了,此時 TreeWalker 就沒有優(yōu)勢了。

總結(jié)起來,在不復(fù)雜的場景下,遍歷的元素數(shù)量不多或者嵌套層級不深,或者對遍歷的元素沒有進行復(fù)雜的DOM操作,使用普通 for 循環(huán),while 循環(huán)操作元素始終比 TreeWalker 快,
反之可以考慮使用 TreeWalker。

浙公網(wǎng)安備 33010602011771號