跳到主要内容

React 核心哲学

React 不仅仅是一个前端 UI 框架,更是一种全新的软件设计范式。理解 React 的核心哲学——声明式 UI组件化单向数据流UI=f(state)UI = f(state),是建立“React 思维”(Thinking in React)的核心关键。


1. 声明式 UI 与指令式编程的本质区别

在现代 Web 开发中,我们有两种主要的 UI 构建思想:指令式(Imperative)和声明式(Declarative)。

指令式编程(How to do)

指令式编程需要开发者精确地描述**“如何一步步改变 DOM”**以达到目标状态。

  • 缺点:当应用逻辑变复杂时,DOM 的状态变化会变得难以追踪,代码中充斥着繁琐的底层 API 操作(如 appendChildclassList.add),极易产生状态不一致的 Bug。

声明式编程(What to get)

声明式编程允许开发者只描述**“目标状态下 UI 应该长成什么样”**,而把“如何从状态 A 变换到状态 B”这一繁重复杂的 DOM 底层操作,全部托管给 React 的渲染引擎。

// 💡 指令式写法:一步步增删改 DOM 节点
function updateNotificationBadgeImperative(count: number) {
let badge = document.getElementById('badge');
if (count > 0) {
if (!badge) {
badge = document.createElement('span');
badge.id = 'badge';
badge.className = 'badge-style';
document.getElementById('header')?.appendChild(badge);
}
badge.innerText = String(count);
} else {
badge?.remove();
}
}

// 💡 声明式写法:只声明 UI 与状态的绑定关系
function NotificationBadge({ count }: { count: number }) {
return (
<div id="header">
{count > 0 && <span className="badge-style">{count}</span>}
</div>
);
}

2. 数学映射模型:UI=f(state)UI = f(state)

React 的核心设计可以用一个极其优美的数学公式来表达: UI=f(state)UI = f(state)

  • statestate(状态):应用当前的内存数据,是唯一的变量源。
  • ff(组件函数):React 的组件逻辑与框架的运行时。它是一个纯净的映射函数,在相同的输入参数下,必定产生相同的输出结果。
  • UIUI(用户界面):最终呈现给用户看到的视图。

纯函数契约与副作用隔离

为了保证这一数学公式的稳定性,React 要求组件在渲染阶段(Render Phase)必须表现得像一个纯函数

  1. 不得修改任何在渲染前就已经存在的变量或对象
  2. 相同的输入(Props/State)必须渲染出相同的 JSX 结构
  3. 渲染过程中不能产生任何副作用(所有副作用必须被严格隔离至 useEffect 或事件处理函数中)。

💡 概念透析:什么是“副作用” (Side Effect)?

副作用是指组件在计算并输出 UI (JSX) 之外,对外部世界产生的任何可观测的改动或影响。在 React 的核心设计中,渲染阶段必须是绝对“纯净”的,这是为了保障并发渲染的中断、重试以及状态更新的可预测性。

1. 常见的副作用类型
  • 修改外部变量:改写在组件渲染函数之外声明的状态(如直接修改全局变量)。
  • 操作原生 DOM:使用 document.getElementByIdwindow.addEventListener 等手动修改 React 托管之外的 DOM 树或全局对象。
  • 异步/时间控制:设置 setTimeoutsetInterval 定时器。
  • 数据 I/O 操作:发起 HTTP 请求(fetch/axios)、读写 localStorage/cookie 等。
2. 为什么不能在渲染过程中产生副作用?

如果渲染函数是非纯的(包含副作用),React 无法预测组件在不同周期的行为。尤其在 Concurrent Mode (并发模式) 下,React 可能为了优化交互体验而强行打断当前的渲染计算并抛弃结果,稍后重新渲染。

  1. 结果不可预测:组件无法保障“相同输入产出相同 UI”(破坏 UI=f(state)UI = f(state) 映射)。
  2. 触发渲染死循环:若在渲染函数体中直接调用网络请求并触发 setState,将导致 “渲染 -> 发送请求 -> setState -> 触发重渲染 -> 再次请求” 的无限死循环。
3. 副作用的正确隔离方式
  • 交互引起的副作用:如点击保存按钮、提交表单等,应当完全隔离在事件处理函数 (Event Handlers) 中。
  • 挂载/更新自动触发的副作用:如组件加载时获取数据、开启全局定时器,应当通过 useEffect Hook 声明,交由 React 在浏览器完成视图渲染并绘制到屏幕后,安全地异步执行。
// ❌ 违背纯函数契约的反模式:在渲染过程中直接修改外部变量
let guestCount = 0;

function Cup() {
guestCount = guestCount + 1; // 产生了副作用,每次渲染都会导致结果不同!
return <h2>这是第 {guestCount} 位客人的杯子</h2>;
}

// 符合核心哲学的写法:将副作用放入合适的作用域
function SafeCup({ guestId }: { guestId: number }) {
return <h2>这是第 {guestId} 位客人的杯子</h2>;
}

3. 组件化设计与单一职责原则

React 提倡**“组件化驱动”**的架构,将庞大复杂的 UI 拆分为小巧、独立、可测试且职责单一的组件。

如何合理拆分组件?

  • 单一职责原则 (SRP):一个组件应该只负责一件事。如果一个组件开始变得庞大,并且承担了多项功能(例如:既展示商品列表,又处理复杂的支付表单,还负责用户的通知轮询),就必须将其拆分为更小的子组件。
  • 表现与容器分离(可选):将只负责 UI 样式的“哑组件”(Presentational Component)与负责处理业务数据和网络请求的“聪明组件”(Container Component)进行解耦,提升复用效率。

4. 单向数据流 (Unidirectional Data Flow)

单向数据流是指数据在应用中只能沿着一个方向进行流动:

[父组件 State] ────传递 Props────> [子组件]
▲ │
│ 调用回调函数
└───────────触发更新────────────┘
  1. 向下流动:父组件可以通过 props 将状态传递给子组件。子组件只能读取 props,不能修改它。
  2. 回调向上通知:如果子组件想要改变父组件的状态,它必须调用父组件通过 props 传下来的回调函数(Callbacks),由拥有该状态的父组件亲自去修改状态。
interface TodoItemProps {
text: string;
onDelete: () => void; // 向上通信的回调函数
}

// 子组件:只读展示,通过回调触发更新
function TodoItem({ text, onDelete }: TodoItemProps) {
return (
<div className="todo-item">
<span>{text}</span>
<button onClick={onDelete}>删除</button>
</div>
);
}

// 父组件:状态源
function TodoList() {
const [todos, setTodos] = useState(['学习 React', '深入研究 Fiber']);

const handleDelete = (index: number) => {
setTodos(todos.filter((_, i) => i !== index));
};

return (
<div>
{todos.map((todo, index) => (
<TodoItem
key={index}
text={todo}
onDelete={() => handleDelete(index)}
/>
))}
</div>
);
}

单向数据流的工程优势

  • 极高的可预测性:当应用中的某个地方数据出错了,你只需要顺着数据流向上游排查,很容易就能定位到是哪个组件的哪个 setState 触发了错误修改。
  • 避免隐式依赖:彻底杜绝了传统的“双向绑定”可能引发的级联数据修改和“状态同步死循环”。

🧪 核心概念自检清单

在继续学习之前,请检查以下几点你是否都理解了:

  • 声明式 vs 指令式:能够用自己的话解释两者的本质区别,以及为什么 React 选择声明式
  • UI = f(state):理解这个公式的含义,知道改变状态会导致 UI 重新渲染
  • 纯函数契约:能够识别代码中的副作用,知道它们不应该出现在渲染函数中
  • 单向数据流:理解 Props 如何从父流向子,回调如何从子向父通信
  • 组件拆分:能够根据单一职责原则拆分一个大组件

如果有任何一项你不太确定,建议再读一遍相关章节。


⚠️ 常见误区与陷阱

误区 1:直接修改 Props

// ❌ 错误:Props 是只读的,不能直接修改
function UserCard({ user }: { user: { name: string; age: number } }) {
user.name = '新名字'; // 违反了 React 的单向数据流原则
return <div>{user.name}</div>;
}

// ✅ 正确:如果需要修改,应该在父组件进行,然后通过回调通知父组件
function UserCard({ user, onUpdate }: {
user: { name: string; age: number };
onUpdate: (newName: string) => void
}) {
const handleChangeName = () => {
onUpdate('新名字');
};
return (
<div>
<h3>{user.name}</h3>
<button onClick={handleChangeName}>改名</button>
</div>
);
}

误区 2:在渲染函数中执行副作用

// ❌ 错误:在渲染过程中发起网络请求,会导致无限请求
function UserList() {
const [users, setUsers] = useState([]);

// 每次渲染都会调用,导致无限的网络请求!
fetch('/api/users').then(res => res.json()).then(data => setUsers(data));

return <div>{users.map(u => <p key={u.id}>{u.name}</p>)}</div>;
}

// ✅ 正确:使用 useEffect 隔离副作用
import { useState, useEffect } from 'react';

function UserList() {
const [users, setUsers] = useState([]);

useEffect(() => {
// 这里的代码只在组件挂载时执行一次
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []); // 空依赖项数组表示只执行一次

return <div>{users.map(u => <p key={u.id}>{u.name}</p>)}</div>;
}

误区 3:忘记 Props 是单向的

// ❌ 错误:期望通过修改子组件来自动更新父组件
function Parent() {
const [name, setName] = useState('Alice');
return <Child name={name} />; // 传递 name
}

function Child({ name }: { name: string }) {
return (
<div>
<p>{name}</p>
<input
value={name}
onChange={e => {
// 这样修改 name 是没有效果的,因为 Props 是只读的
// 父组件的 name 不会改变
}}
/>
</div>
);
}

// ✅ 正确:通过回调函数向上通信
function Parent() {
const [name, setName] = useState('Alice');

const handleNameChange = (newName: string) => {
setName(newName);
};

return <Child name={name} onNameChange={handleNameChange} />;
}

function Child({ name, onNameChange }: {
name: string;
onNameChange: (newName: string) => void
}) {
return (
<div>
<p>{name}</p>
<input
value={name}
onChange={e => onNameChange(e.target.value)}
/>
</div>
);
}

误区 4:组件过于庞大,职责不清

// ❌ 错误:一个组件承担了太多职责
function UserManagementPage() {
// 1. 用户列表展示
// 2. 用户搜索过滤
// 3. 用户编辑表单
// 4. 用户删除确认
// 5. 网络请求与加载状态
// ... 还有 500 行代码

return <div>{/* 混乱的大杂烩 */}</div>;
}

// ✅ 正确:按功能拆分为多个小组件
function UserManagementPage() {
const [users, setUsers] = useState([]);
const [searchTerm, setSearchTerm] = useState('');

const filteredUsers = users.filter(u => u.name.includes(searchTerm));

return (
<div>
<SearchBar value={searchTerm} onChange={setSearchTerm} />
<UserList users={filteredUsers} onDelete={setUsers} />
</div>
);
}

function SearchBar({ value, onChange }: { value: string; onChange: (v: string) => void }) {
return <input value={value} onChange={e => onChange(e.target.value)} />;
}

function UserList({ users, onDelete }: { users: User[]; onDelete: (users: User[]) => void }) {
return (
<ul>
{users.map(u => (
<UserItem key={u.id} user={u} onDelete={() => onDelete(users.filter(x => x.id !== u.id))} />
))}
</ul>
);
}

function UserItem({ user, onDelete }: { user: User; onDelete: () => void }) {
return (
<li>
<span>{user.name}</span>
<button onClick={onDelete}>删除</button>
</li>
);
}

📖 进阶思考

为什么 React 坚持单向数据流?

单向数据流看起来比双向绑定(如 Vue 2 的 v-model)更繁琐,但它带来的优势是:

  1. 可预测性:数据总是从上向下流动,不会有"循环依赖"或"意外修改"
  2. 易于调试:问题来源清晰,溯源容易
  3. 性能优化:React 可以清楚地追踪到哪个状态变化导致了哪些组件更新
  4. 可测试性:组件的输入(Props)和输出(回调)都很清晰

React 如何处理复杂的跨组件通信?

当组件树过深,手动传递 Props 和回调变得繁琐时,React 提供了:

  • Context API:适合中等规模的全局状态(第二阶段学习)
  • 状态管理库(Redux、Zustand、Jotai):适合大型应用(第二阶段学习)

🎯 实战练习

练习 1:重构一个声明式组件

尝试将以下指令式代码改写为声明式的 React 组件:

// 指令式原始代码
const toggleButton = document.getElementById('toggle');
const panel = document.getElementById('panel');

toggleButton.addEventListener('click', () => {
if (panel.style.display === 'none') {
panel.style.display = 'block';
} else {
panel.style.display = 'none';
}
});
参考答案
import { useState } from 'react';

function TogglePanelDemo() {
const [isVisible, setIsVisible] = useState(false);

return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>切换</button>
{isVisible && <div id="panel">这是可见的面板</div>}
</div>
);
}

注意区别:

  • 声明式方法只关心"什么时候显示面板"(isVisible),而不关心"如何改变样式"
  • React 自动处理了 DOM 更新的细节

练习 2:设计单向数据流

设计一个"点赞按钮"组件,要求:

  • 父组件中维护"点赞数"状态
  • 子组件只负责展示和交互,不修改状态
  • 点击时通过回调通知父组件
参考答案
function Parent() {
const [likes, setLikes] = useState(0);

const handleLike = () => {
setLikes(likes + 1);
};

return <LikeButton count={likes} onLike={handleLike} />;
}

function LikeButton({ count, onLike }: { count: number; onLike: () => void }) {
return (
<button onClick={onLike}>
👍 {count} 人点赞
</button>
);
}

📚 关键术语解释

术语解释
Declarative声明式:描述"想要什么",而不是"如何做"
Imperative指令式:精确描述"如何一步步做"
Props组件的输入参数,从父传向子,是只读的
State组件的内部状态,可以修改,修改后触发重渲染
Callback回调函数,子组件通过它向父组件通信
Render Phase渲染阶段,计算新的 UI 结构
Commit Phase提交阶段,将计算结果应用到真实 DOM
Pure Function纯函数,相同输入必定产生相同输出,无副作用

🔗 下一步

理解了 React 的核心哲学后,你已经准备好深入学习:

  1. JSX 语法与规范:学习如何用 JSX 描述 UI
  2. 组件与 Props:深入掌握组件的设计与 Props 的用法