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