这篇文章汇总了 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 的关键所在。