接著我們將上一章節介紹到的一律重繪概念與流程替換成具體的 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 都不會被真正操作到:
setState
方法並傳入新的 state,React 會以 Object.is()
檢查新舊 state 是否不同
react-dom
,以更新到瀏覽器中的真實 DOM TreesetState
觸發的 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
的值來傳入 List
的 items
prop:
items
的 state 是 ['foo', 'bar', 'fizz']
setItems
的 state 更新時,這個 state 所屬的 component App
就會開始進行 re-render,而過程中就會再次調用子 component List
,並傳入新的 propsList
因為父 component(App
)的 re-render 而連帶被觸發 re-render,此時就會收到來自父 component 傳遞的新 items prop ['foo', 'bar', 'fizz', 'foo']
items
prop 來 re-render List
component,並以此類推層層往下 re-render。到這邊為止,有關於 React 畫面更新的核心機制我們就算是解析的差不多了。接下來的篇幅我們會繼續深入剖析關於更新 state 的一些細節機制。
在經過快要一年的努力後,本系列文的實體書版本推出了~其中新增並補充了許多鐵人賽版本中沒有的脈絡與細節,並以全彩印刷拉滿視覺上的閱讀體驗,現正熱銷中!有興趣的話歡迎參考看看,也歡迎分享給其他有接觸前端的朋友們,非常感謝大家~
《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