iT邦幫忙

2022 iThome 鐵人賽

DAY 11
0
Modern Web

一次打破 React 常見的學習門檻與觀念誤解系列 第 11

[Day 11] React 畫面更新的核心機制(下):Reconciliation

  • 分享至 

  • xImage
  •  

接著我們將上一章節介紹到的一律重繪概念與流程替換成具體的 React 程式來解釋:

當我們在 component 裡呼叫 setState 方法來觸發資料更新時,此時 React 會先以 Object.is() 方法來檢查新傳入的 state 是否與舊的不同,如果相同的話則判定資料沒有變化所以畫面不用更新,就會直接中斷接下來的流程。如果不同的話則代表資料有所變化,因此也可能有畫面更新的需求。

此時 React component 會自動觸發 re-render 的流程,再次執行 component function 來 render 出新的 React elements,並且與前一次 render 的 React elements 進行比較,其中比較出來有差異的部分才是真正有需要更新真實 DOM 的部分,react-dom 就會負責自動去操作更新這些 DOM elements。

以上這段「將新產生的 Virtual DOM Tree 並與舊的進行差異比較,再到真實 DOM Tree 被更新完成」的流程,在 React 中就被稱為「Reconciliation」。如果你對新舊 Virtual DOM Tree 差異比較的「diffing 演算法」具體的細節有興趣的話,可以參考 React 官方文件的這篇文章

我們以一個實際的基礎 Counter 範例來解釋:

import { useState } from 'react';

export default function CounterApp() {
	// count 是 state 目前的值,setCount 是這個 state 專用的 setState 方法
  const [count, setCount] = useState(0);

  const decrement = () => {
    // count 是目前的值,因此 setState 傳入的新值是 count - 1
    setCount(count - 1);
  };

  const increment = () => {
    // count 是目前的值,因此 setState 傳入的新值是 count + 1
    setCount(count + 1);
  };

  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  );
}

當 React 首次 render CounterApp 這個 component 時,由於此時的 state count 是預設值 0,所以會得到像這樣的 React element:

<div>
  <button onClick={decrement}>-</button>
  <span>0</span>
  <button onClick={increment}>+</button>
</div>

當我們點擊 increment button 時,會呼叫 setCount(count + 1),因此這次就會以 Object.is(0, 1) 來檢查資料是否有變更。由於比較結果是 false,此時就會觸發這個 component 的 re-render,再次重新執行這個 component function。而重新執行時的 state count 就是會更新後的值 1,因此這次 render 的結果 React element 就會長得像這樣:

<div>
  <button onClick={decrement}>-</button>
  <span>1</span>
  <button onClick={increment}>+</button>
</div>

接著 React 會將這兩段 React element 以一個 diffing 演算法進行比較,計算其中的差異之處。在這個範例中,只有 div 底下的那個 span 的文字內容是有所不同的。

因此 react-dom 就會負責去找到這個 React element 對應真實 DOM element,並進行操作。

我們可以透過瀏覽器的開發者工具來觀察效果,當我們點擊按鈕來觸發資料更新時,除了 span 以外的 DOM elements 都不會被真正操作到:

Apr-15-2022 21-15-57.gif

總結整理一下 Reconciliation 的流程:

  1. 呼叫 setState 方法並傳入新的 state,React 會以 Object.is() 檢查新舊 state 是否不同
    • 如果判定相同則直接中斷流程,不會啟動 Reconciliation
    • 如果判定不同則開始進行 Reconciliation
  2. 觸發 re-render,以新的 state 重新執行一次 component function,並回傳新的 Virtual DOM Tree(React elements)
  3. 將新的 Virtual DOM Tree 與前一次 render 的舊 Virtual DOM Tree,進行 diffing 比較
  4. 將 diffing 的差異結果移交 react-dom ,以更新到瀏覽器中的真實 DOM Tree

補充說明:setState 觸發的 re-render 會連帶觸發子 components 的 re-render

當我們呼叫 setState 方法來觸發 re-render 時,如果在 component 中有呼叫其他 component 的話,就會連帶的讓這些子 component 也進行 re-render:

import { useState } from 'react';

function ListItem({ name }) {
  return <li>item name: {name}</li>;
}

function List({ items }) {
  return (
    <ul>
      {items.map(itemName => (
        <ListItem name={itemName} />
      ))}
    </ul>
  );
}

function App() {
  const [names, setNames] = useState(['foo', 'bar', 'fizz']);
  const handleButtonClick = () => {
    setNames([...names, 'foo']);
  }

  return (
    <div>
      <List items={names} />
      <button onClick={handleButtonClick}>
        Add foo item
      </button>
    </div>
  );
}

上面的範例中可以看到,在 App 的 render 的結果中,會以 state names 的值來傳入 Listitems prop:

  • 首次 render 時 items 的 state 是 ['foo', 'bar', 'fizz']
  • 當我們點擊按鈕觸發 setItems 的 state 更新時,這個 state 所屬的 component App 就會開始進行 re-render,而過程中就會再次調用子 component List,並傳入新的 props
  • List 因為父 component(App)的 re-render 而連帶被觸發 re-render,此時就會收到來自父 component 傳遞的新 items prop ['foo', 'bar', 'fizz', 'foo']
  • 此時就會以更新後的新 state 當作 items prop 來 re-render List component,並以此類推層層往下 re-render。

到這邊為止,有關於 React 畫面更新的核心機制我們就算是解析的差不多了。接下來的篇幅我們會繼續深入剖析關於更新 state 的一些細節機制。


2024/2 更新 - 實體書平裝版本預購

在經過快要一年的努力後,本系列文的實體書版本推出了~其中新增並補充了許多鐵人賽版本中沒有的脈絡與細節,並以全彩印刷拉滿視覺上的閱讀體驗,現正熱銷中!有興趣的話歡迎參考看看,也歡迎分享給其他有接觸前端的朋友們,非常感謝大家~

《React 思維進化:一次打破常見的觀念誤解,躍升專業前端開發者》

目前首刷的軟精裝版本各大通路已經幾乎都銷售一空,接下來會再刷推出新的平裝版本:

天瓏(平裝版預購):
https://www.tenlong.com.tw/products/9786263337695

博客來(平裝版):
https://www.books.com.tw/products/0010982322

momo(平裝版):
https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=12528845


上一篇
[Day 10] React 畫面更新的核心機制(上):一律重繪渲染策略
下一篇
[Day 12] 如何在子 component 裡觸發更新父 component 的資料
系列文
一次打破 React 常見的學習門檻與觀念誤解30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言