使用Map處理Dom節(jié)點(diǎn)的方法詳解
我們?cè)贘avaScript中使用了很多普通的、古老的對(duì)象來(lái)存儲(chǔ)鍵/值數(shù)據(jù),它們處理的非常出色:
const person = {
firstName: 'Alex',
lastName: 'MacArthur',
isACommunist: false
};但是,當(dāng)你開(kāi)始處理較大的實(shí)體,其屬性經(jīng)常被讀取、更改和添加時(shí),人們?cè)絹?lái)越多地使用Map來(lái)代替。這是有原因的:在某些情況下,Map跟對(duì)象相比有多種優(yōu)勢(shì),特別是那些有敏感的性能問(wèn)題或插入的順序非常重要的情況。
但最近,我意識(shí)到我特別喜歡用它們來(lái)處理大量的DOM節(jié)點(diǎn)集合。
這個(gè)想法是在閱讀Caleb Porzio最近的一篇博文時(shí)產(chǎn)生的。在這篇文章中,他正在處理一個(gè)假設(shè)的例子,即一個(gè)由10,000行組成的表,其中一條可以是"active"。為了管理不同行被選中的狀態(tài),一個(gè)對(duì)象被用于鍵/值存儲(chǔ)。下面是他的一個(gè)迭代的注釋版本。
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');
}
});
});這能很好地完成工作。但是,它使用一個(gè)對(duì)象作為一個(gè)大型的類散列表,所以用于關(guān)聯(lián)值的鍵必須是一個(gè)字符串,從而要求每個(gè)項(xiàng)目有一個(gè)唯一的ID(或其他字符串值)。這帶來(lái)了一些額外的程序性開(kāi)銷,以便在需要時(shí)生成和讀取這些值。
對(duì)象即key
與之對(duì)應(yīng)的是,Map允許我們使用HTML節(jié)點(diǎn)作為自身的鍵。上面的代碼片段最終會(huì)是這樣:
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');
}
});
});這里最明顯的好處是,我不需要擔(dān)心每一行都有唯一的ID。具有唯一性的節(jié)點(diǎn)本身就可以作為鍵。正因?yàn)槿绱耍O(shè)置或讀取任何屬性都是不必要的。它更簡(jiǎn)單,也更有彈性。
讀寫性能更佳
在大多數(shù)情況下,這種差別是可以忽略不計(jì)的。但是,當(dāng)你處理更大的數(shù)據(jù)集時(shí),操作的性能就會(huì)明顯提高。這甚至體現(xiàn)在規(guī)范中--Map的構(gòu)建方式必須能夠在項(xiàng)目數(shù)量不斷增加時(shí)保持性能:
Map必須使用哈希表或其他機(jī)制來(lái)實(shí)現(xiàn),平均來(lái)說(shuō),這些機(jī)制提供的訪問(wèn)時(shí)間是集合中元素?cái)?shù)量的亞線性。
"亞線性"只是意味著性能不會(huì)以與Map大小成比例的速度下降。因此,即使是大的Map也應(yīng)該保持相當(dāng)快的速度。
但即使在此基礎(chǔ)上,也不需要搞亂DOM屬性或通過(guò)一個(gè)類似字符串的ID進(jìn)行查找。每個(gè)鍵本身就是一個(gè)引用,這意味著我們可以跳過(guò)一兩個(gè)步驟。
我做了一些基本的性能測(cè)試來(lái)確認(rèn)這一切。首先,按照Caleb的方案,我在一個(gè)頁(yè)面上生成了10,000個(gè)<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);
}接下來(lái),我建立了一個(gè)模板,用于測(cè)量循環(huán)所有這些行并將一些相關(guān)的狀態(tài)存儲(chǔ)在一個(gè)對(duì)象或Map中需要多長(zhǎng)時(shí)間。我還在for循環(huán)中多次運(yùn)行同一過(guò)程,然后確定寫入和讀取的平均時(shí)間。
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);下面是測(cè)試結(jié)果:
| 100行 | 10000行 | 100000行 | |
|---|---|---|---|
| Object | 0.023ms | 3.45ms | 89.9ms |
| Map | 0.019ms | 2.1ms | 48.7ms |
| 17% | 39% | 46% |
請(qǐng)記住,這些結(jié)果在稍有不同的情況下可能會(huì)有相當(dāng)大的差異,但總的來(lái)說(shuō),它們總體上符合我的期望。當(dāng)處理相對(duì)較少的項(xiàng)目時(shí),Map和對(duì)象之間的性能是相當(dāng)?shù)?。但隨著項(xiàng)目數(shù)量的增加,Map開(kāi)始拉開(kāi)距離。這種性能上的亞線性變化開(kāi)始顯現(xiàn)出來(lái)。
WeakMaps更有效地管理內(nèi)存
有一個(gè)特殊版本的Map接口被設(shè)計(jì)用來(lái)更好地管理內(nèi)存--WeakMap。它通過(guò)持有對(duì)其鍵的"弱"引用來(lái)做到這一點(diǎn),所以如果這些對(duì)象鍵中的任何一個(gè)不再有其他地方的引用與之綁定,它就有資格進(jìn)行垃圾回收。因此,當(dāng)不再需要該鍵時(shí),整個(gè)條目就會(huì)自動(dòng)從WeakMap中刪除,從而清除更多的內(nèi)存。這也適用于DOM節(jié)點(diǎn)。
為了解決這個(gè)問(wèn)題,我們將使用FinalizationRegistry,每當(dāng)你所監(jiān)聽(tīng)的引用被垃圾回收時(shí),它就會(huì)觸發(fā)一個(gè)回調(diào)(我從未想到會(huì)發(fā)現(xiàn)這樣的好東西)。我們將從幾個(gè)列表項(xiàng)開(kāi)始:
<ul> <li id="item1">first</li> <li id="item2">second</li> <li id="item3">third</li> </ul>
接下來(lái),我們將把這些項(xiàng)放在WeakMap中并注冊(cè)item2,使其受到注冊(cè)的監(jiān)聽(tīng)。我們將刪除它,只要它被垃圾回收,回調(diào)就會(huì)被觸發(fā),我們就能看到WeakMap的變化。
但是......垃圾收集是不可預(yù)測(cè)的,而且沒(méi)有正式的方法來(lái)使它發(fā)生,所以為了讓垃圾回收產(chǎn)生,我們將定期生成一堆對(duì)象并將它們持久化在內(nèi)存中。下面是整個(gè)腳本代碼
(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持有三個(gè)項(xiàng),正如預(yù)期的那樣。但在第二個(gè)項(xiàng)從DOM中被移除并發(fā)生垃圾回收后,它看起來(lái)有點(diǎn)不同:

由于節(jié)點(diǎn)引用不再存在于DOM中,整個(gè)條目都被從WeakMap中刪除,釋放了一點(diǎn)內(nèi)存。這是一個(gè)我很欣賞的功能,有助于保持環(huán)境的內(nèi)存更加整潔。
太長(zhǎng)不看版
我喜歡為DOM節(jié)點(diǎn)使用Map,因?yàn)椋?/p>
- 節(jié)點(diǎn)本身可以作為鍵。我不需要先在每個(gè)節(jié)點(diǎn)上設(shè)置或讀取獨(dú)特的屬性。
- 和具有大量成員的對(duì)象相比,
Map(被設(shè)計(jì)成)更具有性能。 - 使用以節(jié)點(diǎn)為鍵的
WeakMap意味著如果一個(gè)節(jié)點(diǎn)從DOM中被移除,條目將被自動(dòng)垃圾回收。
以上就是使用Map處理Dom節(jié)點(diǎn)的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Map處理Dom節(jié)點(diǎn)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript動(dòng)態(tài)檢測(cè)密碼強(qiáng)度原理及實(shí)現(xiàn)方法詳解
這篇文章主要介紹了JavaScript動(dòng)態(tài)檢測(cè)密碼強(qiáng)度原理及實(shí)現(xiàn)方法,結(jié)合具體實(shí)例形式詳細(xì)分析了javascript針對(duì)輸入字符串密碼強(qiáng)度檢測(cè)的原理與相關(guān)判斷操作技巧,需要的朋友可以參考下2019-06-06
JS實(shí)現(xiàn)給對(duì)象動(dòng)態(tài)添加屬性的方法
這篇文章主要介紹了JS實(shí)現(xiàn)給對(duì)象動(dòng)態(tài)添加屬性的方法,涉及JS屬性的遍歷、動(dòng)態(tài)賦值及eval方法的簡(jiǎn)單使用技巧,需要的朋友可以參考下2017-01-01
點(diǎn)擊button獲取text內(nèi)容并改變樣式的js實(shí)現(xiàn)
這篇文章主要介紹了點(diǎn)擊button獲取text內(nèi)容并改變樣式的js實(shí)現(xiàn),經(jīng)測(cè)試非常實(shí)用,需要的朋友可以參考下2014-09-09
使用ESLint禁止項(xiàng)目導(dǎo)入特定模塊的方法步驟
這篇文章主要介紹了使用ESLint禁止項(xiàng)目導(dǎo)入特定模塊的方法步驟,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03
js阻止默認(rèn)瀏覽器行為與冒泡行為的實(shí)現(xiàn)代碼
下面小編就為大家?guī)?lái)一篇js阻止默認(rèn)瀏覽器行為與冒泡行為的實(shí)現(xiàn)代碼。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-05-05
獲取3個(gè)數(shù)組不重復(fù)的值的具體實(shí)現(xiàn)
先用concat拼接數(shù)組 ,再使用一個(gè)對(duì)象、一個(gè)新數(shù)組(用于存放不重復(fù)的數(shù)組)具體實(shí)現(xiàn)如下,感興趣的朋友可以參考2013-12-12

