為什麼要使用 key
?
Motivation
When you use React, at a single point in time you can think of the render() function as creating a tree of React elements. On the next state or props update, that render() function will return a different tree of React elements. React then needs to figure out how to efficiently update the UI to match the most recent tree.
譯 :當您使用React時,您可以在單個時間點將該render()
函式視為建立React元素樹。在下一個狀態或道具更新時,該render()
函式將返回一個不同的React元素樹。然後,React
需要弄清楚如何有效地更新UI以匹配最新的樹。
這裡涉及到兩個演算法複雜度的問題,總而言之就是:
- 不同型別的兩個元素將產生不同的tree
- 開發人員可以通過
key
來暗示哪些子元素可以在不同的渲染中保持穩定
key 是如何工作的?
react中的key
屬性是一個特殊的屬性,它是出現不是給開發者用的(例如你為一個元件設定key
之後不能獲取元件的這個key
props),而是給react自己用的。
react利用key
來識別元件,它是一種身份標識標識,就像我們的身份證用來辨識一個人一樣。每個key
對應一個元件,相同的key
react認為是同一個元件,這樣後續相同的key
對應元件都不會被建立。
this.state = {
users: [{id:1,name: '張三'}, {id:2, name: '李四'}, {id: 2, name: "王五"}],
....//省略
}
render()
return(
<div>
<h3>使用者列表</h3>
{this.state.users.map(u => <div key={u.id}>{u.id}:{u.name}</div>)}
</div>
)
);
複製程式碼
上面程式碼在dom
渲染掛載後,使用者列表只有張三和李四兩個使用者,王五並沒有展示處理,主要是因為react
根據key
認為李四和王五是同一個元件,導致第一個被渲染,後續的會被丟棄掉。
key的值必須保證唯一且穩定
這樣,有了key
屬性後,就可以與元件建立了一種對應關係,react
根據key
來決定是銷燬重新建立元件還是更新元件。
-
key相同,若元件屬性有所變化,則react只更新元件對應的屬性;沒有變化則不更新。
-
key
值不同,則react先銷燬該元件(有狀態元件的componentWillUnmount
會執行),然後重新建立該元件(有狀態元件的constructor
和componentWillUnmount
都會執行)
還沒完( 我再簡單說兩句 ),在專案開發中,key屬性的使用場景最多的還是由陣列動態建立的子元件的情況,需要為每個子元件新增唯一的key屬性值。
index
的使用
在list陣列中,用key
來標識陣列建立子元件時,我的通常做法:
{this.state.data.map((v,idx) => <Item key={idx} v={v} />)}
// index 作為key方便快捷一步到位
<ul>
<li key="0">a <input type="text"/></li>
<li key="1">b <input type="text"/></li>
<li key="2">c <input type="text"/></li>
</ul>
複製程式碼
但是···
若涉及到陣列的動態變更,例如陣列新增元素、刪除元素或者重新排序等,這時index
作為key
會導致展示錯誤的資料。
{this.state.data.map((v,idx) => <Item key={idx} v={v} />)}
// 開始時:['a','b','c']=>
<ul>
<li key="0">a <input type="text"/></li>
<li key="1">b <input type="text"/></li>
<li key="2">c <input type="text"/></li>
</ul>
// 陣列重排 -> ['c','b','a'] =>
<ul>
<li key="0">c <input type="text"/></li>
<li key="1">b <input type="text"/></li>
<li key="2">a <input type="text"/></li>
</ul>
複製程式碼
上面例項中在陣列重新排序後,key
對應的例項都沒有銷燬,而是重新更新。具體更新過程我們拿key=0
的元素來說明, 陣列重新排序後:
-
元件重新
render
得到新的虛擬dom
; -
新老兩個虛擬
dom
進行diff
,新老版的都有key=0
的元件,react
認為同一個元件,則只可能更新元件; -
然後比較其
children
,發現內容的文字內容不同(由a--->c),而input
元件並沒有變化,這時觸發元件的componentWillReceiveProps
方法,從而更新其子元件文字內容; -
因為元件的
children
中input
元件沒有變化,其又與父元件傳入的任props
沒有關聯,所以input
元件不會更新(即其componentWillReceiveProps
方法不會被執行),導致使用者輸入的值不會變化。
這就是index
作為key
存在的問題,index
作為key
是一種反模式, 不要輕易使用index作為key。(若陣列的內容只是作為純展示,而不涉及到陣列的動態變更,還是很推薦的。)
官網的兩個demo: demo1, demo2, 大家可以去看看。
index
的替代
歸根結底,使用index
的問題在於兩次渲染的index
是相同的,導致key
也是相同的,回到上面?的總結 :key相同,若元件屬性有所變化,則react只更新元件對應的屬性;沒有變化則不更新。
這時候,如果保證每次的 key
不同,問題不就解決了麼?
於是乎···
key={index + Math.random()}
複製程式碼
一行神奇的程式碼就產生了。
能解決問題麼?能!是最優的麼?不是。
翻看 官方文件 , 官方文件中明確指出 Don’t pass something like Math.random() to keys
。
key應該是穩定的,可預測的和獨特的。不穩定的key
(如由其生成的key Math.random()
)將導致許多元件例項和DOM節點被不必要地重新建立,這可能導致效能下降和子元件中的丟失狀態。
所以,在不能使用random
隨機生成key
時,我們可以像下面這樣用一個全域性的localCounter
變數來新增穩定唯一的key
值。
var localCounter = 1;
this.data.forEach(el=>{
el.id = localCounter++;
});
//向陣列中動態新增元素時,
function createUser(user) {
return {
...user,
id: localCounter++
}
}
複製程式碼
所以,我最後的解決方案是全域性定義一個變數:let ONE = 1;
,然後在元件中使用 key = {ONE++}
。這樣 setSete()
的時候每次key
都產生了變化,也一定程度上避免了key
的不穩定性質。問題解決,收工。
補充一下問題場景:
當時遇到的問題是:需要改變的是內層元件的屬性,而
key
設定在外層子元件上面。最開始使用index
作為key
, 兩次渲染對外層子元件來說 index 是相同的,所以react 找到外層子元件識別到key = index
, 發現key
沒變化。key
相同,若元件屬性有所變化,則react
只更新元件對應的屬性;沒有變化則不更新。外層子元件上其實並沒有屬性改變,改變的屬性是位於內層元件上。所以react
找到外層子元件就終止了。內層子元件相當於還是沒渲染到。