寫React/Vue項目時為什么要在列表組件中寫key,其作用是什么?
參考文章:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/1
沒有綁定key的情況下,並且在遍歷模板簡單的情況下,會導致虛擬新舊節點對比更快,節點也會復用。而這種復用是就地復用,一種鴨子辯型的復用。
什么是鴨子辯型?
鴨式辯型來自於James Whitecomb Riley的名言:“像鴨子一樣走路並且嘎嘎叫的就叫鴨子。”通過制定規則來判定對象是否實現了這個接口
以下為簡單的例子:
<div id="app"> <div v-for="i in dataList">{{ i }}</div> </div>
js
var vm = new Vue({ el: '#app', data: { dataList: [1, 2, 3, 4, 5] } })
以上的例子,v-for的內容會生成以下的dom節點數組,我們給每一個節點標記一個身份id:
[ '<div>1</div>', // id: A '<div>2</div>', // id: B '<div>3</div>', // id: C '<div>4</div>', // id: D '<div>5</div>', // id: E ]
1. 改變dataList數據,進行數據位置替換,對比改變后的數據。
vm.dataList = [4, 1, 3, 5, 2]; // 數據位置替換 // 沒有key的情況,節點位置不變,但是節點innerText內容更新了 [ '<div>4</div>', // id: A '<div>1</div>', // id: B '<div>3</div>', // id: C '<div>5</div>', // id: D '<div>2</div>', // id: E ] // 有key的情況,dom節點位置進行了交換,但是內容沒有更新 // <div v-for="i in dataList" :key="i">{{ i }}</div> [ '<div>4</div>', // id: D '<div>1</div>', // id: A '<div>3</div>', // id: C '<div>5</div>', // id: E '<div>2</div>', // id: B ]
增刪dataList列表項
vm.dataList = [3, 4, 5, 6, 7] // 數據進行增刪 // 1. 沒有key的情況,節點位置不變,內容也更新了 [ '<div>3</div>', // id: A '<div>4</div>', // id: B '<div>5</div>', // id: C '<div>6</div>', // id: D '<div>7</div>', // id: E ] // 2. 有key的情況,節點刪除了A, B節點,新增了F, G節點 // <div v-for="i in dataList" :key="i"></div> [ '<div>3</div>', // id: C '<div>4</div>', // id: D '<div>5</div>', // id: E '<div>6</div>', // id: F '<div>7</div>', // id: G ]
從以上來看,不帶有key,並且使用簡單的模板,基於這個前提下,可以更有效的復用節點,diff速度來看也是不帶key更加快速的,因為帶key在增刪節點上有耗時,這就是vue文檔所說的默認模式。但是這個並不是key作用,而是沒有key的情況下可以對節點就地復用,提高性能。
這種模式會帶來一些隱藏的副作用,比如可能不會產生過渡效果,或者在某些節點有綁定數據(表單)狀態,會出現狀態錯位,Vue文檔也說明了這個默認的模式是高效的,但是只適用於不依賴子組件狀態或臨時DOM狀態(例如:表單輸入值)的列表渲染輸出
key的作用是什么?
key是給每一個vnode的唯一id,可以依賴key,更准確,更快地拿到oldVnode中對應的vnode節點
1. 更准確
因為帶key就不是就地復用了,在someNode函數 a.key === b.key 對比中可以避免就地復用的情況,所以會更加准確。
2. 更快
利用key的唯一性生成map對象來獲取對應節點,比遍歷方式更快。
vue和react都是采用diff算法來對比新舊虛擬節點,從而更新節點。在vue的diff函數中(建議先了解一下diff算法過程)
在交叉對比中,當新節點跟舊節點頭尾交叉對比沒有結果時,會根據新節點的key去對比舊節點數組中的key,從而找到相應舊節點(這里對應的是一個key => index的map映射)。如果沒找到就認為是一個新增節點。而如果沒有key,那么就會采用遍歷查找的方式去找到對應的舊節點。一種是一個map映射,另一種是遍歷查找。相比而言,map映射的速度更快。
vue部分源碼如下:
// vue項目 src/core/vdom/patch.js -488行 // oldCh是一個舊虛擬節點數組 if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) } if (idDef(newStartVnode.key)) { // map方式獲取 idxInOld = oldKeyToIdx[newStartVnode.key] } else { // 遍歷方式獲取 idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) } // 創建map函數 function createKeyToOldIdx (children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map } // 遍歷尋找 // sameVnode是對比新舊節點是否相同的函數 function findIdxInOld (node, oldCh, start, end) { for (let i = start; i < end; i++) { const c = oldCh[i] if (isDef(c) && sameVnode(node, c)) return i; } }