iT邦幫忙

2023 iThome 鐵人賽

DAY 5
0
Modern Web

從Vue學React!不只要會用,還要真的懂~系列 第 5

【Day 5】觸發重新渲染後的下一步 - Reconciliation (下)

  • 分享至 

  • xImage
  •  

昨天已經了解Reconciliation是什麼樣的過程,以及Reconciliationy的過程中是怎麼比較新舊Virtual DOM,今天讓我們從一個判斷有沒有進行畫面更新的例子來繼續了解Reconciliation的部分。

你以為的會更新,它其實不會被更新

今天一開始先來分享一個大家可能會或已經踩到過的雷,會發生這樣的雷主要原因就是與Reconciliation有關聯。

這個畫面中有一個切換按鈕,可以切換不同的form。
https://i.imgur.com/1Q6hTqX.gif

function App() {
  const [isProfileForm, setIsProfileForm] = useState(true);
 const toggle = () => {
  setIsProfileForm(!isProfileForm)
 };
  return (
    <div className="App">
      <div className="container">
        <h1>{isProfileForm ? 'Profile' : 'Account Info' }</h1>
        {
          isProfileForm ? (
            <form>
              <input placeholder="first name"/>
              <input placeholder="last name"/>
              <input placeholder="address"/>
            </form>
          ) : (
            <form>            
              <input placeholder="account name" />
              <input placeholder="line ID" />
            </form>
          ) 
        }
      </div>
      <button onClick={toggle}>Change Form Type</button>
    </div>
  );
}

可以先思考一下當我們在input透過輸入一個欄位後,在按下切換按鈕會發生什麼事情。第一直覺應該都會覺得原本在input輸入的內容會消失,因為當我們透過按鈕切換form的類型時,就會把整組的form替換掉,尤其是下述這段程式碼就是在進行form替換的部分。

isProfileForm ? (
  <form>
    <input placeholder="first name"/>
    <input placeholder="last name"/>
    <input placeholder="address"/>
  </form>
) : (
  <form>            
    <input placeholder="account name" />
    <input placeholder="line ID" />
  </form>
) 

這樣的程式碼內容的確是我們理解並且想實作出來的邏輯,但是在Reconciliation過程中比較新舊Virtual DOM的時候,也認為整個form包含input都被替換掉了嗎?

先來看看實際上的結果是否真的如預期。
https://i.imgur.com/2c1eMzr.gif

什麼!不是換了整組form了嗎?怎麼輸入的值還停留在input上呢?

這就需要回來思考一下「在Reconciliation過程中比較新舊Virtual DOM的時候,是否也認為form包含input被替換了?」。昨天在看「如何判斷新舊Virtual DOM有無不同」的時候,有提到主要是會先比較HTML元素有無不同,再來比對屬性,如果HTML元素一樣,只有屬性不同,就只會跟新屬性的部分。說到這裡,應該稍微有些靈感了。沒錯!雖然我們透過isProfileForm替換了整組的form,但是在往下比對後,不只form沒有變動,children也依然還是input,不同的地方只有placeholder這個屬性,所以只會更新屬性,並在最下面插入一個input,而不會把整個input都移除掉再重新建立新的input。

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

從這裡就可以看到當切換form的類型時,只有placeholder和最下方插入的input會閃動一下,其他則維持不變。

如果想要讓input的內容清空,就需要用一些方式讓React在進行Reconciliation時,能夠知道現在已經替換成別的input。而這個時候就可以使用key來讓每個input都有一個獨特的身分,進而讓React可以辨識這些input雖然都是input,但其實是不同的input。

給元素一個key,讓React辨識它們的身分

當我們在input上補上一個獨一無二的key後,在比對新舊元素有無不同時,React就可以知道雖然type都是input,但是它們卻是不同key的input,所以不能只更新placeholder屬性,還必須移除掉現在原本的input,重新創建input。

{
  isProfileForm ? (
    <form>
      <input key="first-name" placeholder="first name"/>
      <input key="last-name" placeholder="last name"/>
      <input key="address" placeholder="address"/>
    </form>
  ) : (
    <form>            
      <input key="account-name" placeholder="account name" />
      <input key="line-id" placeholder="line ID" />
    </form>
  ) 
}

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

透過上面的實作結果就可以看到加上key後,當替換form type的時候,所有input就都會被替換掉,原本輸入的文字也就不會被保存在那裡。

給元素一個key,讓React知道這些元素原本就存在過

當我們給元素補上一個key之後,除了可以讓React辨識這些元素的身分,進而分辨出現在有沒有變成不同key的元素,還有一個從辨識身分延伸出來的功效,就是讓React在Reconciliation的過程中,知道某些元素原本就已經存在過,也就知道不需要再重新創建並更新這部分的DOM。

還記得昨天有提到一個,把名字加在最前面後,就算後面的child都是一樣的,還是全部都被移除,重新創建的例子嗎?那個例子也就是可以透過加上key來優化的情境。

// 插入新名字前
<div>
  <p key="Jolin">Jolin</p>
  <p key="Hebe">Hebe</p>
</div>
// 插入新名字後
<div>
  <p key="IU">IU</p>
  <p key="Jolin">Jolin</p>
  <p key="Hebe">Hebe</p>
</div>

這時候的新的Virtual DOM及舊的Virtual DOM會是類似這樣的物件。

// 改動前

{
  type: 'div',
  props: {},
  children: [
    {
      type: 'p',
      props: { key: 'Jolin' },
      children: 'Jolin'
    },
    {
      type: 'p',
      props: { key: 'Hebe' },
      children: 'Hebe'
    }
  ]
}

// 改動後
{
  type: 'div',
  props: {},
  children: [
    {
      type: 'p',
      props: { key: 'IU' },
      children: 'IU'
    },
    {
      type: 'p',
      props: { key: 'Jolin' },
      children: 'Jolin'
    },
    {
      type: 'p',
      props: { key: 'Hebe' },
      children: 'Hebe'
    }
  ]
}

https://i.imgur.com/flWNrks.gif
加上key後,React就可以確實地知道哪些child是原本就存在的內容,所以在實作結果中也就能看到實際上只有真正新加入的那個p元素有閃了一下。

key必須是獨一無二且不會變動的值

雖然加上key能達到讓React避免重新創建原本就一樣的元素,但並不是什麼值都可以被拿來幫作key使用,用在元素上的key必須要是一個唯一值。

為什麼呢?
前面我們已經透過例子了解到如果加上key,就可以讓React在進行Reconciliation中的比較環節時,清楚的知道哪些元素是原本就已經存在過的舊元素,也就是說React會把這個key當作身分證。既然key值是讓React辨識元素的身分的身分證,那這個key的值當然就必須是沒有重複且只限特定元素可以擁有的值,否則就會在順序有變動時,因為身分辨識的錯亂,而出現一些state錯亂的狀況。

有時候我們可能會因為方便使用index來當作這個key使用,雖然以index本身來說,的確是獨一無二的值,但是卻不是只有限定某元素才可以擁有的值,舉例來說只要擺放在陣列的第一個位置,那個陣列元素的index就是0,當排序有變動,這個元素的index可能就會變成1或其他數字,這樣的狀況也會導致前面提到的state的錯亂狀況。

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

實際上會出現什麼樣的情況,大家可以親自去玩玩看React官方文件中提供的範例,比較能有更深刻的感受。

用index當作key的版本
調整過後的版本

延伸補充:如果對Vue比較熟悉的朋友,應該都對key的使用不陌生,因為使用v-for跑迴圈的時候,也會需要放上key。在Vue迴圈中,用index當作key來使用時,如果是排序會變動的元素,一樣會發生上述提到的類似的問題,所以官方一樣也是建議不用把inde當作key來使用。

對於效能的影響

到目前為止我們已經了解比較新舊Virtual DOM的不同之處是什麼樣的一個過程,以及什麼樣的狀況會造成相異處的出現,還有了解key的使用。這裡再從之前提到的例子,來看看Reconciliation對於效能會有怎麼樣的影響。

一般來說,Reconciliation主要是為了減少對真實DOM操作所產生的效能消耗,因為他可以在比較的過程中,找出真正需要更新的內容,然後只更新需要更新的部份。像是如果它比對過後,發現只有class屬性有變動,它就會只更新class屬性的部會,不會將整個元素都重新創建並更新上去。但是在某些特定的操作中,還是有可能會造成一些不必要的效能消耗。

在前面我們有看到的一個例子有出現只是插入一個新的p元素在前面,就造成後面的p元素也需要重新創建更新情況。前面的提到的例子,看起來雖然好像沒有什麼大影響,因為那個例子比較單純,但是如果裡面的children內還有內涵很多層children的話,這樣因為插入一個p元素,就需要把下面的一大堆內容都重新創建替換掉的話,就很有可能對效能有明顯的負面影響。不過幸好在這個情境下我們還是可以透過加上key來讓React去辨識相同元素間的特定身分,來達到功能和效能都兼顧的效果。不過如果沒有必要的話,也還是盡可能避免這樣的操作出現會比較好。

回顧及總結

今天我們透過一個例子來看一些我們對於Reconciliation可能會出現誤解的地方,其中的重點就是Reconciliation比較差異的方法不是比較我們肉眼或是人腦認為的不同的地方,而是透過比較HTML元素、屬性、children的變動來判斷有無差異。也從這個例子延伸了解key在Reconciliation過程中,主要是用作標示元素身份,讓React在Reconciliation的時候,可以知道看起來是相同HTML元素的內容,其實已經是不同身份的元素,還有使用key的時候需要注意不要使用非唯一值及不要使用index,避免產生一些資料混亂的問題。關於認識Reconciliation的部分就在這裡告一個段落了,明天會接著了解關於元件的一生-生命週期的部份。

參考資料

Reconciliation


上一篇
【Day 4】 觸發重新渲染後的下一步 - Reconciliation (上)
下一篇
【Day 6】元件的一生!Vue的生命週期 vs. React的生命週期
系列文
從Vue學React!不只要會用,還要真的懂~30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言