陌上人如玉
公子世无双

1.2 元素与组件

1.2 元素与组件

一句话:元素(Element)是描述 UI 的不可变对象,组件(Component)是返回元素的函数或类;React 渲染的起点是元素,而组件是复用的单位。

一、React Element 是什么

// 元素就是一个普通对象(JSX 的产物)
const element = <h1 className="title">Hello</h1>;

// 等价于
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 = <h1 className="new">Hello</h1>;

// ✅ 用 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 = <div>Hello</div>;
  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 <div ref={ref}>Hello</div>;
}

三、组件的两种定义方式

// ① 函数组件(推荐)
function Welcome({ name }) {
  return <h1>Hello, {name}</h1>;
}

// 也可以用箭头函数
const Welcome2 = ({ name }) => <h1>Hello, {name}</h1>;

// ② 类组件(了解,新代码不推荐)
class Welcome3 extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

// ③ 两者可以混用(同一个应用里)
function App() {
  return (
    <>
      <Welcome name="函数组件" />
      <Welcome3 name="类组件" />
    </>
  );
}

四、函数组件 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 <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}

// 类组件
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 (
      <button onClick={() => this.setState((s) => ({ count: s.count + 1 }))}>
        {this.state.count}
      </button>
    );
  }
}

为什么现在推荐函数组件:

① 逻辑复用更自然:自定义 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 <div className={styles.card}>{user.name}</div>;
}

六、<Comp /> 与 Comp() 的区别(重要)

function Child() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}

function ParentA() {
  return <Child />;          // ✅ 创建元素,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 (
    <div className="card">
      <h3>{name}</h3>
      {age} 岁
      {children}                    {/* 内容由使用方决定 */}
    </div>
  );
}

// 使用
<UserCard name="张三" age={18}>
  <span>来自北京</span>
</UserCard>
jsx  <UserCard name="张三" age={18}>child</UserCard>
        ↓ 编译后等价于
js   <UserCard name="张三" age={18} children="child" />

children 只是 props 的一个保留字段,没有特殊语义(不像 Vue 的 slot 有名字区分)。需要多个「插槽」时,用普通 props 传元素即可(见 1.3 Props)。

八、组件的组合与嵌套

// ① 组合(最常见)
function Page() {
  return (
    <Layout>
      <Header />
      <Main>
        <Article />
      </Main>
      <Footer />
    </Layout>
  );
}

// ② 布局组件:只负责结构与样式,内容由外部注入
function Layout({ children }: { children: React.ReactNode }) {
  return <div className="layout">{children}</div>;
}

// ③ 复合组件(把子组件挂到父组件上,形成「命名空间」)
const Select = ({ children }: { children: React.ReactNode }) => (
  <div className="select">{children}</div>
);
Select.Option = function Option({ value, children }: OptionProps) {
  return <div data-value={value}>{children}</div>;
};

<Select>
  <Select.Option value="1">选项一</Select.Option>
  <Select.Option value="2">选项二</Select.Option>
</Select>;

// ④ 条件组合:用表达式决定渲染哪些部分
function App({ isAdmin }) {
  return (
    <Layout>
      <Header />
      {isAdmin && <AdminPanel />}
    </Layout>
  );
}

九、常见坑

// ① 组件名小写开头 → 被当成 HTML 标签
function myButton() { return <button />; }
<myButton />                  // ❌ 渲染出一个 <mybutton> 空标签

// ② 直接调用组件函数
return Child();                // ❌ Hooks 归属错误

// ③ 把组件当普通函数传给事件
<button onClick={handleClick()}>   // ❌ 立即执行(应传引用)
<button onClick={handleClick}>     // ✅

// ④ 返回多个根节点
return (<A /> <B />);          // ❌ 语法错误,需要 Fragment 包裹

// ⑤ 忘记 return
function Comp() {
  <div />;                     // ❌ 没有 return → 渲染为空
}

// ⑥ 箭头函数用 {} 却想返回对象
const foo = () => { name: 'a' };    // ❌ 被当作代码块
const foo2 = () => ({ name: 'a' }); // ✅

// ⑦ 类组件的 this 绑定丢失
class A extends React.Component {
  handle() { console.log(this); }
  render() { return <button onClick={this.handle} />; }   // ❌ this 为 undefined
}
// ✅ 三种修复:构造函数 bind / 类字段箭头函数 / 渲染时 bind

// ⑧ 在组件内部定义组件
function Parent() {
  function Child() { return <div />; }    // ❌ 每次渲染都是新组件 → 状态丢失
  return <Child />;
}
// ✅ 定义在组件外部,或直接内联返回元素
// ⑧ 的深入解释:为什么「组件内定义组件」是灾难
function Parent() {
  function Child() {
    const [count, setCount] = useState(0);   // 每次渲染 Child 都是新函数
    return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
  }
  const [n, setN] = useState(0);
  return (
    <>
      <button onClick={() => setN((x) => x + 1)}>父组件重渲染 {n}</button>
      <Child />        {/* ⚠️ 类型变了(新函数)→ React 卸载旧的、挂载新的 → 状态被重置 */}
    </>
  );
}

面试延伸

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

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

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

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

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

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

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

  8. 「怎么在 JSX 里遍历一个对象渲染?」
    不能用 map(对象没有 map)。应该用 Object.entries 转成数组再映射:

    {Object.entries(userMap).map(([id, user]) => (
     <UserCard key={id} user={user} />
    ))}
    

    或者用 Object.keys(userMap).map((key) => ...)。注意一定要有稳定的 key,通常用对象的键。

一句话速记

元素是描述 UI 的不可变对象($$typeof 是 Symbol,防伪造),组件是返回元素的函数;永远写 <Comp /> 不要写 Comp()(后者会让 Hooks 挂错组件、调用顺序错乱);函数组件全面优于类组件(唯一例外是 Error Boundary);组件名必须大写开头;不要在其他组件内部定义组件(每次渲染都是新类型 → 状态被重置);children 只是普通的 props 字段,没有 slot 语义。

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

评论 抢沙发

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

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

微信扫一扫

支付宝扫一扫