筆記:React 中關於 key 的一點總結

宮商角徵羽發表於2018-10-26

為什麼要使用 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對應元件都不會被建立。

Markdown

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會執行),然後重新建立該元件(有狀態元件的constructorcomponentWillUnmount都會執行)

Markdown

還沒完( 我再簡單說兩句 ),在專案開發中,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方法,從而更新其子元件文字內容;

  • 因為元件的childreninput元件沒有變化,其又與父元件傳入的任props沒有關聯,所以input元件不會更新(即其componentWillReceiveProps方法不會被執行),導致使用者輸入的值不會變化。

這就是index作為key存在的問題,index作為key是一種反模式, 不要輕易使用index作為key。(若陣列的內容只是作為純展示,而不涉及到陣列的動態變更,還是很推薦的。)

Markdown

官網的兩個demo: demo1, demo2, 大家可以去看看。

index 的替代

歸根結底,使用index的問題在於兩次渲染的index是相同的,導致key也是相同的,回到上面?的總結 :key相同,若元件屬性有所變化,則react只更新元件對應的屬性;沒有變化則不更新

這時候,如果保證每次的 key 不同,問題不就解決了麼?

於是乎···

key={index +  Math.random()}
複製程式碼

一行神奇的程式碼就產生了。

能解決問題麼?能!是最優的麼?不是。

Markdown

翻看 官方文件 , 官方文件中明確指出 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的不穩定性質。問題解決,收工。

Markdown

補充一下問題場景:

當時遇到的問題是:需要改變的是內層元件的屬性,而key 設定在外層子元件上面。最開始使用 index 作為 key , 兩次渲染對外層子元件來說 index 是相同的,所以react 找到外層子元件識別到 key = index , 發現 key 沒變化。key 相同,若元件屬性有所變化,則react只更新元件對應的屬性;沒有變化則不更新。外層子元件上其實並沒有屬性改變,改變的屬性是位於內層元件上。所以 react 找到外層子元件就終止了。內層子元件相當於還是沒渲染到。

參考:

React之key詳解

React中文文件 key

React中文文件 key

相關文章