接著我們將上一章節介紹到的一律重繪概念與流程替換成具體的 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
的值來傳入 List
的 items
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 的一些細節機制。