1.4 组件基础


文档摘要

1.4 组件基础 本节摘要:组件是 React 应用的基本构建块。本节用同一个计数器分别用函数组件与类组件实现,让你在动手对比中理解两者的差异;再讲透 props 与 state 的分工——props 只读传数据,state 可变管界面,以及 setState 触发重渲染的因果链;最后看组件的组合与复用。 阅读收获 阅读完本节,你应当能够: 用函数组件和类组件各写一个计数器,说明两者差异 解释 props 为什么只读、state 为什么可变 理解 setState 触发重渲染的因果链 通过 props.children 组合组件,说出组合与继承的取舍 判断何时该拆分组件 一、问题与直觉:组件是 UI 的乐高积木 上一节我们把界面写成了 JSX。

1.4 组件基础

本节摘要:组件是 React 应用的基本构建块。本节用同一个计数器分别用函数组件与类组件实现,让你在动手对比中理解两者的差异;再讲透 props 与 state 的分工——props 只读传数据,state 可变管界面,以及 setState 触发重渲染的因果链;最后看组件的组合与复用。

阅读收获

阅读完本节,你应当能够:

  1. 用函数组件和类组件各写一个计数器,说明两者差异
  2. 解释 props 为什么只读、state 为什么可变
  3. 理解 setState 触发重渲染的因果链
  4. 通过 props.children 组合组件,说出组合与继承的取舍
  5. 判断何时该拆分组件

一、问题与直觉:组件是 UI 的乐高积木

上一节我们把界面写成了 JSX。但一个真实的界面往往包含几十个区块:导航栏、侧边栏、卡片、弹窗……把它们全部写在一个巨大的函数里,很快会变成几千行的怪物,改一处牵全身。

组件的思路是:把界面拆成独立、可复用的小积木,每块积木只负责一件事。导航栏是组件,卡片是组件,卡片里的按钮也是组件。积木可以嵌套——大积木由小积木拼成,小积木还能继续拆。

SOURCE 原文的比喻很贴切:组件是 React 应用的基本砖块。它接收输入(props),返回描述界面的 React 元素。这句话值得刻在脑子里,因为整个 React 的学习都围绕它展开。

二、核心原理:组件是「props 进、JSX 出」的函数

先给组件下个精确的定义:组件是接收 props 并返回 React 元素的函数

这个概念性理解非常重要。把组件当函数看,一切豁然开朗:

  • props 是函数入参——只读,调用方传什么就是什么
  • 返回的 JSX 是函数的输出——描述界面长什么样
  • 每次渲染,本质是「用当前 props 调用组件函数,得到界面描述」

函数组件就是最直接的体现:

function Welcome(props) { return <h1>Hello, {props.name}!</h1>; }

接收 props,返回 JSX,完了。它是个纯函数——同一次调用,同样的 props,必然返回同样的 JSX。这个「纯」字是 React 的心法:纯函数易推理、易测试、易复用,后面讲 React.memo(第 3.1 节)时,记忆化的前提就是纯函数。

类组件则是另一个历史阶段的主角:

import React from 'react'; class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}!</h1>; } }

类组件继承 React.Component,必须实现 render 方法,通过 this.props 访问属性。它能持有 state 和生命周期方法——这在 Hooks 出现之前是函数组件做不到的。

用同一个计数器对比两条路

为了不让差异停留在概念,把同一个计数器分别用两种写法实现。

类组件版:

import React from 'react'; class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; this.increment = this.increment.bind(this); } increment() { this.setState({ count: this.state.count + 1 }); } render() { return ( <div> <p>Count: {this.state.count}</p> <button onClick={this.increment}>Increment</button> </div> ); } }

注意三个细节:constructor 里 super(props) 是必须的,否则 this 未正确初始化;state 在构造函数里初始化;方法里要用 this.setState 修改状态,直接 this.state.count++ 不会触发重渲染。另外 this.increment.bind(this) 是类组件处理 this 的传统姿势——JavaScript 的方法调用方式决定了 this 可能丢失,所以要手动绑定(第 2.3 节事件处理会细讲)。

函数组件版:

import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); const increment = () => { setCount(count + 1); }; return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); }

函数组件版短了一大截:没有 constructor、没有 this、没有 bind。useState 返回一个数组,解构出状态值 count 和更新函数 setCount。箭头函数天然捕获 this,不需要绑定。

对比下来,函数组件的简洁是压倒性的。这也是 React 16.8 之后官方推荐函数组件的原因——Hooks 把状态和副作用的能力给了函数组件,同时保留它原本的简洁与纯粹。

维度 类组件 函数组件
语法 类 + render 方法 纯函数
state this.state + setState useState
this 处理 需要 bind 或箭头函数 无 this
生命周期 生命周期方法 useEffect 等 Hooks
代码量
现状 维护存量项目 新代码首选

三、props 与 state 的分工:数据的两个身份

这是 React 初学者最容易混淆的一对概念,用一句话厘清:

props 是外部传进来的数据(只读),state 是组件内部的数据(可变)。

props:入参,只读

props 由父组件传入,子组件不能改。这保证了数据流方向的单一——父传子,层层往下,方向明确。如果子组件想改 props,正确做法是调用父组件传下来的回调函数,让父组件去改(第 2.3 节事件处理会看到这种「状态提升」的用法)。

function Profile(props) { return ( <div> <h2>{props.name}</h2> <p>Age: {props.age}</p> </div> ); } function App() { return <Profile name="John Doe" age={30} />; }

name="John Doe" 是字符串字面量直接写,age={30} 是数字需要花括号。props 可以是任意类型:数字、字符串、数组、对象、函数,甚至 JSX 元素(通过 children)。

state:局部变量,可变但要走正门

state 是组件自己的数据,只有组件自己可以改。关键在「怎么改」:

function Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}> 点击次数:{count} </button> ); }

改 state 必须用 setCount,不能直接 count++。原因有两层:一是 React 需要知道「状态变了」,才能安排重渲染;二是直接修改 state 对象会绕过 React 的追踪,界面不会更新,还会留下脏数据。

因果链:setState → 重渲染 → 界面更新

这条链是 React 的心脏。每次点击按钮,链就完整走一遍。理解它,你就理解了为什么「改了 state 界面才变」「不 setState 界面不变」。

💡 关键直觉:把 props 想成「别人递给你的材料」,把 state 想成「你自己柜子里的东西」。材料只能看不能用改,自己柜子里的东西想换就换——但换之前必须跟 React 打声招呼(调用 setState),否则没人知道该重画界面。

四、组件的组合:从积木到城堡

组件最强大的能力是组合——用小组件拼大组件,大组件再拼更大。

function Card(props) { return <div className="card">{props.children}</div>; } function CardTitle(props) { return <h2 className="card-title">{props.children}</h2>; } function CardBody(props) { return <div className="card-body">{props.children}</div>; } function UserProfileCard(props) { const { name, bio } = props; return ( <Card> <CardTitle>{name}</CardTitle> <CardBody>{bio}</CardBody> </Card> ); }

Card、CardTitle、CardBody 各管一件事,UserProfileCard 把它们拼起来。props.children 是组合的钥匙——容器组件不关心里面装什么,只负责提供结构。

React 强调组合而非继承。你想扩展一个组件,优先考虑「在它外面包一层」或「往它里面塞内容」,而不是继承它。组合的好处是解耦:基础组件可以独立修改而不影响使用它的组件。

方案 做法 适用
组合 嵌套 + children 传内容 React 官方推荐,绝大多数场景
继承 类继承复用 几乎不用,React 组件继承价值低

何时拆分组件

拆太碎,文件满天飞;不拆,组件臃肿。两条实用标准:

  • 职责单一:一个组件只做一件事。加载态、错误态、内容态是三种事,就该是三个组件。
  • 可复用:同一段 JSX 出现在两处以上,就该抽成组件。

SOURCE 原文演示了一个反面案例:把加载、错误、展示全塞进一个 UserProfile,然后把它拆成 LoadingIndicator、ErrorMessage、UserInfo 三个独立组件——这个「先写臃肿、再拆干净」的对比练习值得亲手做一遍。

五、工程实践:props 传递的常见模式

多层传递与状态提升

数据往往要从「拥有它的组件」传给「展示它的组件」,中间可能隔好几层。props 一层层传下去,叫 prop drilling——这在层数浅时没问题,深了就会很痛苦。第 2.5 节 Context 就是为缓解它而生的。这里先记住:状态放在尽量靠近使用它的地方,不要无脑放顶层。

用 props 传函数

props 不仅能传数据,还能传函数。这是 React 里子组件「通知父组件」的标准姿势:

function Child({ onIncrement }) { return <button onClick={onIncrement}>点我</button>; } function Parent() { const [count, setCount] = useState(0); return ( <div> <p>父组件计数:{count}</p> <Child onIncrement={() => setCount(count + 1)} /> </div> ); }

子组件 Child 不知道父组件具体怎么计数,它只知道「点击后调用 onIncrement」。这种「数据向上汇报」的模式是单向数据流的重要补充,后面事件处理章节会反复用到。

默认 props 与必填 props

真实项目里,有些 props 是可选的,需要给默认值;有些必须传,缺了要提示。函数组件里可以用默认参数:

function StatusBar({ color = 'blue', size = 'medium' }) { return <div style={{ color, fontSize: size === 'large' ? 20 : 14 }}>{/* ... */}</div>; }

类组件用静态属性 defaultProps。缺省的 props 会在调用方漏传时自动补上默认值——这比在组件内部做「如果是空就怎么样」的判断干净得多。

props 不可变性的一次实操验证

为了真正理解「props 只读」,动手做一个实验:在子组件里尝试给 props 重新赋值。

function Child({ name }) { name = '被改动了'; // 运行时会不会报错? return <p>{name}</p>; }

JavaScript 允许对函数参数重新赋值,所以这行代码不会抛错,但没有任何意义——它只改了局部变量,父组件的 props 原封不动,下次渲染又回到原值。React 的「props 只读」不是靠语法禁止,而是靠约定。真正危险的是修改 props 里的对象或数组的引用——虽然 React 不强制冻结,但你应该把它当作不可变数据对待。

六、拆组件的实战练习

SOURCE 原文的反面案例值得完整展开。先看「违反单一职责」的版本——一个组件同时管加载、错误、内容三件事:

function UserProfile(props) { const { user, isLoading, error } = props; if (isLoading) { return <div>Loading...</div>; } if (error) { return <div>Error: {error.message}</div>; } return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ); }

问题在于:加载态、错误态、内容态三种 UI 被绑在一个组件里,任何一处的样式改动都要进这个组件。拆开之后,每个组件只负责一种状态:

function LoadingIndicator() { return <div>Loading...</div>; } function ErrorMessage({ error }) { return <div>Error: {error.message}</div>; } function UserInfo({ user }) { return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ); }

拆开后的价值立刻显现:LoadingIndicator 全项目复用,ErrorMessage 可以统一错误样式,UserInfo 只管展示。改一个状态不影响另外两个。判断拆不拆,就看你有没有「这里改一处、别处受牵连」的感觉——有,就说明该拆了。拆的时机也别太早,等确实出现重复或臃肿再动手,避免为了抽象而抽象。

组件命名的讲究

命名是组件的脸面。React 社区约定用 PascalCase(首字母大写):UserProfile、ProductList。避免 MyComponent、DataDisplay 这类毫无信息量的名字——名字是给三个月后的自己看的,含糊的名字等于埋雷。一个好命名的组件,别人不看注释也能猜出它干嘛。

为什么「纯」如此重要

回到开头那个定义——组件是接收 props 并返回 JSX 的纯函数。纯函数的三个性质,对应 React 的三个保障:

第一,同样的输入必有同样的输出,这让组件可预测。同一个组件、同样的 props,在任何页面渲染结果一致,不会出现「这里正常那里抽风」的灵异现象。第二,纯函数不产生副作用,这让组件可测试。写测试时你只需要准备输入、断言输出,不需要 mock 一堆外部状态(第 3.6 节测试会展开)。第三,纯函数便于缓存,这让 React 可以做记忆化优化。React.memo 能跳过重复渲染的前提,就是「同样的 props 渲染结果必然相同」。

想保持组件的纯,就要克制两件事:不要在渲染过程中修改全局变量,不要在渲染过程中发起网络请求或写 localStorage——这些副作用应该放到事件处理或 useEffect 里。记住这条,你就避开了 React 初学者的一大类诡异 bug。

本节速览

  • 组件即函数:接收 props,返回 React 元素;纯函数思想贯穿始终
  • 函数组件 vs 类组件:函数简洁无 this,类有生命周期与 setState,新代码用函数
  • props 只读:外部传入,子组件不能改,方向单一
  • state 可变:内部数据,必须经 setState/useState 更新
  • 因果链:setState → 重渲染 → 新虚拟 DOM → Diff → 更新真实 DOM
  • 组合优于继承:用 children 和嵌套搭积木,别用继承扩展
  • 状态提升:数据放靠近使用处的组件,子组件通过回调向父组件汇报
  • 默认 props:函数组件用默认参数,类组件用 defaultProps
  • 拆分标准:职责单一 + 可复用,两者满足其一就值得拆
  • 命名规范:PascalCase、信息量足,别用 MyComponent 这类占位名
  • 纯组件:渲染不做副作用,副作用交给事件处理与 useEffect

组件基础打牢了,下一章进入核心概念。先看虚拟 DOM 与 Diff——这是 React 性能的秘密,也是理解 key、React.memo 等一切优化的地基。你已经在第 1 章里亲手写出了组件,接下来的机制讲解就有了体感支撑,学起来会比直接啃原理轻松很多。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U