在上一篇中,我們介紹了 key 為何,以及如何在 React 中使用 key。
Key 可以想成是 React element list 中每個元素的 id,用來最佳化 render list 時的效能,因此通常搭配 React element list 使用:
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
在這篇中則會深入探討與觀察 key 在 React render element list 時扮演的角色。
還記得在 Render React element 篇有提到,re-render 時,React 會比較當次的 Virtual DOM 與前一次的 Virtual DOM 的差異嗎?這個比較前後 Virtual DOM 差異的方式即稱為 Diff 演算法(Reconciliation)。
在這個段落中,我們將會專注在解釋 key
在 Diff 演算法中扮演的角色,並實際從 chrome dev tool 觀察 list 中 diff 的行為。
首先我們須先了解不設定 key
時的狀況。
key
的 Diff 演算法當 React element list 發生 re-render 發生時,如果 list elements 沒有設定 key,則 React 會按照 index 順序同時遍歷當次與前一次 list 來找出差異(相當於 key={index}
)。
舉例來說,如果一個 element 被加到 list 的最後面:
// original react element list
[
<li>Peter</li>,
<li>Jamie</li>,
]
// new react element list
[
<li>Peter</li>,
<li>Jamie</li>,
<li>Henry</li>,
]
則 React 的 diff 步驟會如下:
list[0]
與後來的 list[0]
。內容皆為 <li>Peter</li>
,沒有 difflist[1]
與後來的 list[1]
。內容皆為 <li>Jamie</li>
,沒有 difflist[2]
與後來的 list[2]
。發現原本的 list
沒有第三個元素,有 diff,創建一個新的 <li>Henry</li>
element在這個狀況下,很幸運的原本資料都不會被改動到,所以 diff 的效率很好。
然而當一個 React element list 中的元素順序變動時, React 並無法聰明的知道哪個舊的 element 被換到哪個新的位置了。React 依然只會照著 key={index}
的規則逐一更新 element 而已。
舉例來說,如果一個 React element list 中加入了新的 element 到 list 的最前面,
導致原有 element 的順序被打亂了:
// original react element list
[
<li>Peter</li>,
<li>Jamie</li>,
]
// new react element list
[
<li>Henry</li>,
<li>Peter</li>,
<li>Jamie</li>,
]
則 React 的 diff 步驟會如下:
list[0]
與後來的 list[0]
。內容從 Peter
,改為 Henry
,有 difflist[1]
與後來的 list[1]
。內容從 Jamie
,改為 Peter
,有 difflist[2]
與後來的 list[2]
。發現原本的 list
沒有第三個元素,有 diff,創建一個新的 <li>Jamie</li>
element這個狀況為 worst case,diff 的效率極差。
可以看到,因為 Henry
加入了 list 的最前面,導致 Peter
跟 Jamie
在 list 中順序 index 順移了。
然而,React 並不知道 Peter
跟 Jamie
只是順序移動了,React 仍會以修改每個 DOM element 的方式來修正畫面,並很浪費的重複 create 了 Jamie
。如圖所示:
讀者也可以到 CodePen 查看 console。
key
的 Diff 演算法當 list 中的每個 React element 都有屬於自己的固定 key(id)時,React 就可以在 list 內容 / 順序變動時,識別出原本就存在 list 中的 React element 的所在位置。這樣 React 就不需要 render 新的 React element,只要把原有的 React element 移動到新的位置即可。
讓我們把剛才 worst case 範例中的每個 React element 都加上唯一值作為 key
:
// original react element list
[
<li key="Peter">Peter</li>,
<li key="Jamie">Jamie</li>,
]
// new react element list
[
<li key="Henry">Henry</li>,
<li key="Peter">Peter</li>,
<li key="Jamie">Jamie</li>,
]
現在,因為有為每個 element 都加上唯一 key
的關係,就算 list 內的 element 位置改動了,React 還是可以藉由 key
找到原來的 element。
因此 React 會知道只要 Peter
跟 Jamie
是舊有的 element,只要創建新的 element <li>Henry</li>
到最前面即可。如圖所示:
讀者也可以到 CodePen 查看 console。
如此比對下來可以發現,key
可以提高 React 在 render list 的效能。
key={index}
)的問題不設定 key,或者 key
設為為 index
可能帶來以下兩個問題:
從上面段落可以很清楚的知道,key={index}
在某些狀況下效能會非常差。
想像如果把範例的資料數量改成 999 筆,然後將第 1000 筆安插到 list 的最上方,則所有原本的 999 筆資料都將發生 re-render。然而如果 key
是設定為每個資料特有 id 的話,則 Virtual DOM 的 diff 則只會有一筆。這時候效能就會有極為顯著的差距了。
key={id}
的效能可能不會那麼顯著的優異,例如 sort 1000 筆數字的狀況。因為 DOM 並沒有提供原生 sort 的功能,因此拔掉大量的 DOM node 再重新插入可能會拖慢效能,這部分還要再研究一下。但平均來說 key={id}
的效能會比 key={index}
還要好。當 key={index}
時,也可能會使 compoennt state 錯亂導致非預期的 render 出現。
一個很經典的例子就是 list 中的 React element 包含 uncontrolled components 時進行 sort,則 uncontrolled components 不會如預期的被移動到新的位置。
舉例來說,如果要顯示一個 table,table 的每個 row 會顯示 id、input、date。除此之外,也有按鈕可以提供增加 row 與 sort table 的功能。在 sort 時,我們希望 input 裡的輸入值也會跟著一起移動。
請注意,這裡的 input
是一個 uncontrolled component,React 不會干涉其內容。
錯誤範例:
為了能夠突顯重點,以上的程式碼做了一些簡化。詳細的範例請到 CodePen 上查看。
const ToDo = (props) => (
<tr>
{/* ... */}
<td>
<input />
</td>
<td>
<label>{props.createdAt.toTimeString()}</label>
</td>
</tr>
);
class ToDoList extends React.Component {
render() {
return (
<div>
{/* ... */}
<table>
{/* ... */}
{this.state.list.map((todo, index) => (
<ToDo key={index} {...todo} />
))}
</table>
</div>
);
}
}
ReactDOM.render(<ToDoList />, document.getElementById("root"));
以上範例使用 index
作為 key
的值,會發生什麼事呢?讓我們開啟 Dev tool 看看:
從 Dev tool 上可以看到,在 input 有值的狀況下 sort,雖然時間跟 id 有改變了,但是 input 卻沒有跟著一起改變。這並不符合 input 內容也要跟著一起 sort 的需求。
再更進一步觀察,我們可以發現 DOM node 只有 label 的地方有閃爍,input 並沒有被搬移到正確的位置。
這是因為目前的 key={index}
,所以就算重新 sort,key
的值還是會在同樣的位置,導致 React 認為要改變的是 list 元素的內容,而非 list 元素的順序。所以在更新時不會有任何搬移的動作產生,input 也理所當然的被留在原本 index
的位置,造成非預期的 render 行為。
正確範例:
要修正這個問題其實很簡單,只要把 key={id}
即可。
{this.state.list.map((todo, index) => (
<ToDo key={todo.id} {...todo} />
))}
可以看到,這一次 input 有正確的跟著內容搬移了。而從 Dev tool 中的 DOM node 閃爍來看也不再是針對 label 做改變,而是對整個 row(<tr>
)搬移。
由這兩個比較可知,大部分的狀況下應盡量以 key={id}
取代 key={index}
。
在這個章節中,我們學到了 React 會預設以 key={index}
的方式來比較前後兩個 list 的 Diff。
key={index}
會有以下問題:
因此大部分的狀況下應盡量以 key={id}
取代 key={index}
。在下一章節中,我們將進一步說明選擇 key
值的最佳實踐。