React這個框架的核心思想是,將頁面分割成一個個組件,一個組件還可能嵌套更小的組件,每個組件有自己的數據(屬性/狀態);當某個組件的數據發生變化時,更新該組件部分的視圖。更新的過程是由數據驅動的,新的數據自該組件頂層向下流向子組件,每個組件調用自己的render
方法得到新的視圖,並與之前的視圖作diff-比較差異,完成更新。這個過程就叫作reconciliation-調和。
React通過virtual dom
來實現高效的視圖更新。基本原理是用純js對象模擬dom樹,每當更新時,根據組件們的render
方法計算出新的虛擬dom樹,並與此前的虛擬dom樹作diff,得到一個patch(差異補丁),最后映射到真實dom樹上完成視圖更新。而兩棵樹的完全的 diff 算法是一個時間復雜度為 O(n^3) 的問題。但是在前端當中,很少出現跨越層級移動DOM元素的情況,所以React采用了簡化的diff算法,只會對virtual dom中同一個層級的元素進行對比,這樣算法復雜度就可以達到 O(n)。
由於React采用的diff算法是對新舊虛擬dom樹同層級的元素挨個比較,碰到循環輸出的元素時會有一些問題,比如列表。先來看一個例子:
// 舊v-dom <ul> <li>first</li> <li>second</li> </ul> // 新v-dom <ul> <li>zero</li> <li>first</li> <li>second</li> </ul>
React在diff兩棵樹時,發現原來的兩個li元素都與新v-dom中對應位置上的兩個li元素不同,就會對其修改,並向真實dom樹中插入新的second節點。實際上,我們可能只是進行了在first之前插入新zero節點的操作,而現在進行了額外的修改操作。
React官方文檔提示我們應該使用key屬性來解決上述問題。key是一個字符串,用來唯一標識同父同層級的兄弟元素。當React作diff時,只要子元素有key屬性,便會去原v-dom樹中相應位置(當前橫向比較的層級)尋找是否有同key元素,比較它們是否完全相同,若是則復用該元素,免去不必要的操作。
延續第一個例子,如果每個li元素都有key屬性:
// 舊v-dom <ul> <li key="1">first</li> <li key="2">second</li> </ul> // 新v-dom <ul> <li key="0">zero</li> <li key="1">first</li> <li key="2">second</li> </ul>
現在React就知道了,新增了key為"0"的元素,而"1"與"2"僅僅移動了位置。
key必須是字符串類型,它的取值可以用數據對象的某個唯一屬性,或是對數據進行hash來生成key。
<ul> {list.map(v=> <li key={v.idProp}>{v.text}</li>)} </ul>
但是強烈不推薦用數組index來作為key。如果數據更新僅僅是數組重新排序或在其中間位置插入新元素,那么視圖元素都將重新渲染。來看下例子:
<ul>{list.map((v,idx)=><li key={idx}>{v}</li>)}</ul> // ['a','b','c']=> <ul> <li key="0">a</li> <li key="1">b</li> <li key="2">c</li> </ul> // 數組重排 -> ['c','a','b'] => <ul> <li key="0">c</li> <li key="1">a</li> <li key="2">b</li> </ul>
React發現key為0,1,2的元素的text都變了,將會修改三者的html,而不是移動它們。
渲染同類型元素不帶key只會產生性能問題,如果渲染的是不同類型的狀態性組件,組件將會被替換,狀態丟失。
class Box extends React.Component { constructor(p) { super(p) this.state = {type: true} this.handler = this.handler.bind(this) } handler() { this.setState({ type: !this.state.type }) } render() { return (<div> <button onClick={this.handler}>haha</button> {this.state.type ? (<div><Son_1 /><Son_2 /></div>) : (<div><Son_2 /><Son_1 /></div>) } </div>) } }
如上述代碼,每次按下按鈕,原Son_1與Son_2組件的實例都將被銷毀,並創建新的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>) }
所以,碰到數組->列表的映射,或是同級元素需要移位的情況,一定要給元素加上key屬性!
結論:用key可以提升react的性能,但是在很多地方維護key會增大復制操作業務的復雜程度,需根據情況權衡