React Reconciliation

React...

React 讓我們可以更專注在元件開發、狀態改變後的畫面該長什麼樣子 (What)

 

而不用去思考、關注畫面怎麼更新、改變(How),讓我們可以快速開發 Web App

DOM based vs React based

const container = document.getElementById(‘container’);
const btn = document.createElement(‘button’);

btn.className = ‘btn red’;
btn.onclick = function(event) {
    if (this.classList.contains(‘red’)) {
        this.classList.remove(‘red’);
        this.classList.add(‘blue’);
    } else {
        this.classList.remove(‘blue’);
        this.classList.add(‘red’);
    }
};

container.appendChild(btn);
const Container = (props) => {

    const [color, setColor] = useState('red')
    const handleChange = () => {
        setColor(prevColor => (
          prevColor === 'red'? 'blue' : 'red')
        )
    }
    
    return (
      <div>
        <button 
           className=`btn ${color}`
           onClick={handleChange}
        >
          點我
        </button>
      </div>
    )
}

重視當下的值

接下來要怎麼操縱值

重視流程、結果

JSX 關注的是畫面

How

What

React...

我們只要關注 What ,讓 React 來處理 How

但 React 如何更新我們的畫面?

Reconciliation

Reconciliation 是 React 如何更新畫面的過程;

也有人直接將 React 的 Diff 的演算法稱作 Reconciliation

協調、調節、對帳、一致

DOM Tree

偵測到改變,vDom 準備開始比較差異

Dom 維持不變

Dom 維持不變

Dom 更新畫面

vDom
比較前後差異

vDom
更新差異到 Dom

舊 新 比較差異

將差異更新到畫面

render() 就會回傳一組新的 React Elements Tree

每當有 state 或 props 更新時...

React 需要比較 新 Tree 、舊 Tree 的差別

然後更新到 DOM

(也是 function component 的 return)

如何比較樹的差異?

刪除 c

插入 c

f 改名 a

e 改名 d

x

c

a

d

從一棵樹轉移到另一棵樹

O(n^3)

3 個 節點,27 個單位時間

100 個 節點,10,00,000 個單位時間

我有個 100 節點的 html ,
我每次狀態更新的比較要花 100 萬個工作時間?!

Complexity?

😱

那麼...

React採取什麼策略讓 component 的更新是可預測的,同時可以滿足要求高效能的應用程式

所以...

所以 React 透過兩個假設下,實作了一個                 
的啓發式 (heuristic) 演算法。

假設1:兩個不同類型的 element 會產生出不同的 tree

不會再往下進行比較了~
直接更新!

假設2:稍後揭曉

O(n)

React 如何比較樹的差異?

Complexity?

😱

👍

補充- heuristic 演算法

在 React 提到的啟發式(heuristic) 演算法,通常就是指virtual DOM diff的演算法。

heuristic,有種經驗法則的意思在!
 

在實務上,只要你能證明你的演算法效率良好且可拿到最佳解或次佳解的話。

都可以稱之為啟發式演算法

 

比如 A*搜尋演算法、雁群(PSO)演算法等

在機器學習跟人工智慧是很常探討到的研究課題之一。

Diff 演算法比什麼

1. 不同的 Element Type

round1 render: <div />
round2 render: <span />
=> [移除 <div />], [插入 <span />]
                <div>
round1 render:    <Counter />
                </div>

                <span>
round2 render:    <Counter />
                </span>

<Counter> 會被銷毀,並且在 round2 重建

Diff 演算法比什麼

2. 相同 Type 的 React DOM Element 

round1 render: <div className="before" title="stuff" />
round2 render: <div className="after" title="stuff" />
=> [取代 attribute className 為 "after"]
round1 render: <div style={{color: 'red'}} />
round2 render: <div style={{fontWeight: 'bold'}} />
=> [移除樣式 color], [新增樣式 font-weight 'bold']

這層比較完,接著遞迴往下檢查 children

Diff 演算法比什麼

3. 相同 Type 的 React Component Element 

當一個 component 更新時,該 component 的 instance .....

instance 快速回顧

class 是一個模板,供我們快速建立相似的物件
object 就是 instance of class

class Person {
  constructor(firstName,lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  eat() {
    console.log('eat')
  }
}

const ken = new Person('Ken','Chen')
const pj = new Person('PJ','Chen')

ken 這個物件是 Person 的 instance (實例)

pj 這個物件也是 Person 的 instance

Diff 演算法比什麼

3. 相同 Type 的 React Component Element 

當一個 component 更新時,該 component 的 instance 保持不變,這樣 state 能夠被保留在不同次的 render 中。

儲存既有狀態

更新未來狀態

比較兩者差異、回傳新 Tree

React 會更新該 component instance 的 props 以跟最新的 element 保持一致,並且呼叫該 instance 的 UNSAFE_componentWillReceiveProps() 、 UNSAFE_componentWillUpdate() 和 componentDidUpdate() 方法。

 

接下來,該 instance 會再呼叫 render() 方法,而 diff 算法將會遞迴處理舊的結果以及新的結果。

Diff 演算法比什麼

4. 比較遞迴處理中的 Children

React.createElement(component, props, ...children)

Diff 演算法比什麼

4. 比較遞迴處理中的 Children

round1 render: <ul><li>first</li></ul>
round2 render: <ul><li>first</li><li>second</li></ul>
=> [插入 <li>second</li>]
round1 render: <ul><li>first</li></ul>
round2 render: <ul><li>second</li><li>first</li></ul>
=> [取代 attribute textContent 為 "second"], 
   [插入 <li>first</li>]

keys

救星降臨:

🤪??

<ul>
  <li>first</li>
  <li>second</li>
</ul>
<ul>
  <li>second</li>
  <li>first</li>
</ul>
<ul>
  <li>first</li>
</ul>
<ul>
  <li>first</li>
</ul>

Diff 演算法比什麼

4. 比較遞迴處理中的 Children

round1 render: <ul><li key='1st'>first</li></ul>
round2 render: <ul><li key='2nd'>second</li><li key='1st'>first</li></ul>
=> [插入 <li>second</li>]

👍

heuristic 演算法的假設

所以 React 透過兩個假設下,實作了一個                 
的啓發式 (heuristic) 演算法。

假設1:兩個不同類型的 element 會產生出不同的 tree

假設2:開發者可以通過 key prop 來指出哪些 child element 在不同的 render 下能保持不變

O(n)

key 的案外案

<ul>
  <li>first</li>
  <li>second</li>
</ul>
<ul>
  {[<li>first</li>, <li>second</li>]}
</ul>
<ul>
  {
    list.map((todo,index)=>(
      <li key={index}>
        {todo}
      </li>
    ))
  }
</ul>
const [list, setList] = useState(['first','second'])

key 與 arr index 的火花

<ul>
  <li key={0}>first</li>
  <li key={1}>second</li>
</ul>
setList([...list].reverse())
<ul>
  <li key={0}>second</li>
  <li key={1}>first</li>
</ul>
round1 render: <ul><li>first</li><li>second</li></ul>
round2 render: <ul><li>second</li><li>first</li></ul>
=> [取代 attribute textContent 為 "second"], 
   [取代 attribute textContent 為 "first"]

🤪??

假設你的 li 是 Custom Component, 裡面除了文字,還有些 state, props 或 refs?

小結

Diff 演算法,用來比較 vitual DOM 前後差異

Reconciliation 是 React 如何更新畫面的過程;

也有人直接將 React 的 Diff 的演算法稱作 Reconciliation

小結

假設1:兩個不同類型的 element 會產生出不同的 tree
假設2:開發者可以通過 key prop 來指出哪些 child element 在不同的 render 下能保持不變

heuristic 演算法 的 4 個比較原則及 2 個假設

1. 比較 Element Type

2. 相同的 React DOM Element Type 比較

3. 相同的 React Component Element Type 比較

4. 比較遞迴處理中的 Children


 

聲明

Reconciliation 只是 React 底層其中一個實作的細節

React 會基於前述規則進行差異合併

rerender 會在所有元件呼叫 render(),

但不代表 React 一定會 unmount, remount 它們

Limitations & Trade-offs

React 官方會定期改良 heuristic 演算法

但以現階段而言,ㄧ個 subTree 只能在 Siblings 中移動

只要往父層或子層移動,必重新 rerender

heuristic 演算法,基於前述 2 個假設

1. 演算法不會特別比較 subTree,若不斷在 2 元件中切換,且底下有很相似的內容,可能有效能問題

2. Key 應該具有穩定性、唯一性、可預測性,否則可能導致效能低落、子元件 state 遺失

 

 

Limitations & Trade-offs

1. 演算法不會特別比較 subTree,若不斷在 2 元件中切換,且底下有很相似的內容,可能有效能問題

                <div>
round1 render:    <Counter />
                </div>

                <span>
round2 render:    <Counter />
                </span>

<Counter> 會被銷毀,並且在 round2 重建

2. Key 應該具有穩定性、唯一性、可預測性,否則可能導致效能低落、子元件 state 遺失

round1 render: <ul><Li key="0">first</Li><Li key="1">second</Li></ul>
round2 render: <ul><Li key="0">second</Li><Li key="1">first</Li></ul>
=> [取代 attribute textContent 為 "second"], 
   [取代 attribute textContent 為 "first"]

🤪??

render & render

React

假如你比較喜歡用影片觀看的話

歡迎點選以下連結



https://www.youtube.com/watch?v=jyd7UyAiwX4

參考資料

React Reconciliation

By 陳Ken

React Reconciliation

A brief intro about the concept of React virtual DOM update. Find sharing at https://www.youtube.com/watch?v=jyd7UyAiwX4

  • 2,239