接著我們將上一章節介紹到的一律重繪概念與流程替換成具體的 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 (
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>

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

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

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

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

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

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

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

總結整理一下 Reconciliation 的流程:

  • 呼叫 setState 方法並傳入新的 state,React 會以 Object.is() 檢查新舊 state 是否不同
  • 如果判定相同則直接中斷流程,不會啟動 Reconciliation
  • 如果判定不同則開始進行 Reconciliation
  • 觸發 re-render,以新的 state 重新執行一次 component function,並回傳新的 Virtual DOM Tree(React elements)
  • 將新的 Virtual DOM Tree 與前一次 render 的舊 Virtual DOM Tree,進行 diffing 比較
  • 將 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 (
          {items.map(itemName => (
            <ListItem name={itemName} />
    function App() {
      const [names, setNames] = useState(['foo', 'bar', 'fizz']);
      const handleButtonClick = () => {
        setNames([...names, 'foo']);
      return (
          <List items={names} />
          <button onClick={handleButtonClick}>
            Add foo item
          </button>
    

    上面的範例中可以看到,在 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 的一些細節機制。