陌上人如玉
公子世无双

1.2 元素与组件

1.2 元素与组件

一句话:元素(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 
// ⑧ 的深入解释:为什么「组件内定义组件」是灾难
function Parent() {
  function Child() {
    const [count, setCount] = useState(0);   // 每次渲染 Child 都是新函数
    return ;
  }
  const [n, setN] = useState(0);
  return (
    <>
      
              {/* ⚠️ 类型变了(新函数)→ React 卸载旧的、挂载新的 → 状态被重置 */}
    
  );
}

面试延伸

  1. 「React Element 和 DOM 节点有什么区别?」

Element 是描述 UI 的普通 JS 对象(不可变、轻量、无浏览器资源),DOM 节点是浏览器对象(可修改、持有布局与内存资源)。React 的工作就是把「元素树」映射为「DOM 树」,并在两次渲染之间做差异比较。Element 还可以渲染到非 DOM 目标(React Native、Canvas、SSR 字符串),这正是虚拟 DOM 跨平台能力的来源。

  1. 「函数组件和类组件怎么选?」

新代码一律用函数组件。理由是:① 逻辑复用更自然(自定义 Hook 是组合式的,不像 HOC/render props 会带来嵌套与命名冲突);② 没有 this 的绑定困扰;③ 与并发特性、React Compiler 配合更好;④ 代码更短、TS 类型推导更准。类组件仍然必要的一种场景是 Error Boundary(只有类组件能实现 componentDidCatch),但也可以引入 react-error-boundary 之类的库。

  1. 「<Comp /> 和 Comp() 有什么区别?」

<Comp /> 创建 Element 并交给 React 渲染,React 会为它建立独立的 Fiber 节点,因此 Hooks 归属正确、有独立状态、生命周期与优化都正常;Comp() 是直接调用函数,返回值被内联到父组件的渲染结果里,不产生新 Fiber——于是 Comp 里的 Hooks 会被记到父组件的 Hook 链表上,导致「Hook 调用顺序不一致」的报错或状态错乱。所以永远用 JSX 形式。

  1. 「为什么组件名必须大写开头?」

因为 JSX 编译时靠首字母大小写区分「HTML 标签」与「组件」:小写(div、span)会被编译成字符串 'div',大写(MyComp)会被编译成变量引用。所以 <myButton /> 会被当成一个自定义 HTML 标签,浏览器渲染出一个空的 <mybutton> 元素,而不会执行你的组件函数。

  1. 「React Element 是「不可变」的,那 UI 怎么会更新?」

因为更新不是「修改元素」,而是「创建一棵新的元素树」:状态变化 → 组件函数重新执行 → 产生新元素树 → React 用 diff 比较新旧树 → 只把差异应用(apply)到真实 DOM 上。这种「用新数据重新描述 UI」的模式就是声明式 UI,好处是你不需要手写「怎么改 DOM」的命令式代码。

  1. 「可以在组件内部定义组件吗?」

技术上可以,但不应该。因为每次父组件渲染都会创建新的函数引用,React 会认为「组件类型变了」,于是卸载旧的、挂载新的——导致子组件的状态与 DOM 被清空(输入框失焦、动画重置等)。正确做法是把子组件定义在组件外部(模块作用域),或者不抽成组件、直接内联返回元素。

  1. 「children 有特殊语义吗?」

在 React 里 children 没有任何特殊语义,它只是 props 的一个保留字段(JSX 的嵌套内容会被塞进它)。这与 Vue 的具名 slot 不同:React 需要多个「插槽」时,直接用普通 props 传元素即可(如 <Layout header={<Header />} footer={<Footer />} />)。children 之所以常用,是因为它让「容器组件」的写法非常简洁。

  1. 「怎么在 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 语义。

赞(0) 打赏
未经允许不得转载:陌上寒 » 1.2 元素与组件

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫