使用Map處理Dom節(jié)點的方法詳解
我們在JavaScript中使用了很多普通的、古老的對象來存儲鍵/值數(shù)據(jù),它們處理的非常出色:
const person = { firstName: 'Alex', lastName: 'MacArthur', isACommunist: false };
但是,當(dāng)你開始處理較大的實體,其屬性經(jīng)常被讀取、更改和添加時,人們越來越多地使用Map
來代替。這是有原因的:在某些情況下,Map跟對象相比有多種優(yōu)勢,特別是那些有敏感的性能問題或插入的順序非常重要的情況。
但最近,我意識到我特別喜歡用它們來處理大量的DOM節(jié)點集合。
這個想法是在閱讀Caleb Porzio最近的一篇博文時產(chǎn)生的。在這篇文章中,他正在處理一個假設(shè)的例子,即一個由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(或其他字符串值)。這帶來了一些額外的程序性開銷,以便在需要時生成和讀取這些值。
對象即key
與之對應(yīng)的是,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'); } }); });
這里最明顯的好處是,我不需要擔(dān)心每一行都有唯一的ID。具有唯一性的節(jié)點本身就可以作為鍵。正因為如此,設(shè)置或讀取任何屬性都是不必要的。它更簡單,也更有彈性。
讀寫性能更佳
在大多數(shù)情況下,這種差別是可以忽略不計的。但是,當(dāng)你處理更大的數(shù)據(jù)集時,操作的性能就會明顯提高。這甚至體現(xiàn)在規(guī)范中--Map
的構(gòu)建方式必須能夠在項目數(shù)量不斷增加時保持性能:
Map
必須使用哈希表或其他機(jī)制來實現(xiàn),平均來說,這些機(jī)制提供的訪問時間是集合中元素數(shù)量的亞線性。
"亞線性"只是意味著性能不會以與Map
大小成比例的速度下降。因此,即使是大的Map也應(yīng)該保持相當(dāng)快的速度。
但即使在此基礎(chǔ)上,也不需要搞亂DOM屬性或通過一個類似字符串的ID進(jìn)行查找。每個鍵本身就是一個引用,這意味著我們可以跳過一兩個步驟。
我做了一些基本的性能測試來確認(rèn)這一切。首先,按照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); }
接下來,我建立了一個模板,用于測量循環(huán)所有這些行并將一些相關(guān)的狀態(tài)存儲在一個對象或Map
中需要多長時間。我還在for
循環(huán)中多次運(yù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é)果:
100行 | 10000行 | 100000行 | |
---|---|---|---|
Object | 0.023ms | 3.45ms | 89.9ms |
Map | 0.019ms | 2.1ms | 48.7ms |
17% | 39% | 46% |
請記住,這些結(jié)果在稍有不同的情況下可能會有相當(dāng)大的差異,但總的來說,它們總體上符合我的期望。當(dāng)處理相對較少的項目時,Map
和對象之間的性能是相當(dāng)?shù)摹5S著項目數(shù)量的增加,Map
開始拉開距離。這種性能上的亞線性變化開始顯現(xiàn)出來。
WeakMaps更有效地管理內(nèi)存
有一個特殊版本的Map
接口被設(shè)計用來更好地管理內(nèi)存--WeakMap
。它通過持有對其鍵的"弱"引用來做到這一點,所以如果這些對象鍵中的任何一個不再有其他地方的引用與之綁定,它就有資格進(jìn)行垃圾回收。因此,當(dāng)不再需要該鍵時,整個條目就會自動從WeakMap
中刪除,從而清除更多的內(nèi)存。這也適用于DOM節(jié)點。
為了解決這個問題,我們將使用FinalizationRegistry
,每當(dāng)你所監(jiān)聽的引用被垃圾回收時,它就會觸發(fā)一個回調(diào)(我從未想到會發(fā)現(xiàn)這樣的好東西)。我們將從幾個列表項開始:
<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
的變化。
但是......垃圾收集是不可預(yù)測的,而且沒有正式的方法來使它發(fā)生,所以為了讓垃圾回收產(chǎn)生,我們將定期生成一堆對象并將它們持久化在內(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中被移除并發(fā)生垃圾回收后,它看起來有點不同:
由于節(jié)點引用不再存在于DOM中,整個條目都被從WeakMap
中刪除,釋放了一點內(nèi)存。這是一個我很欣賞的功能,有助于保持環(huán)境的內(nèi)存更加整潔。
太長不看版
我喜歡為DOM節(jié)點使用Map
,因為:
- 節(jié)點本身可以作為鍵。我不需要先在每個節(jié)點上設(shè)置或讀取獨特的屬性。
- 和具有大量成員的對象相比,
Map
(被設(shè)計成)更具有性能。 - 使用以節(jié)點為鍵的
WeakMap
意味著如果一個節(jié)點從DOM中被移除,條目將被自動垃圾回收。
以上就是使用Map處理Dom節(jié)點的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Map處理Dom節(jié)點的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript動態(tài)檢測密碼強(qiáng)度原理及實現(xiàn)方法詳解
這篇文章主要介紹了JavaScript動態(tài)檢測密碼強(qiáng)度原理及實現(xiàn)方法,結(jié)合具體實例形式詳細(xì)分析了javascript針對輸入字符串密碼強(qiáng)度檢測的原理與相關(guān)判斷操作技巧,需要的朋友可以參考下2019-06-06點擊button獲取text內(nèi)容并改變樣式的js實現(xiàn)
這篇文章主要介紹了點擊button獲取text內(nèi)容并改變樣式的js實現(xiàn),經(jīng)測試非常實用,需要的朋友可以參考下2014-09-09js阻止默認(rèn)瀏覽器行為與冒泡行為的實現(xiàn)代碼
下面小編就為大家?guī)硪黄猨s阻止默認(rèn)瀏覽器行為與冒泡行為的實現(xiàn)代碼。小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-05-05獲取3個數(shù)組不重復(fù)的值的具體實現(xiàn)
先用concat拼接數(shù)組 ,再使用一個對象、一個新數(shù)組(用于存放不重復(fù)的數(shù)組)具體實現(xiàn)如下,感興趣的朋友可以參考2013-12-12