作者 | Function | 前端時空 Vue 和 React 中的 key 到底有什麼用? key 是給每一個 vnode 的唯一 id,依靠 key,我們的 diff 操作可以更準確、更快速。 對於簡單列表頁渲染來說 diff 節點也更快,但會產生一些隱藏的副作用,比如可能不會產生過渡效果,或 ...
作者 | Function | 前端時空
Vue 和 React 中的 key 到底有什麼用?
diff 演算法的過程中,先會進行新舊節點的首尾交叉對比,當無法匹配的時候會用新節點的 key 與舊節點進行比對,從而找到相應舊節點。
你以為這樣回答,面試官就能放過你。Too young,To simple。下麵是面試官的反問三連擊:
為什麼更準確??
因為帶 key 就不是就地復用了,在 sameNode 函數 a.key === b.key 對比中可以避免就地復用的情況。所以會更加準確,如果不加 key,會導致之前節點的狀態被保留下來,會產生一系列的 bug。
為什麼更快速?
key 的唯一性可以被 Map 數據結構充分利用,相比於遍歷查找的時間複雜度 O(n),Map 的時間複雜度僅僅為 O(1)。
為什麼 Map 數據結構會更快?
Map / Set / WeakSet / WeakMap 就是使用隱藏的 Hash code 優化哈希表
ECMAScript 2015 引入了幾個新的數據結構,例如 Map,Set,WeakSet和 WeakMap,所有這些結構都在後臺使用哈希表。下麵詳細介紹了V8 v6.3+ 如何將 key 存儲在哈希表中的最新進展。
哈希碼 Hash code
hashCode = hashFunc(key)
在 V8 中,哈希碼只是一個隨機數,與對象值無關。因此,我們無法重新計算它,這意味著我們必須存儲它。
以前,對於那些把 JavaScript 對象作為 key 的情況,V8 將哈希碼作為私有符號(private symbol)存儲在對象上。V8 中的私有符號類似於Symbol,只是它不可枚舉,也不會不會泄漏到用戶空間 JavaScript 中。也就是說這個 symbol 只在 V8 引擎內部使用,用戶的 JavaScript 代碼訪問不到。
function GetObjectHash(key) {
let hash = key[hashCodeSymbol];
if (IS_UNDEFINED(hash)) {
hash = (MathRandom() * 0x40000000) | 0;
if (hash === 0) hash = 1;
key[hashCodeSymbol] = hash;
}
return hash;
}
之所以行之有效,是因為在將對象添加到哈希表之前,我們不必為哈希碼欄位保留記憶體。當對象被添加到哈希表時,才把新的私有符號存儲在對象上。
與使用內聯緩存(IC)系統進行的任何其他屬性查找一樣,V8 還可以優化哈希碼符號查找,從而為哈希碼提供非常快速的查找。當鍵具有相同的隱藏類時,這對於單態內聯緩存查找非常有效。但是,大多數現實世界的代碼都不遵循這種模式,並且鍵通常具有不同的隱藏類,導致散列碼的復態內聯緩存查找變慢。
私有符號方法的另一個問題是,它在存儲哈希碼時觸發了密鑰的隱藏類轉換。這導致不僅對哈希代碼查找也為上鍵和其他財產查找差的多形態代碼去優化從優化代碼。
JavaScript 對象支持存儲
- word (computer architecture)
元素存儲用於像數組索引的屬性,而屬性存儲用於其鍵為字元串或符號的屬性。有關這些的更多信息,請參見 Camillo Bruni 的 V8 博客文章。
const x = {};
x[1] = 'bar'; // ← stored in elements
x['foo'] = 'bar'; // ← stored in properties
隱藏哈希碼 Hiding the hash code
存儲哈希碼最簡單的方法是將 JavaScript 對象的大小擴展一個字,並將散列碼直接存儲在對象上。但是,對於那些沒有添加到哈希表中的對象,這會浪費記憶體。相反,我們可以嘗試將散列碼存儲在元素存儲或屬性存儲中。
元素存儲是一個包含其長度和所有元素的數組。在這裡沒有太多的工作要做,因為可以把哈希碼存儲在一個保留的槽中(比如第 0 個索引),不過,當我們不使用這個對象作為哈希表中的關鍵字時,仍然會浪費記憶體。
讓我們看看屬性存儲。有兩種數據結構用作屬性存儲:數組和字典。
與元素存儲中使用的數組不同,元素存儲不具有上限,而屬性存儲中使用的數組的上限為 1022 個值。由於性能原因,V8 在超過此限制時則轉換為使用字典模式。(我略微簡化了這一點 - V8 也可以在其他情況下使用字典,但是可以存儲在數組中的值的數量有一個固定的上限。)
因此,屬性存儲有三種可能的狀態:
- 空(沒有屬性)
- 數組(最多可以存儲 1022 個值)
- 字典
1、屬性存儲是空的
對於空的情況,我們可以直接在 JSObject 的偏移量上存儲哈希碼。
2、屬性存儲是一個數組
V8 表示小於 231 的整數(在 32 位系統上)更加高效,如 Smi。在一個 Smi 中,最低有效位是用來區別指針的 tag,而其餘的 31 位保存實際的整數值。
通常,數組將它們的長度存儲為 Smi。既然我們知道這個數組的最大容量只有 1022 個,我們只需要 10 個比特就可以存儲這個長度。我們可以使用剩下的 21 位來存儲哈希碼!
3、屬性支持存儲是一個字典
對於字典的情況,我們將字典大小增加1個字,以便將哈希碼存儲在字典起始位置的專用槽中。在這種情況下,我們可能會浪費掉一個字的存儲空間,因為這個比例增長的大小並不像數組那麼大。
通過這些更改,哈希碼查找不再需要經過複雜的 JavaScript 屬性查找機制。
性能改進
這一變化也導致 ARES6 中的基準測試提高了 5%。
這也導致 Emberperf 基準測試套件中測試的 Ember.js 提高了 18%。