
一句话:元素(Element)是描述 UI 的不可变对象,组件(Component)是返回元素的函数或类;React 渲染的起点是元素,而组件是复用的单位。
一、React Element 是什么
// 元素就是一个普通对象(JSX 的产物)
const element = Hello
;
// 等价于
const element = {
$$typeof: Symbol(react.element),
type: 'h1',
key: null,
ref: null,
props: {
className: 'title',
children: 'Hello',
},
};
元素的三个关键特性:
| 特性 | 说明 |
|---|---|
| 不可变(immutable) | 创建后不能修改;「更新 UI」是创建新的元素交给 React 比较,而不是改老元素 |
| 轻量 | 只是描述对象,不持有 DOM 引用,创建成本极低 |
| 可嵌套 | props.children 里可以放其它元素,形成树 |
// ❌ 不能这样「修改」元素
element.props.className = 'new'; // 无效且危险
// ✅ 重新创建一个元素
const newElement = Hello
;
// ✅ 用 cloneElement 基于旧元素创建新元素(谨慎使用)
const cloned = React.cloneElement(element, { className: 'new' });
「元素不可变」的实际含义:
每次渲染都会重新执行组件函数 → 产生一棵新的元素树
→ React 比较新旧两棵树(diff)→ 只把差异应用到真实 DOM
→ 这就是「声明式 UI」:你描述「应该长什么样」,React 负责怎么改
二、元素与 DOM 节点的区别
| 维度 | React Element | 真实 DOM 节点 |
|---|---|---|
| 本质 | 普通 JS 对象 | 浏览器对象(HTMLElement) |
| 创建成本 | 极低 | 高(会触发解析、样式计算、布局) |
| 能否修改 | 不可变 | 可直接修改 |
| 是否持有浏览器资源 | ❌ | ✅(内存、事件、布局信息) |
| 获取方式 | 组件返回值的部分 | ref、document.querySelector |
| 跨平台 | ✅(可渲染到 Native / Canvas) | ❌ |
// 元素 ≠ DOM 节点:下面打印的是对象,不是 DOM
function App() {
const el = Hello;
console.log(el); // { $$typeof: Symbol(react.element), type: 'div', ... }
return el;
}
// 要拿到 DOM 节点必须用 ref
function App2() {
const ref = useRef(null);
useEffect(() => {
console.log(ref.current); // 真实 DOM 节点
}, []);
return Hello;
}
三、组件的两种定义方式
// ① 函数组件(推荐)
function Welcome({ name }) {
return Hello, {name}
;
}
// 也可以用箭头函数
const Welcome2 = ({ name }) => Hello, {name}
;
// ② 类组件(了解,新代码不推荐)
class Welcome3 extends React.Component {
render() {
return Hello, {this.props.name}
;
}
}
// ③ 两者可以混用(同一个应用里)
function App() {
return (
<>
>
);
}
四、函数组件 vs 类组件
| 维度 | 函数组件 | 类组件 |
|---|---|---|
| 定义 | 返回元素的函数 | 继承 React.Component |
| 状态 | useState / useReducer |
this.state + this.setState |
| 生命周期 | useEffect 等 Hooks |
componentDidMount 等 |
| 逻辑复用 | 自定义 Hook(组合) | HOC / render props |
this |
无 | 需要处理绑定问题 |
| 代码量 | 少 | 多(样板代码) |
| 编译优化 | ✅ 天生适合优化(Compiler 主要针对函数组件) | 受限 |
| 错误边界 | ❌ 不能(只能类组件) | ✅ componentDidCatch / getDerivedStateFromError |
| 性能优化 | memo / useMemo / useCallback |
PureComponent / shouldComponentUpdate |
| 官方态度 | 推荐 | 保留(不推荐新代码使用) |
// 同一个计数器的两种写法对比
// 函数组件
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `点击了 ${count} 次`;
}, [count]); // 依赖数组控制执行时机
return ;
}
// 类组件
class Counter2 extends React.Component {
state = { count: 0 };
componentDidMount() { // 初始化
document.title = `点击了 0 次`;
}
componentDidUpdate(prevProps, prevState) { // 更新(需要自己比较)
if (prevState.count !== this.state.count) {
document.title = `点击了 ${this.state.count} 次`;
}
}
render() {
return (
);
}
}
为什么现在推荐函数组件:
① 逻辑复用更自然:自定义 Hook 把「状态 + 副作用 + 生命周期」打包,
类组件的 HOC/render props 会带来「嵌套地狱」与「来源不明」问题
② 没有 this 的困扰(不需要 bind、不担心回调里的 this 丢失)
③ 与并发特性、React Compiler 等新能力配合更好
④ 代码更短、类型推导更准确(TS 场景)
⑤ 心智模型更简单:每次渲染就是「一次函数调用」
五、组件命名与组织
命名规范:
① 组件名必须【大写开头】(否则 JSX 会当 HTML 标签)
② 用 PascalCase(`UserCard`、`OrderList`)
③ 文件名与组件名一致(`UserCard.tsx`)
④ 一个文件一个主组件(小组件可以同文件,但不要过多)
组织方式:
atoms/ 基础组件(Button、Input、Icon)
molecules/ 组合组件(SearchBar、UserCard)
organisms/ 业务组件(OrderTable、UserForm)
pages/ 页面组件(路由级)
layouts/ 布局组件(MainLayout、AuthLayout)
// 目录式组件(组件带自己样式与测试)
// components/
// UserCard/
// index.tsx
// UserCard.module.css
// UserCard.test.tsx
// components/UserCard/index.tsx
export function UserCard({ user }: { user: User }) {
return {user.name};
}
六、<Comp /> 与 Comp() 的区别(重要)
function Child() {
const [count, setCount] = useState(0);
return ;
}
function ParentA() {
return ; // ✅ 创建元素,React 把它作为独立组件渲染
}
function ParentB() {
return Child(); // ❌ 直接调用函数,返回值被内联
}
| 写法 | React 如何处理 | 后果 |
|---|---|---|
<Child /> |
创建 Element,渲染时作为独立 Fiber 节点 | ✅ Hooks 归属正确、有独立状态、可被 memo 优化 |
Child() |
直接调用函数,返回值内联到父组件 | ❌ Hooks 挂到父组件上、调用顺序错乱、无独立状态 |
为什么 `Child()` 会出问题:
React 通过 Fiber 树来管理「每个组件自己的 Hook 链表」。
`Child()` 不产生新的 Fiber 节点 → Child 的 useState 被记到 Parent 的 Hook 链表里
→ 父组件每次渲染都会「多出一组 Hook」→ 调用顺序不一致 → 报错或状态错乱
报错示例:
React has detected a change in the order of Hooks called by Parent
七、组件的 props 与 children
// 组件就是一个「接收 props 返回元素」的函数
function UserCard({ name, age, children }) {
return (
{name}
{age} 岁
{children} {/* 内容由使用方决定 */}
);
}
// 使用
来自北京
jsx child
↓ 编译后等价于
js
children只是props的一个保留字段,没有特殊语义(不像 Vue 的 slot 有名字区分)。需要多个「插槽」时,用普通 props 传元素即可(见 [1.3 Props](./1.3-Props.md))。
八、组件的组合与嵌套
// ① 组合(最常见)
function Page() {
return (
);
}
// ② 布局组件:只负责结构与样式,内容由外部注入
function Layout({ children }: { children: React.ReactNode }) {
return {children};
}
// ③ 复合组件(把子组件挂到父组件上,形成「命名空间」)
const Select = ({ children }: { children: React.ReactNode }) => (
{children}
);
Select.Option = function Option({ value, children }: OptionProps) {
return {children};
};
;
// ④ 条件组合:用表达式决定渲染哪些部分
function App({ isAdmin }) {
return (
{isAdmin && }
);
}
九、常见坑
// ① 组件名小写开头 → 被当成 HTML 标签
function myButton() { return ; }
// ❌ 渲染出一个 空标签
// ② 直接调用组件函数
return Child(); // ❌ Hooks 归属错误
// ③ 把组件当普通函数传给事件
// ⑧ 的深入解释:为什么「组件内定义组件」是灾难
function Parent() {
function Child() {
const [count, setCount] = useState(0); // 每次渲染 Child 都是新函数
return setCount((c) => c + 1)}>{count} ;
}
const [n, setN] = useState(0);
return (
<>
setN((x) => x + 1)}>父组件重渲染 {n}
{/* ⚠️ 类型变了(新函数)→ React 卸载旧的、挂载新的 → 状态被重置 */}
>
);
}
面试延伸
- 「React Element 和 DOM 节点有什么区别?」
Element 是描述 UI 的普通 JS 对象(不可变、轻量、无浏览器资源),DOM 节点是浏览器对象(可修改、持有布局与内存资源)。React 的工作就是把「元素树」映射为「DOM 树」,并在两次渲染之间做差异比较。Element 还可以渲染到非 DOM 目标(React Native、Canvas、SSR 字符串),这正是虚拟 DOM 跨平台能力的来源。
- 「函数组件和类组件怎么选?」
新代码一律用函数组件。理由是:① 逻辑复用更自然(自定义 Hook 是组合式的,不像 HOC/render props 会带来嵌套与命名冲突);② 没有 this 的绑定困扰;③ 与并发特性、React Compiler 配合更好;④ 代码更短、TS 类型推导更准。类组件仍然必要的一种场景是 Error Boundary(只有类组件能实现 componentDidCatch),但也可以引入 react-error-boundary 之类的库。
- 「
<Comp />和Comp()有什么区别?」
<Comp /> 创建 Element 并交给 React 渲染,React 会为它建立独立的 Fiber 节点,因此 Hooks 归属正确、有独立状态、生命周期与优化都正常;Comp() 是直接调用函数,返回值被内联到父组件的渲染结果里,不产生新 Fiber——于是 Comp 里的 Hooks 会被记到父组件的 Hook 链表上,导致「Hook 调用顺序不一致」的报错或状态错乱。所以永远用 JSX 形式。
- 「为什么组件名必须大写开头?」
因为 JSX 编译时靠首字母大小写区分「HTML 标签」与「组件」:小写(div、span)会被编译成字符串 'div',大写(MyComp)会被编译成变量引用。所以 <myButton /> 会被当成一个自定义 HTML 标签,浏览器渲染出一个空的 <mybutton> 元素,而不会执行你的组件函数。
- 「React Element 是「不可变」的,那 UI 怎么会更新?」
因为更新不是「修改元素」,而是「创建一棵新的元素树」:状态变化 → 组件函数重新执行 → 产生新元素树 → React 用 diff 比较新旧树 → 只把差异应用(apply)到真实 DOM 上。这种「用新数据重新描述 UI」的模式就是声明式 UI,好处是你不需要手写「怎么改 DOM」的命令式代码。
- 「可以在组件内部定义组件吗?」
技术上可以,但不应该。因为每次父组件渲染都会创建新的函数引用,React 会认为「组件类型变了」,于是卸载旧的、挂载新的——导致子组件的状态与 DOM 被清空(输入框失焦、动画重置等)。正确做法是把子组件定义在组件外部(模块作用域),或者不抽成组件、直接内联返回元素。
- 「
children有特殊语义吗?」
在 React 里 children 没有任何特殊语义,它只是 props 的一个保留字段(JSX 的嵌套内容会被塞进它)。这与 Vue 的具名 slot 不同:React 需要多个「插槽」时,直接用普通 props 传元素即可(如 <Layout header={<Header />} footer={<Footer />} />)。children 之所以常用,是因为它让「容器组件」的写法非常简洁。
- 「怎么在 JSX 里遍历一个对象渲染?」
不能用 map(对象没有 map)。应该用 Object.entries 转成数组再映射: “jsx {Object.entries(userMap).map(([id, user]) => ( <UserCard key={id} user={user} /> ))} <code> 或者用 </code>Object.keys(userMap).map((key) => ...)<code>。注意**一定要有稳定的 </code>key**,通常用对象的键。
一句话速记
元素是描述 UI 的不可变对象(
$$typeof是Symbol,防伪造),组件是返回元素的函数;永远写<Comp />不要写Comp()(后者会让 Hooks 挂错组件、调用顺序错乱);函数组件全面优于类组件(唯一例外是 Error Boundary);组件名必须大写开头;不要在其他组件内部定义组件(每次渲染都是新类型 → 状态被重置);children只是普通的 props 字段,没有 slot 语义。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!