iT邦幫忙

2023 iThome 鐵人賽

DAY 4
0
Modern Web

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

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

  • 分享至 

  • xImage
  •  

昨天已經了解了imutable和mutable特性與觸發畫面重新渲染的關係,今天再另外深入一個與畫面更新有關的部分,也就是Reconciliation,或是稱為Patch。這個部分因為是和畫面有關係的其中一個環節,所以其實昨天已經有稍微提到它。但我覺得這個環節也算是滿重要的一部份,所以再往這個再深入了解一點點吧!
(小提醒! 本篇不會很深入談演算法的部分,只會稍微深入一點點了解Reconciliation的過程。另外,雖然Vue也有Reconciliation的過程,但這篇主要是理解React的情境。)

畫面更新的環節之一「Reconciliation」

在正式看什麼是Reconciliation之前,我們先回顧一下昨天提到過的畫面更新的渲染機制。昨天有說到「當state有變動時,會觸發重新渲染,產生新的virtual DOM,並把這個新的Virtual DOM與舊的Virtual DOM進行比對,再將有差異的部分更新到真實的DOM」,如果要進一步分析這句話的話,其實可以把這句話分為三個步驟,分別是「透過setState對state進行改動後,用Object.is確認到state確實有變動」、「產生新的Virtual DOM」、「比較新舊virtual DOM的差異並更新到真實DOM」。在這三個步驟中,「產生新的Virtual DOM」也就是我們昨天提到過的「重新渲染(呼叫component function)」的部分,而「比較新舊virtual DOM的差異並更新到真實DOM」也就是今天的主題Reconciliation

如何判對新舊Virtual DOM有無不同?

現在我們已經知道「Reconciliation」是在進行什麼樣的事情了,那再進一步了解何謂「新舊Virtual DOM有沒有不一樣」?它的標準是什麼?

在很早之前,我對於這部分的認知其實很單純,覺得只要放在畫面上的state不一樣,在Reconciliation的過程就會認定為不一樣。例如畫面上原本顯示Apple,按一個按鈕要顯示Orange,state不一樣了,代表放到畫面上也會長得不一樣,當然就會被判定為不同而重新更新上實體的DOM。但這個情境只是包含在內的其中一個小情境,其實所謂的「比較不同」並沒有那麼的單純。

在提到「有無不同」的部份怎麼判斷之前,我們先把畫面更新的過程往前回到「產生virtual DOM」的階段。還記得Virtual DOM是用來描述實體DOM的物件嗎?那要比較差異,當然也就會是用這個描述實體DOM的物件來進行比較,而不單單只是比較顯示在畫面的值。

以下是一個簡化的Virtual DOM物件:
(這裡以React為例,但是Vue的Virtual DOM也是類似這個樣子的物件,只是結構會有點不同)

<p class="intro">My name is Phoebe.</p>

會被轉換成類似以下這個物件(這是簡化的版本)

const reactVirtualDomObject = {
  type: 'p',
  props: {
    className: 'intro',
    children: 'My name is Phoebe.'
  }
};

在比較是不是有相異時,會從最上面的type去比對,是否有改變,再往下比對其他屬性。

根元素相異時

這個例子的改動是將最外層的div(也就是根元素)替換section,裡面的card沒有變動。

<div>
  <Card />
</div>
<section>
  <Card />
</section>

上面的內容會轉換成類似這樣的Virtual DOM物件進行比較。

// 變更前
{
  type: 'div',
  props: {
    children: {
      type: 'Card',
      props: {}
    }
  }
}
// 變更後
{
  type: 'section',
  props: {
    children: {
      type: 'Card',
      props: {}
    }
  }
}

如同前面所提到過的,會先從根元素開始比較,也就是最上面的type,因為type已經改變了,React會認定從根元素往下的內容都需要更新,而將包含Count元件的部分都重新建立並更新上去。

相同元素上的屬性相異時

這個例子雖然改動前和改動後都是相同的元素,但是className這個屬性有變動。

<p className="yesterday">I'm 18 years old</p>
<p className="today">I'm 18 years old</p>

上面的內容會一樣會轉換成類似這樣的Virtual DOM物件進行比較。

// 變更前
{
  type: 'p',
  props: {
    className: 'yesterday',
    children: "I'm 18 years old"
  }
}

// 變更後
{
  type: 'p',
  props: {
    className: 'today',
    children: "I'm 18 years old"
  }
}

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

按照慣例一樣會從type開始檢查,在這裡檢查到type是一樣的,會接著檢查這個相同HTML元素用到的屬性,確認到屬性部分不同時,會只更新屬性的部分,其他相同的部分則維持不變。這裡也可以看到,當屬性變更時,只有屬性的部份會閃一下。

children內容有相異時

這個例子是在相同的根元素中,放著多個child,改動後會多一個children。

<div>
  <p>Jolin</p>
  <p>Hebe</p>
</div>
<div>
  <p>Jolin</p>
  <p>Hebe</p>
  <p>IU</p>
</div>

在這個情境中,當被轉換成Virtual DOM時,會變成類似這樣的物件。

// 變更前
{
  type: 'div',
  props: {},
  children: [
    {
      type: 'p',
      props: {},
      children: 'Jolin'
    },
    {
      type: 'p',
      props: {},
      children: 'Hebe'
    }
  ]
}

// 變更後
{
  type: 'div',
  props: {},
  children: [
    {
      type: 'p',
      props: {},
      children: 'Jolin'
    },
    {
      type: 'p',
      props: {},
      children: 'Hebe'
    },
    {
      type: 'p',
      props: {},
      children: 'IU'
    }
  ]
}

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

在這個情境下,主要會是在比較children的部分,從Jolin這個child一個個往下比對後,會發現多一個Hebe這個child,就會在原本的DOM中,插入這個新增的child,原本一樣的部分則不會被更動,所以只有在新插入的DOM元素有閃一下。

這是將新的名字插入在最下面的部分,因為比對是從上而下,所以會將上面已經先比對過的部分先保留下來,只更新有變動的部份。

但是如果今天是把新的名字插入到最上面的話,會發生什麼事情呢?

<div>
  <p>Jolin</p>
  <p>Hebe</p>
</div>
<div>
<!--  從這裡插入 -->
  <p>IU</p>
  <p>Jolin</p>
  <p>Hebe</p>
</div>

變更後的物件則會變成這個樣子。

// 變更後
{
  type: 'div',
  props: {},
  children: [
    {
      type: 'p',
      props: {},
      children: 'IU'
    },
    {
      type: 'p',
      props: {},
      children: 'Jolin'
    },
    {
      type: 'p',
      props: {},
      children: 'Hebe'
    },
  ]
}

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

比對children部份時,一樣會從上到下比對,但是當比對到第一個child時,就會發現現在這個children陣列跟原本的children陣列不相同,而認定接下來的內容都需要更新,即使接下來的幾個child是原本就存在的child,還是會把接下來的child都重新創建並更新上。所以可以看到畫面上,三個p標籤都會閃一下。

回顧&總結

看了幾個例子之後,我們再來回顧和思考一下「新舊Virtual DOM有無不同」的部份是如何判斷。當state有不同時,只是觸發再次呼叫component function產生新的virtual DOM的動作,實際上還是要比較新舊Virtual DOM這個物件後,再透過兩個新舊物件去看哪個部份有不同。這個比較的階段也就是我們這次的主題「Reconciliation」。

在比較的過程中,首先會去對照type,接著會再進一步去比對屬性等內容,最後是比對children內的child,比對方式一樣是從type開始比較。如果是從type開始(也就是我們看到的HTML元素不同)時,就會認定底下的children也會有不同,而連同底下的children都一起更新。所以不只有顯示在畫面上的state的不同,會造成新舊Virtual DOM有不同之處,包含HTML元素、屬性、children的變動,也會讓新舊Virtual DOM有不同

今天已經從幾個例子了解Reconciliation是怎麼判定新舊virtual DOM有無不同,明天會再延續這個主題,去看一個大多數人都會搞錯的情境,以及Reconciliation與效能的關係。

參考資料

Reconciliation
Introducing JSX


上一篇
【Day 3】可變特性與不可變特性&渲染的觸發與Virtual DOM的產生
下一篇
【Day 5】觸發重新渲染後的下一步 - Reconciliation (下)
系列文
從Vue學React!不只要會用,還要真的懂~30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Zet
iT邦新手 2 級 ‧ 2023-09-11 04:11:51

Day 3 留言提到的,React 並不會「監聽state的變動」,而是開發者要手動呼叫 setState 方法來觸發 re-render

感謝Zet大大,已經修改理解錯誤的地方了 :)

我要留言

立即登入留言