深度掌握 ReactJS 高級概念:前端開發者必備
這篇文章匯總了 ReactJS 中值得深入研究的高級概念。讀完后,不僅在前端面試中能更胸有成竹,還能自行開發一個類似 ReactJS 的 UI 庫。
目錄
- Rendering 的含義與過程
- Re-rendering 發生的機制及原因
- Virtual DOM 的原理
- Reconciliation 算法的運行方式
- ReactJS 的性能優化方案
1. 什么是 Rendering?它是如何進行的?
在 React 中,我們常提到 “渲染(Rendering)”。本質上,它是把 JSX 或通過 React.createElement()
生成的元素轉換為實際的 DOM 節點,讓頁面在瀏覽器中展現出來。
JSX 與 React.createElement()
JSX(JavaScript XML)是一種 React 引入的語法糖。瀏覽器只能理解 JavaScript,所以 JSX 需要先經過 Babel 編譯成 React.createElement()
的調用,才會生成所謂的 “React Element”(一個純粹的 JavaScript 對象)。
示例:
例 1
// JSX 寫法
const jsx = <h1>Hello, React!</h1>;
// Babel 轉換后
const element = React.createElement("h1", null, "Hello, React!");
例 2
const Jsx = <h1 className="title">Hello, React!</h1>;
// Babel 轉換后
const element = React.createElement("h1", { className: "title" }, "Hello, React!");
例 3
<div>
<h1>Hello</h1>
<p>Welcome to React</p>
</div>
// Babel 轉換后
const element = React.createElement(
"div",
null,
React.createElement("h1", null, "Hello"),
React.createElement("p", null, "Welcome to React")
);
例 4
const Jsx = <Card data = {cardData} />
// Babel 轉換后
const element = React.createElement(Card, { data: cardData })
React.createElement(type, props, ...children)
會返回一個描述 DOM 結構的 JS 對象,如:
{
type: "h1",
props: {
className: "title",
children: "Hello, React!"
},
key: null,
ref: null,
...
}
React 最終會根據這些對象來構造真實 DOM。
初次渲染(Initial Rendering)
初次渲染的流程大致是:
- React 組件(函數式/類)返回 JSX
- Babel 將其轉換為 React Element
- React 構建出一份虛擬的 DOM 結構(Virtual DOM)
- React 將虛擬 DOM 與真實 DOM 同步,頁面上出現相應的節點
大型應用通常有成百上千個組件嵌套,最終 React 會構建出巨大的虛擬 DOM 樹,再將其 “映射” 到真實 DOM。初次加載時生成的真實 DOM 較多,耗時也更多。
2. 什么是 Re-rendering,組件何時會重新渲染?
Re-rendering 指組件為了更新 UI,會再次執行渲染過程。React 只在需要時重新渲染,而不是盲目全量刷新,以提高效率。
觸發重新渲染的場景
- State 變化
當useState
或this.setState
更新了 state,組件會重新渲染。
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
console.log("Counter Re-Rendered!");
return (
<div>
<h1>Count: {count}</h1>
<button notallow={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default Counter;
- Props 改變
如果父組件傳遞的新 props 和舊 props 不同,子組件會重新渲染。
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<Child count={count} />
<button notallow={() => setCount(count + 1)}>Update Count</button>
</div>
);
}
function Child({ count }) {
console.log("Child Re-Rendered!");
return <h1>Count: {count}</h1>;
}
export default Parent;
- 父組件重渲染
只要父組件重新渲染,即使子組件的 props 沒變,子組件也默認跟著渲染。
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<Child />
<button onClick={() => setCount(count + 1)}>Re-Render Parent</button>
</div>
);
}
function Child() {
console.log("Child Re-Rendered!");
return <h1>Hello</h1>;
}
點按鈕后,父組件因為 state 改變而重渲染,Child 也跟著渲染。如果不想子組件重復渲染,可以使用 React.memo(Child)
,阻止不必要的更新。
React 18+ 中的嚴格模式雙重渲染
在開發模式下,<React.StrictMode>
會讓組件在初始化時執行兩次渲染,以檢測副作用。這在生產環境不會觸發,只需要知道這是為了幫助開發調試即可。
import React from "react";
import ReactDOM from "react-dom";
function App() {
console.log("Component Rendered!");
return <h1>Hello</h1>;
}
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
3. 理解 Virtual DOM
虛擬 DOM(V-DOM)是 React 在內存中維護的一份輕量級 DOM 結構,能顯著減少對真實 DOM 的頻繁操作。
- 真實 DOM 操作昂貴
- 虛擬 DOM 先在內存中對比,再只更新有差異的地方
工作流程
- 生成初始虛擬 DOM
- 數據或 props 變動時,生成新的虛擬 DOM
- 對比新舊虛擬 DOM 的差別(Diff 過程)
- 有變化的地方才更新真實 DOM
這種按需更新機制提升了性能。比方說文本從 “Count: 0” 變成 “Count: 1”,React 只會修改文本內容,而不會重新創建整個 <h1>
標簽。
4. Reconciliation:React 的高效更新算法
Reconciliation 是 React 用來高效處理 DOM 更新的過程,核心是 Diff 算法。
Diff 規則
- 不同類型的元素
如果type
變了(比如從<h1>
變<p>
,或從Card
組件變成List
組件),React 會銷毀原節點并新建節點。
function App({ showText }) {
return showText ? <h1>Hello</h1> : <p>Hello</p>;
}
- 相同類型的元素
如果type
相同,只更新變更部分。例如修改屬性或文本內容。
function App({ text }) {
return <h1 className="title">{text}</h1>;
}
將 text 從 "Hello" 改為 "World" 會使 React 僅更新文本。
- 列表中的 Key
當使用map()
渲染列表時,務必給每個項加唯一key
,這樣 React 才能跟蹤列表項,做最小化更新。如果沒有 key(或 key 不唯一),React 很可能重渲染整個列表,導致性能浪費。
代碼錯誤(無key) → diff 效率低
function List({ items }) {
return (
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
);
}
如果在開始時添加了一個新項目,React 會重新渲染所有 <li>
元素,這樣做很慢,因為 React 無法跟蹤沒有鍵的單個項目。
良好代碼(key) → 優化對賬
function List({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
5. ReactJS 的性能優化技巧
5.1 React.memo():防止不必要的子組件重復渲染
在父組件刷新而子組件 props 未變的情況下,React.memo(Child)
能阻止子組件重復渲染。
const ChildComponent = React.memo(({ count }) => {
console.log("Child render");
return <h2>Count: {count}</h2>;
});
只要 count
沒變化,就不會重復渲染。
5.2 useMemo():緩存昂貴計算結果
如果某個函數計算量大且多次使用相同參數,可以用 useMemo
緩存結果,避免重復計算。
function expensiveComputation(num) {
console.log("Computing...");
return num * 2;
}
function App() {
const [number, setNumber] = useState(5);
const memoizedValue = useMemo(() => expensiveComputation(number), [number]);
// 每次渲染,只要 number 不變,就不會重復執行 expensiveComputation
return <h2>Computed Value: {memoizedValue}</h2>;
}
5.3 useCallback():緩存函數引用,減少子組件不必要的渲染
React 每次渲染都會重新創建函數。如果子組件接收函數作為 props,默認會認為 props 變了,進而觸發子組件渲染。用 useCallback()
可以讓函數在依賴不變時保持相同引用。
function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []);
return (
<div>
<ChildComponent onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
這樣 ChildComponent
不會因為 onClick
prop 每次都換新函數而被動重渲染。
總結
ReactJS 的核心運行機制就是把 JSX 轉成 React.createElement()
調用,再把這些 “React Element” 組成虛擬 DOM。通過比較新舊虛擬 DOM 的差異(Reconciliation),React 能用最小代價更新真實 DOM。基于這個原理,就能延伸出許多優化策略,比如:
- 使用
React.memo
防止子組件反復刷新 - 通過
useMemo
、useCallback
緩存耗時操作及函數引用 - 在列表中使用
key
,避免不必要的遍歷和重繪
這些技巧能夠在大規模項目中讓性能和可維護性都大幅提升,也是真正掌握 ReactJS 的關鍵所在。