當我們在開發React的時候,多少會遇到一些Warning,例如:map需要加上key的訊息,但是有時候卻會忽略裡面所要傳達的意思,或是很少花時間去研究需要那樣做的原因為何,這次就來重新認識他們吧!

keys的Warning

相信很多人開發React都會看過一種warning
Warning: Each child in an array or iterator should have a unique “key” prop. Check the render method of EventsTable. See fb.me/react-warning-keys for more information.

這個時候你會做什麼動作?
A. Google我來了!
B. stackoverflow開起來
C. 仔細查看Warning

如果連log的資訊都沒仔細看,直接丟到Google去搜尋問題就會看到需要在map的時候加上key。
但是為什麼要加上key這件事情可能還是一知半解,甚至連stackoverflow這種文章也很常看到。
如果仔細查看訊息,你會發現他有提供一個網址:fb.me/react-warning-keys

沒錯,請點進去!你的一個小動作,可以幫助提升團隊日後的開發品質(如果你會害怕看英文,右上角也有翻譯可以選擇)

這個時候請看這段話:
Keys help React identify which items have changed, are added, or are removed.

所以你會看到官方建議你這麼做:

1
2
3
4
5
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);

但是請注意!盡量不要使用map提供的index來當作key

1
2
3
4
5
6
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);

這個時候你可能會想說…為什麼呢?以前使用都好好的沒問題呀?

key使用index與id的比較

以下提供一個簡易的範例給大家使用,請新增欄位看看發生什麼變化:

或是你可以參考以下的圖片示範:
https://stackoverflow.com/questions/46735483/error-do-not-use-array-index-in-keys

你會發現使用index竟然發生了神奇的變化!新增的欄位竟然會取得前一個舊欄位的key,反而是最後一個舊欄位的key消失了!
這個範例故意從最前面插入欄位,主要是想展示index完全位移會有什麼變化。
主要的問題是,React為了優化效能會使用到key來辨識要更新哪些DOM,你也可以打開React Dev tools查看key。
如果key整個不一樣,將會影響到React render的過程。

index作為key的問題

React官方雖然說明是不鼓勵大家使用index作為key,也不表示全盤否定這種做法,但是一旦發生key是預期中的狀況時,可能會發生不可預期狀況。請注意喔!可能發生不可預期的狀況,因此是有機率發生不可預期的狀況,儘管自己在local開發沒遇到狀況,但是就是可能發生這種狀況,同時也可能會影響到效能。
如果你真的有使用index的需求,可以參考以下的狀況使用:

  1. 找不到可用的唯一值作為key
    官方說,基本上通常是可以在元素內找到值當作key的,如果真的真的找不到的話,使用index也是合理的情況。
    例如:你想創造loading效果的dummy UI,那你一定不會有值可以用,同時你也沒有理由去改變它。
  2. 陣列不會進行排序, 篩選或更動
    排序和篩選都會造成元素順序的更動,key綁定index的情況下更動陣列可能會發生預期外的狀況。

小結:只要確保每個欄位擁有unique且不會更動的key即可。

eslint的rule

eslint提供防止開發者使用index當作key的rule,有興趣可以參考react/no-array-index-key
加在專案上,你不會看到你的協作專案上出現

1
2
3
things.map((thing, index) => (
<Hello key={index} />
));

而是

1
2
3
things.map((thing) => (
<Hello key={thing.id} />
));

使用keys優化的動機

React官方提供了Declative API讓開發者不用去管每次渲染畫面的時候需要更新哪個Component與底層的一些變化。
React官方也怕開發者不理解他們當初設計的原理,因此寫了這篇文章
主要介紹了他們更新Component使用的Diffing演算法,滿足目前SPA高效率的渲染。

更新DOM相當於更新一個Tree

我們都知道React在渲染Component的時候,render會回傳一個DOM tree,但是一旦有state change或props change的時候,回傳的DOM就會不一樣,React如何將舊的DOM替換成新的DOM就成為討論的議題了。

官方有一段文字描述:
There are some generic solutions to this algorithmic problem of generating the minimum number of operations to transform one tree into another. However, the state of the art algorithms have a complexity in the order of O(n3) where n is the number of elements in the tree.

官方說即使是目前最快的演算法,要比較兩者節點的差異並且更新這件事情,最快也要O(n^3)的時間複雜度。
什麼意思呢?假設今天你有一個form表單,裡面有十個欄位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Form = () => {
return (
<form>
<input name="1" value="" onChange={()=>{...}}>...</input>
<input name="2" value="" onChange={()=>{...}}>...</input>
<input name="3" value="" onChange={()=>{...}}>...</input>
<input name="4" value="" onChange={()=>{...}}>...</input>
<input name="5" value="" onChange={()=>{...}}>...</input>
<input name="6" value="" onChange={()=>{...}}>...</input>
<input name="7" value="" onChange={()=>{...}}>...</input>
<input name="8" value="" onChange={()=>{...}}>...</input>
<input name="9" value="" onChange={()=>{...}}>...</input>
<input name="10" value="" onChange={()=>{...}}>...</input>
</form>
);
};

今天使用者只要更新一次input就會觸發onChange,我們來算10個element,每次觸發就會需要做1000次比較。
光是一個component進行比較就要1000次,如果今天是一個頁面大概就更可觀了,

我們再看看官方提供的話:
If we used this in React, displaying 1000 elements would require in the order of one billion comparisons. This is far too expensive.

如果有1000個元素就要更新10億次!!OMG

啟發式演算法(heuristic)

所以React開發了一種heuristic演算法,有就是假定部分狀況而成立的一種算法,速度只要O(n):
Instead, React implements a heuristic O(n) algorithm based on two assumptions:

  1. Two elements of different types will produce different trees.
  2. The developer can hint at which child elements may be stable across different renders with a key prop.

1 比較兩者的元素

基本上第一個假設是合理的,因為不同類型的元素,理所當然會產生出不同的tree。
假設今天有以下兩個節點:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Page = () => {
return (
<>
// node1
<div>
<Test />
</div>

// node2
<span>
<Test />
</span>
</>
);
}

2 使用key標示

第二種假設也是很直接的可以使用key提升效能,舉個用key提升效能的簡單例子
今天如果你希望設計一個日後可以查詢的資料,假設你有這樣的資料:

1
2
3
4
5
const data = [
{ value: 1 },
{ value: 2 },
{ value: 3 },
];

當你每次要查詢資料的時候,都會從頭到尾或是用其他搜尋法來找指定的資料
例如使用以下的方法,複雜度就會是O(n)

1
2
const targetValue = 2;
const result = data.find(currentData => currentData.value === targetValue);

但是其實你可以把這個data變成key-value的形式來優化

1
2
3
4
5
6
7
8
9
10
11
const data = {
1: {
value: 1,
},
2: {
value: 2,
}
3: {
value: 3,
}
}

讓我們試著查詢看看,你會發現經過一開始結構的優化,僅需要O(1)

1
2
const targetKey = 2;
const result = data[targetKey];

如果今天你把更動的Component key告訴React:我改動了這個Component喔,key給你
如此React就會知道,我下次需要re-render哪個component,也不需要去仔細的比較Diff了
因此這個假設很合理

Diffing演算法的預設處理

讓我們回到一開始談的新增key的問題,官方有講一段話:
By default, when recursing on the children of a DOM node, React just iterates over both lists of children at the same time and generates a mutation whenever there’s a difference.

在預設的狀況,會去比較兩者的Diff,有不同的時候會會執行mutation。

最佳狀況

這邊一樣拿官方的範例來看,這是一個改變DOM的最佳狀況,因為前兩者是一樣的元素,因此只需要在最後一個元素進行mutation。

1
2
3
4
5
6
7
8
9
10
<ul>
<li>first</li>
<li>second</li>
</ul>

<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>

最壞狀況

如果你直接新增在第一個元素上面,這樣React在迭代的時候會認為每個元素都發生了改變,以這個例子會進行三次的mutation。
如果各個元素裡面又有其他元素,想當然這個效率會不太好。

1
2
3
4
5
6
7
8
9
10
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>

<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>

Key改善Diff效能

上面介紹過Key沒設置好的狀況,這裡來介紹為何要使用Key,讓我們再看官方的敘述:
In order to solve this issue, React supports a key attribute. When children have keys, React uses the key to match children in the original tree with children in the subsequent tree.

再來看官方的範例:

1
2
3
4
5
6
7
8
9
10
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>

<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>

加上key之後,結果就大不同了,只要告訴React 2015, 2016是舊的key需要位移,2014才是新的key,那麼僅需要進行一次mutation了。

影響啟發式演算法的議題

讓我們對官方的描述做最後的討論:

  1. The algorithm will not try to match subtrees of different component types. If you see yourself alternating between two component types with very similar output, you may want to make it the same type. In practice, we haven’t found this to be an issue.
  2. Keys should be stable, predictable, and unique. Unstable keys (like those produced by Math.random()) will cause many component instances and DOM nodes to be unnecessarily recreated, which can cause performance degradation and lost state in child components.

基本上就是需要確保key是穩定且可預測的,應該盡量任何可能讓key更動的情境發生,例如:index, Math.random
否則會讓DOM有意外的情況發生(畢竟你也無法確定React的render演算法每次都是怎麼渲染畫面的)

結論

React官方紀錄了不少在開發過程中很重要的訊息,也會在上面看到使用者之前反映的問題,他們提供了哪些解決方案或是解釋。看官方文件是開發者必經的過程,React官方的文件相較其他比較起來簡單易讀很多,因此推薦大家時常花一點時間看一下,你會發現不錯的收穫喔!