昨天已經了解了imutable和mutable特性與觸發畫面重新渲染的關係,今天再另外深入一個與畫面更新有關的部分,也就是Reconciliation,或是稱為Patch。這個部分因為是和畫面有關係的其中一個環節,所以其實昨天已經有稍微提到它。但我覺得這個環節也算是滿重要的一部份,所以再往這個再深入了解一點點吧!
(小提醒! 本篇不會很深入談演算法的部分,只會稍微深入一點點了解Reconciliation的過程。另外,雖然Vue也有Reconciliation的過程,但這篇主要是理解React的情境。)
在正式看什麼是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。
現在我們已經知道「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"
}
}
按照慣例一樣會從type開始檢查,在這裡檢查到type是一樣的,會接著檢查這個相同HTML元素用到的屬性,確認到屬性部分不同時,會只更新屬性的部分,其他相同的部分則維持不變。這裡也可以看到,當屬性變更時,只有屬性的部份會閃一下。
這個例子是在相同的根元素中,放著多個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'
}
]
}
在這個情境下,主要會是在比較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'
},
]
}
比對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與效能的關係。
同 Day 3 留言提到的,React 並不會「監聽state的變動」,而是開發者要手動呼叫 setState
方法來觸發 re-render
感謝Zet大大,已經修改理解錯誤的地方了 :)