使用 Maps 和 WeakMaps 可以提高代碼的可讀性和可維護性。將DOM節(jié)點與相關(guān)數(shù)據(jù)關(guān)聯(lián)起來,有助于使代碼更清晰易懂。
這篇文章討論了使用 Maps 和 WeakMaps 處理DOM節(jié)點的優(yōu)勢。Maps 和 WeakMaps 是非常實用的工具,尤其在處理大量DOM節(jié)點時,它們發(fā)揮著重要作用。
文章作者認為,使用 Maps 和 WeakMaps 處理 DOM 節(jié)點有以下幾個優(yōu)點。首先,它們可以方便地存儲和檢索數(shù)據(jù)。與其他數(shù)據(jù)結(jié)構(gòu)相比,Maps和 WeakMaps 可以更簡潔地組織和查找相關(guān)數(shù)據(jù)。其次,它們可以幫助開發(fā)者更好地管理內(nèi)存。當不再需要某個DOM節(jié)點時,WeakMaps可以自動釋放與該節(jié)點相關(guān)的內(nèi)存,從而提高程序的性能。最后,使用 Maps 和 WeakMaps 可以提高代碼的可讀性和可維護性。將DOM節(jié)點與相關(guān)數(shù)據(jù)關(guān)聯(lián)起來,有助于使代碼更清晰易懂。
下面是正文:
在JAVAScript中, 我們經(jīng)常使用普通的對象來存儲鍵/值數(shù)據(jù),它們非常擅長這項工作 - 清晰易讀:
const person = {
firstName: 'Alex',
lastName: 'macArthur',
isACommunist: false
};
但是,當我們開始處理經(jīng)常被讀取、更改和添加屬性的較大實體時,更傾向于使用 Maps。因為在某些情況下,Map 比對象具有多個優(yōu)勢,特別是性能問題或插入順序比較重要的情況下。
但最近我特別喜歡使用它們來處理大量的DOM節(jié)點。
在閱讀Caleb Porzio最近的博客文章時,我想到了這個想法。在這篇文章中,他正在使用由10,000個表行組成的表格,其中一個可以是“active”。為了管理選擇不同行時的狀態(tài),使用對象作為鍵/值存儲。這是他的一個迭代版本的注釋版本。
import { ref, watchEffect } from 'vue';
let rowStates = {};
let activeRow;
document.querySelectorAll('tr').forEach((row) => {
// Set row state.
rowStates[row.id] = ref(false);
row.addEventListener('click', () => {
// Update row state.
if (activeRow) rowStates[activeRow].value = false;
activeRow = row.id;
rowStates[row.id].value = true;
});
watchEffect(() => {
// Read row state.
if (rowStates[row.id].value) {
row.classList.add('active');
} else {
row.classList.remove('active');
}
});
});
它使用一個對象作為大型哈希映射表,因此用于關(guān)聯(lián)值的鍵必須是字符串,因此需要在每個項目上存在唯一的ID(或其他字符串值)。這帶來了一些額外的編程開銷,需要在需要時生成和讀取這些值。
任何對象都可以作為鍵
相反,使用 Map 可以讓我們直接將 html 節(jié)點作為鍵。因此,該代碼片段最終看起來像這樣:
import { ref, watchEffect } from 'vue';
- let rowStates = {};
+ let rowStates = new Map();
let activeRow;
document.querySelectorAll('tr').forEach((row) => {
- rowStates[row.id] = ref(false);
+ rowStates.set(row, ref(false));
row.addEventListener('click', () => {
- if (activeRow) rowStates[activeRow].value = false;
+ if (activeRow) rowStates.get(activeRow).value = false;
activeRow = row;
- rowStates[row.id].value = true;
+ rowStates.get(activeRow).value = true;
});
watchEffect(() => {
- if (rowStates[row.id].value) {
+ if (rowStates.get(row).value) {
row.classList.add('active');
} else {
row.classList.remove('active');
}
});
});
這里最明顯的好處是我不需要擔心每行存在唯一ID。節(jié)點引用本身是唯一的,可以作為鍵。因此,既不需要設(shè)置也不需要讀取任何屬性。這更簡單、更具彈性。
讀/寫操作通常更高效
當我們處理更大的數(shù)據(jù)集時,操作的性能顯著提高。甚至在規(guī)范中也有說明 - 必須以保持性能的方式構(gòu)建 Map
,以便隨著項目數(shù)量的增加而增長:
Maps must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements in the collection.
“Sublinear” 的意思是性能不會隨著 Map 的大小成比例地下降。因此,即使是大型 Map,性能也應(yīng)該保持相當迅速。
再次強調(diào),沒有必要干擾DOM屬性或通過類似字符串的ID執(zhí)行查找。每個鍵本身就是一個引用,這意味著我們可以跳過一兩個步驟。
我進行了一些基本的性能測試來確認所有這些。首先,按照Caleb的場景,我在頁面上生成了10,000個 <tr> 元素:
const table = document.createElement('table');
document.body.Append(table);
const count = 10_000;
for (let i = 0; i < count; i++) {
const item = document.createElement('tr');
item.id = i;
item.textContent = 'item';
table.append(item);
}
接下來,我設(shè)置了一個模板來測量循環(huán)遍歷所有這些行并將一些相關(guān)狀態(tài)存儲在對象或Map
中需要多長時間。我還在 for
循環(huán)中運行了同樣的過程多次,然后確定編寫和讀取所需的平均時間。
const rows = document.querySelectorAll('tr');
const times = [];
const testMap = new Map();
const testObj = {};
for (let i = 0; i < 1000; i++) {
const start = performance.now();
rows.forEach((row, index) => {
// Test Case #1
// testObj[row.id] = index;
// const result = testObj[row.id];
// Test Case #2
// testMap.set(row, index);
// const result = testMap.get(row);
});
times.push(performance.now() - start);
}
const average = times.reduce((acc, i) => acc + i, 0) / times.length;
console.log(average);
我用不同的行大小運行了這個測試:
請記住,即使是稍微不同的情況,這些結(jié)果也可能會有很大的差異,但總體而言,它們通常符合我的預(yù)期。在處理相對較少的項目時,Map 和對象之間的性能是可比的。但隨著項目數(shù)量的增加,Map 開始拉開差距。性能的次線性變化開始顯現(xiàn)。
WeakMaps更有效地管理內(nèi)存
有一個特殊版本的 Map 接口,旨在更好地管理內(nèi)存 - WeakMap 。它通過保持對其鍵的“弱”引用來實現(xiàn)這一點,因此,如果這些對象鍵中的任何一個不再具有其他地方綁定的引用,則它有資格進行垃圾回收。因此,當不再需要該鍵時,整個條目將自動從 WeakMap 中刪除,從而清除更多內(nèi)存。它也適用于DOM節(jié)點。
為了調(diào)整這個,我們將使用 FinalizationRegistry ,它會在你正在觀察的引用被垃圾回收時觸發(fā)回調(diào)。我們將從幾個列表項開始:
<ul>
<li id="item1">first</li>
<li id="item2">second</li>
<li id="item3">third</li>
</ul>
接下來,我們將把這些項目放入 WeakMap 中,并將 item2 注冊為注冊表監(jiān)視的對象。我們將其刪除,每當它被垃圾回收時,回調(diào)將被觸發(fā),我們將能夠看到 WeakMap 如何發(fā)生變化。
但是...垃圾收集是不可預(yù)測的,也沒有官方的方法來觸發(fā)它,因此為了測試,我們將定期生成一堆對象并將它們保存在內(nèi)存中。以下是整個腳本:
(async () => {
const listMap = new WeakMap();
// Stick each item in a WeakMap.
document.querySelectorAll('li').forEach((node) => {
listMap.set(node, node.id);
});
const registry = new FinalizationRegistry((heldValue) => {
// Garbage collection has happened!
console.log('After collection:', heldValue);
});
registry.register(document.getElementById('item2'), listMap);
console.log('Before collection:', listMap);
// Remove node, freeing up reference!
document.getElementById('item2').remove();
// Periodically create a bunch o' objects to trigger collection.
const objs = [];
while (true) {
for (let i = 0; i < 100; i++) {
objs.push(...new Array(100));
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
})();
在發(fā)生任何事情之前,WeakMap 如預(yù)期的那樣包含三個項。但是在從DOM中刪除第二項并進行垃圾收集之后,它看起來有點不同
由于節(jié)點引用在DOM中不再存在,整個條目已從 WeakMap 中刪除,從而釋放了更多的內(nèi)存。這是一個很 nice 功能,有助于使環(huán)境的內(nèi)存更加整潔。