上次更新: 2024-07-08
這系列文章是一邊閱讀《React 思維進化:一次打破常見的觀念誤解,躍升專業前端開發者》,一邊做的筆記摘要。
由於主要是寫給自己看的筆記,所以會比較精簡,也會省略一些細節說明。從 Chapter 2 ~ Chapter 5,總共會有五篇文章。
DOM 是一種是樹狀資料結構,用來表示瀏覽器畫面中的元素。
操作 DOM 會連動瀏覽器的渲染引擎重繪畫面,因而效能成本昂貴。
Virtual DOM 就是描述、模擬實際的 DOM,本身也是一種是樹狀資料結構。
一個常見的錯誤觀念是:Virtual DOM 是複製真實 DOM 的資料。實際上應是反過來,先用 Virtual DOM 定義好期望的畫面,再依據 Virtual DOM 操作真實 DOM。
當畫面需要更新時,可以透過產生新的 Virtual DOM 結構,並比較新舊的差異,來執行最小範圍的 DOM 操作,以減少效能成本。
React element 是 React 中組成畫面的最小元素,它是一個物件,裡面包含了元素的標籤、id、屬性...等,用來描述真實 DOM 元素結構。意義上來說就是 Virtual Dom。
React 中有個 createElement 方法,就是用來產生 React element 的,而 React element 會再經由 React 處理轉換,變成實際的 DOM element。
React element 一旦建立後就無法被修改,因為它是在描述某個歷史時間點的畫面結構。
若要產生新畫面,應是建立全新的 React element,而不是修改舊有的,這樣才有 Virtaul DOM 新舊版本的比較依據。
首先要知道什麼是 react-dom,它是 React 官方開發並維護的套件,能夠把 React element 轉換並繪製成實際的 DOM element。
分成兩個階段
react-dom,將 React element 繪製成實際 DOM由於分為「定義及管理畫面描述(Reconciler)」和「將畫面結構描述繪製成實際畫面成品(Renderer)」兩個階段,只要有支援其他環境的 Renderer 配合,React 也可以用於產生其他環境的畫面。
因此能達到官方所說 "Learn once, write anywhere" 的效果。
例如:react-dom 是用於瀏覽器環境,能夠將 React element 轉為 DOM;react-native 則能產生原生 Android/iOS App 畫面。
在 React 中,會使用 createElement 方法來產生 React element,而 JSX 就是 createElement 的替代方法,讓開發者更直觀、簡單地創建 React element。
JSX 其實就是在呼叫 React.createElement。
例如以下這段 JSX 語法是不合法的,因為 JSX 其實就是呼叫 React.createElement,而它只會回傳一個元素。
另外,若以樹狀結構的觀點去思考,就能發現這是因為「樹狀結構只能有一個根節點」。
const element = (
<button>foo<button>
<div>bar<div>
)在單向資料流的概念中,資料的傳遞是單向且不可逆的。
在前端的情境,就是界定原始資料與畫面結果的因果關係:只有資料改變時,畫面才會有變動;並且畫面(也就是 Dom 元素)也不允許透過互動去逆向修改資料的源頭。
以此可以確保「資料」是畫面變動的主要變因。而畫面不會逆向去改變資料,因此資料的改變只會來自於開發者手動的觸發資料更新。
策略一
當資料更新後,人工判斷並手動修改所有受牽連的 DOM
優點:只修改受牽連的 DOM,減少多餘的 DOM 操作所造成的效能浪費
缺點:依靠人為判斷,在大型且複雜專案中不可靠,且難除錯
策略二
當資料更新後,將 DOM element 全部清除,再全部重繪
優點:開發者只需專注在資料處理,要維持單線資料流非常直覺簡單
缺點:一律全部重繪,大量 DOM 操作造成不必要的效能浪費
如 Vue 的 v-model
<input v-model="message" />
<p>{{ message }}</p>v-model 會將資料(message)傳遞到畫面,顯示在 input 及 p 裡面。v-model 自動將這個值更新回資料 message,使資料與使用者輸入保持同步。範例程式碼在這邊
在這個範例中,我們定義了三層 component,並且他們渲染時都會印出該 component 的名字。
function Component1() {
console.log('render Component 1');
return (
<>
<h1>Component 1</h1>
<Component2 />
<Component2 />
</>
);
}
function Component2() {
console.log('render Component 2');
return (
<>
<h2>Component 2</h2>
<Component3 />
<Component3 />
</>
);
}
function Component3() {
console.log('render Component 3');
return (
<>
<h3>Component 3</h3>
</>
);
}
export default function App() {
return (
<div className="App">
<Component1 />
</div>
);
}component 的 render 是由外至內的,React 會依據結構依次呼叫子 Component,並且等待子 component 執行完成後,再處理下一個子 component。
因此最後 console 結果會如下:
render Component 1
render Component 2
render Component 3
render Component 3
render Component 2
render Component 3
render Component 3
前面講到,單向資料流是指資料的流動方向是單一的、不可逆的,它常跟「以資料驅動畫面」的概念綁在一起,他們兩個是相輔相成的。
總之,當資料改變時,畫面才產生改變,而 React 中,state 就扮演這個資料的角色。
React 採用一律重繪的策略來達到單向資料流,但前端程式可能很龐大,不可能因為一個資料改變就把整個應用程式重繪。
因此,React 會以 component 當作 state 機制運作的載體以及一律重繪的界線。state 必須依附於 component 身上運作,當 state 改變觸發重繪時,只重繪此 component(以及子 component)的畫面區塊。
這個問題我從沒想過,在書上看到覺得恍然大悟,原來原因這麼簡單。
以 API 開發者的角度,若讓回傳值為一個物件會怎麼樣?
// 假設回傳值是一個物件
const { state, setState } = useState(0);開發者會需要自訂變數名稱,因此要對解構賦值的變數重新取名,這樣語法撰寫不簡潔,所以才將 useState 設計成回傳陣列。
const { state: count, setState: setCount } = useState(0);
const { state: name, setState: setName } = useState('Iris');React component 的管理機制分為兩個階段:
React 中,我們通常會把「畫面更新的流程」稱為 Reconciliation。
我們知道:要使用 setState 來更新 state;且當 state 的值有變時,就會觸發 re-render,更新畫面。
那具體到底發生什麼事呢?
用三個步驟來說明:
Object.is() 來比較新傳入的 state 值是否有變。我們以計數器為例子,再說明一次:
export function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count + 1);
};
return (
<>
<button onClick={decrement}> - </button>
<div>{count}</div>
<button onClick={increment}> + </button>
</>
);
}Object.is(0, 1) 來判斷兩個值是否相同,得到 false,順利觸發 re-render<div> 內數字改變,因此只處理此對應的實際 DOM 更新,也就是將 <div>0</div> 更新為 <div>1</div>。