1. 程式人生 > >React之key詳解(轉)

React之key詳解(轉)

寫出來完全是因為自己不長記性啊,三次了因為給陣列設定key={index}導致資料出問題,每次都找半天找不到問題。

一個例子

有這樣的一個場景如下圖所示,有一組動態數量的input,可以增加和刪除和重新排序,陣列元素生成的元件用index作為key的值,例如下圖生成的ui展示:


上面例子中的input元件渲染的程式碼如下所示,全部完整程式碼可以參考 ==>完整code點選預覽。

{this.state.data.map((v,idx)=><Item key={idx} v={v} />)}

//Item元件render方法
render(){
   return <li>{this.props.v} <input type="text"/></li>
}

首先說明的是,若頁面中陣列內容是固定而不是動態的話,上面的程式碼也不會有什麼問題(。•ˇ‸ˇ•。 但是如此這也是不是推薦的做法)。

但是,動態陣列導致其渲染的元件就會有問題,從上面圖中你也能看出問題:陣列動態改變後,頁面上input的輸入內容跟對應的陣列元素順序不對應。

為什麼會這樣呢?本文後面會有解釋。react初學者對這可能更加迷惑,本文就來跟大家探討一下react的key用法,

react key概述

key的作用

react中的key屬性,它是一個特殊的屬性,它是出現不是給開發者用的(例如你為一個元件設定key之後不能獲取元件的這個key props),而是給react自己用的。

那麼react是怎麼用key的呢?react的作者之一Paul O’Shannessy有提到:

Key is not really about performance, it’s more about identity (which in turn leads to better performance). Randomly assigned and changing values do not form an identity

簡單來說,react利用key來識別元件,它是一種身份標識標識,就像我們的身份證用來辨識一個人一樣。每個key對應一個元件,相同的key react認為是同一個元件,這樣後續相同的key對應元件都不會被建立。例如下面程式碼:

//this.state.users內容
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屬性後,就可以與元件建立了一種對應關係,react根據key來決定是銷燬重新建立元件還是更新元件。

  • key相同,若元件屬性有所變化,則react只更新元件對應的屬性;沒有變化則不更新。

  • key值不同,則react先銷燬該元件(有狀態元件的componentWillUnmount會執行),然後重新建立該元件(有狀態元件的constructorcomponentWillUnmount都會執行)

另外需要指明的是:

key不是用來提升react的效能的,不過用好key對效能是有幫組的。

key的使用場景

在專案開發中,key屬性的使用場景最多的還是由陣列動態建立的子元件的情況,需要為每個子元件新增唯一的key屬性值。

那麼,為何由陣列動態建立的元件必須要用到key屬性呢?這跟陣列元素的動態性有關。

拿上述使用者列表的例子來說,看一下babel對上述程式碼的轉換情況:

// 轉換前
const element = (
  <div>
    <h3>使用者列表</h3>
    {[<div key={1}>1:張三</div>, <div key={2}>2:李四</div>]}
  </div>
);

// 轉換後
"use strict";

var element = React.createElement(
  "div",
  null,
  React.createElement("h3",null,"使用者列表"),
  [
    React.createElement("div",{ key: 1 },"1:張三"), 
    React.createElement("div",{ key: 2 },"2:李四")
  ]
);

有babel轉換後React.createElement中的程式碼可以看出,其它元素之所以不是必須需要key是因為不管元件的state或者props如何變化,這些元素始終佔據著React.createElement固定的位置,這個位置就是天然的key。

而由陣列建立的元件可能由於動態的操作導致重新渲染時,子元件的位置發生了變化,例如上面使用者列表子元件新增一個使用者,上面兩個使用者的位置可能變化為下面這樣:

var element = React.createElement(
  "div",
  null,
  React.createElement("h3",null,"使用者列表"),
  [
    React.createElement("div",{ key: 3 },"1:王五"), 
    React.createElement("div",{ key: 1 },"2:張三"), 
    React.createElement("div",{ key: 2 },"3:李四")
  ]
);

可以看出,陣列建立子元件的位置並不固定,動態改變的;這樣有了key屬性後,react就可以根據key值來判斷是否為同一組件。

另外,還有一種比較常見的場景:為一個有複雜繁瑣邏輯的元件新增key後,後續操作可以改變該元件的key屬性值,從而達到先銷燬之前的元件,再重新建立該元件。

key的最佳實踐

上面說到了,由陣列建立的子元件必須有key屬性,否則的話你可能見到下面這樣的warning:

Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of`ServiceInfo`. See https://fb.me/react-warning-keys for more information.

可能你會發現,這只是warning而不是error,它不是強制性的,為什麼react不強制要求用key而報error呢?其實是強制要求的,只不過react為按要求來預設上幫我們做了,它是以陣列的index作為key的。

index作為key是一種反模式

在list陣列中,用key來標識陣列建立子元件時,若陣列的內容只是作為純展示,而不涉及到陣列的動態變更,其實是可以使用index作為key的。

但是,若涉及到陣列的動態變更,例如陣列新增元素、刪除元素或者重新排序等,這時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

key的值要穩定唯一

在陣列中生成的每項都要有key屬性,並且key的值是一個永久且唯一的值,即穩定唯一。

在理想情況下,在迴圈一個物件陣列時,陣列的每一項都會有用於區分其他項的一個鍵值,相當資料庫中主鍵。這樣就可以用該屬性值作為key值。但是一般情況下可能是沒有這個屬性值的,這時就需要我們自己保證。

但是,需要指出的一點是,我們在保證陣列每項的唯一的標識時,還需要保證其值的穩定性,不能經常改變。例如下面程式碼:

{
    this.state.data.map(el=><MyComponent key={Math.random()}/>)
}

上面程式碼中中MyComponent的key值是用Math.random隨機生成的,雖然能夠保持其唯一性,但是它的值是隨機而不是穩定的,在陣列動態改變時會導致陣列元素中的每項都重新銷燬然後重新建立,有一定的效能開銷;另外可能導致一些意想不到的問題出現。所以:

key的值要保持穩定且唯一,不能使用random來生成key的值。

所以,在不能使用random隨機生成key時,我們可以像下面這樣用一個全域性的localCounter變數來新增穩定唯一的key值。

var localCounter = 1;
this.data.forEach(el=>{
    el.id = localCounter++;
});
//向陣列中動態新增元素時,
function createUser(user) {
    return {
        ...user,
        id: localCounter++
    }
}

key其它注意事項

當然除了為資料元素生成的元件要新增key,且key要穩定且唯一之外,還需要注意以下幾點:

  • key屬性是新增到自定義的子元件上,而不是子元件內部的頂層的元件上。

//MyComponent
...
render() {//error
    <div key={{item.key}}>{{item.name}}</div>
}
...

//right
<MyComponent key={{item.key}}/>
  • key值的唯一是有範圍的,即在陣列生成的同級同類型的元件上要保持唯一,而不是所有元件的key都要保持唯一

  • 不僅僅在陣列生成元件上,其他地方也可以使用key,主要是react利用key來區分元件的,相同的key表示同一個元件,react不會重新銷燬建立元件例項,只可能更新;key不同,react會銷燬已有的元件例項,重新建立元件新的例項

{
  this.state.type ? 
    <div><Son_1/><Son_2/></div>
    : <div><Son_2/><Son_1/></div>
}

例如上面程式碼中,this.state.type的值改變時,原Son_1和Son2元件的例項都將會被銷燬,並重新建立Son_1和Son_2元件新的例項,不能繼承原來的狀態,其實他們只是互換了位置。為了避免這種問題,我們可以給元件加上key。

{
  this.state.type ? 
    <div><Son_1 key="1"/><Son_2 key="2"/></div>
    : <div><Son_2 key="2" /><Son_1 key="1"/></div>
}

這樣,this.state.type的值改變時,Son_1和Son2元件的例項沒有重新建立,react只是將他們互換位置。