iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 18
1
Modern Web

I Want To Know React系列 第 18

I Want To Know React - Key & Diff 演算法

回顧 key

上一篇中,我們介紹了 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 時扮演的角色。

Key 與 React element list Diff 演算法

還記得在 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 步驟會如下:

  1. 比較原本的 list[0] 與後來的 list[0]。內容皆為 <li>Peter</li>,沒有 diff
  2. 比較原本的 list[1] 與後來的 list[1]。內容皆為 <li>Jamie</li>,沒有 diff
  3. 比較原本的 list[2] 與後來的 list[2]。發現原本的 list 沒有第三個元素,有 diff,創建一個新的 <li>Henry</li> element
  4. React 會在適當的時機把這個 diff 更新到 DOM 上

在這個狀況下,很幸運的原本資料都不會被改動到,所以 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 步驟會如下:

  1. 比較原本的 list[0] 與後來的 list[0]。內容從 Peter,改為 Henry,有 diff
  2. 比較原本的 list[1] 與後來的 list[1]。內容從 Jamie,改為 Peter,有 diff
  3. 比較原本的 list[2] 與後來的 list[2]。發現原本的 list 沒有第三個元素,有 diff,創建一個新的 <li>Jamie</li> element
  4. React 會在適當的時機把這個 diff 更新到 DOM 上

這個狀況為 worst case,diff 的效率極差。

可以看到,因為 Henry 加入了 list 的最前面,導致 PeterJamie 在 list 中順序 index 順移了。

然而,React 並不知道 PeterJamie 只是順序移動了,React 仍會以修改每個 DOM element 的方式來修正畫面,並很浪費的重複 create 了 Jamie。如圖所示:

https://i.imgur.com/RXBjTET.gif

讀者也可以到 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 會知道只要 PeterJamie 是舊有的 element,只要創建新的 element <li>Henry</li> 到最前面即可。如圖所示:

https://i.imgur.com/I31zyqs.gif

讀者也可以到 CodePen 查看 console。

如此比對下來可以發現,key 可以提高 React 在 render list 的效能。

不設定 key( key={index})的問題

不設定 key,或者 key 設為為 index 可能帶來以下兩個問題:

  • 平均效能較差
  • 可能導致非預期的 render

平均效能較差

從上面段落可以很清楚的知道,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} 還要好。

可能導致非預期的 render

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 不會干涉其內容。

https://i.imgur.com/yZE2HKZ.png

錯誤範例:

為了能夠突顯重點,以上的程式碼做了一些簡化。詳細的範例請到 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 看看:

https://i.imgur.com/j1ujAWz.gif

從 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} />
))}

https://i.imgur.com/AL7c8xT.gif

可以看到,這一次 input 有正確的跟著內容搬移了。而從 Dev tool 中的 DOM node 閃爍來看也不再是針對 label 做改變,而是對整個 row(<tr>)搬移。

由這兩個比較可知,大部分的狀況下應盡量以 key={id} 取代 key={index}

小結

在這個章節中,我們學到了 React 會預設以 key={index} 的方式來比較前後兩個 list 的 Diff。

key={index} 會有以下問題:

  • 平均效能較差
  • 可能導致非預期的 render

因此大部分的狀況下應盡量以 key={id} 取代 key={index}。在下一章節中,我們將進一步說明選擇 key 值的最佳實踐。

參考資料


上一篇
I Want To Know React - 初探 Key
下一篇
I Want To Know React - Key 的常見值 & 最佳實踐
系列文
I Want To Know React30

尚未有邦友留言

立即登入留言