React 核心概念
React 是一个用于构建用户界面的 JavaScript 库,由 Meta(原 Facebook)开发并维护。它以声明式、组件化的方式极大地简化了前端 UI 的开发。本文将深入探讨 React 的核心概念,涵盖 JSX、虚拟 DOM、组件化、Props 与 State、生命周期以及 React 18 的新特性。
JSX 语法
JSX 是 JavaScript 的语法扩展,全称为 JavaScript XML。它允许开发者在 JavaScript 文件中编写类似 HTML 的标记,是 React 组件的核心表达方式。
JSX 表达式
在 JSX 中,使用单花括号 {} 可以嵌入任何有效的 JavaScript 表达式:
const name = 'React';
const element = <h1>Hello, {name}!</h1>;花括号内可以放置变量、函数调用、算术运算、三元表达式等:
function Greeting({ user }) {
const now = new Date();
return (
<div>
<h1>欢迎{user ? `, ${user}` : ',访客'}!</h1>
<p>当前时间:{now.toLocaleTimeString()}</p>
<p>访问量:{getViews() * 2}</p>
</div>
);
}条件渲染
React 中条件渲染有多种方式,最常用的是三元运算符和逻辑与运算符:
function StatusBadge({ status }) {
return (
<div className="status">
{/* 三元条件渲染 */}
{status === 'online' ? <span className="dot-green" /> : <span className="dot-gray" />}
{/* 逻辑与:满足条件时才渲染 */}
{status === 'online' && <span>在线</span>}
{status === 'offline' && <span>离线</span>}
{/* 或运算符:提供默认值 */}
{status || <span>未知状态</span>}
</div>
);
}列表渲染
使用数组的 map 方法遍历数据并返回 JSX 元素数组。每个元素必须指定唯一的 key 属性:
function TaskList({ tasks }) {
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<span>{task.title}</span>
<span className={task.done ? 'done' : 'pending'}>
{task.done ? '✓ 已完成' : '○ 进行中'}
</span>
</li>
))}
</ul>
);
}JSX 的原理与 Babel 编译
JSX 并非浏览器原生支持,它需要经过 Babel 编译转换为 React.createElement 调用。例如:
// JSX 写法
const element = <h1 className="title">Hello</h1>;
// Babel 编译后
const element = React.createElement('h1', { className: 'title' }, 'Hello');React.createElement 返回一个描述 DOM 结构的普通 JavaScript 对象——即"虚拟 DOM 节点":
// createElement 返回的对象大致如下
const element = {
type: 'h1',
props: {
className: 'title',
children: 'Hello'
}
};这一编译过程让开发者可以用直观的模板语法描述 UI,同时获得了 JavaScript 的全部表达能力。
虚拟 DOM
虚拟 DOM(Virtual DOM)是 React 高性能的关键设计之一。
工作原理
React 在内存中维护一棵轻量级的 JavaScript 对象树,这棵树与真实 DOM 树一一对应。其工作流程如下:
- 创建虚拟 DOM:当组件状态发生变化时,React 构建一棵新的虚拟 DOM 树。
- Diff 比较:将新的虚拟 DOM 树与上一次的虚拟 DOM 树进行对比(Diffing)。
- 计算差异:通过高效的 Diff 算法找出两棵树之间的最小差异。
- 批量更新:将差异应用到真实 DOM 上,只更新发生变化的部分。
Diff 算法与 Reconciliation
React 的 Reconciliation(协调)过程基于以下三条核心假设,从而将 O(n³) 的复杂度降低到 O(n):
- 不同类型的元素:如果两个元素的类型不同(如从
<div>变为<span>),React 会销毁整个旧子树并重新构建新子树。 - 同类型元素:对于同类型的 DOM 元素,React 只更新变化的属性;对于同类型的组件,组件会经历更新过程。
- Key 的作用:当处理列表时,通过
key属性识别哪些元素被添加、删除或移动。
Key 的作用详解
key 帮助 React 准确识别列表中的每个元素:
// 不推荐:用索引作为 key——列表顺序变化会导致性能问题和状态混乱
function BadList({ items }) {
return items.map((item, index) => <li key={index}>{item.text}</li>);
}
// 推荐:使用稳定的唯一标识符
function GoodList({ items }) {
return items.map(item => <li key={item.id}>{item.text}</li>);
}使用稳定的 key(如数据库 ID)能确保 React 正确匹配前后元素,避免不必要的重新渲染,同时保留组件内部状态。
组件化
React 应用由一个个独立的组件组成,每个组件封装了自己的结构、样式和行为。
函数组件
函数组件是定义 React 组件最简洁的方式,也是官方推荐的方式(自 React 16.8 引入 Hooks 后):
function Welcome({ name }) {
return <h1>你好,{name}!</h1>;
}类组件
在 Hooks 出现之前,类组件是唯一能使用 state 和生命周期的方式。如今它仍然在遗留代码库中广泛存在:
class Welcome extends React.Component {
render() {
return <h1>你好,{this.props.name}!</h1>;
}
}组合 vs 继承
React 推荐使用组合而非继承来实现代码复用。组合通过 children prop 或命名 slot 模式来组织组件:
function Panel({ title, children, footer }) {
return (
<div className="panel">
<div className="panel-header">{title}</div>
<div className="panel-body">{children}</div>
<div className="panel-footer">{footer}</div>
</div>
);
}
// 使用
<Panel title="用户信息" footer={<button>保存</button>}>
<p>这里放置主体内容</p>
</Panel>组件设计原则
- 单一职责:每个组件只负责一个功能或一个 UI 片段。
- 组件大小适中:一个文件通常只包含一个主要组件,如果组件过大,考虑拆分。
- 可复用性:提取通用 UI 元素为独立组件,通过 Props 配置差异。
- 明确的数据流:数据从上往下传递,事件从下往上传递。
- 保持纯粹:相同的输入(Props)总是产生相同的输出(渲染结果)。
Props 与 State
Props 和 State 是 React 组件数据管理的两类核心。
单向数据流
React 采用自上而下的单向数据流:父组件通过 Props 将数据传给子组件,子组件不能直接修改 Props。这种设计使数据流向清晰可追溯:
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<Child count={count} />
<button onClick={() => setCount(c => c + 1)}>增加</button>
</div>
);
}
function Child({ count }) {
// count 是只读的,子组件不能修改它
return <p>当前计数:{count}</p>;
}Props 类型检查
使用 PropTypes 或 TypeScript 对 Props 进行类型检查,有助于在开发阶段捕获错误:
import PropTypes from 'prop-types';
function UserCard({ name, age, isActive, tags }) {
return (
<div className="card">
<h3>{name}</h3>
<p>年龄:{age}</p>
<p>状态:{isActive ? '活跃' : '离线'}</p>
<ul>{tags.map(t => <li key={t}>{t}</li>)}</ul>
</div>
);
}
UserCard.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
isActive: PropTypes.bool,
tags: PropTypes.arrayOf(PropTypes.string)
};State 更新机制
State 更新是异步的。当调用 setState 或 setXxx 时,React 会将更新加入队列,在合适的时机批量执行:
const [count, setCount] = useState(0);
// 依赖旧状态时,使用函数式更新
setCount(prev => prev + 1);
setCount(prev => prev + 1);
// 最终 count 增加 2
// 直接传入值不会合并,第二次覆盖第一次
setCount(count + 1);
setCount(count + 1);
// 最终 count 只增加 1(两次都基于同一个快照)不可变性
React 强烈推荐以不可变方式更新状态。这意味着不要直接修改状态对象,而是创建新的副本:
// ❌ 错误的做法——直接修改状态
const [user, setUser] = useState({ name: 'Tom', skills: ['JS'] });
user.skills.push('React'); // 直接修改原对象
setUser(user); // React 无法检测到变化
// ✅ 正确的做法——创建新对象
setUser({
...user,
skills: [...user.skills, 'React']
});不可变性保证了 React 可以高效地通过浅比较(shallow comparison)检测状态变化,避免不必要的重新渲染。
生命周期
类组件生命周期方法
类组件的生命周期分为三个阶段:
| 阶段 | 方法 | 用途 |
|---|---|---|
| 挂载 | constructor | 初始化 state、绑定事件 |
| 挂载 | render | 返回 JSX |
| 挂载 | componentDidMount | DOM 已挂载,适合发起网络请求、添加订阅 |
| 更新 | shouldComponentUpdate | 性能优化,决定是否重新渲染 |
| 更新 | render | 重新渲染 |
| 更新 | componentDidUpdate | 更新后操作,处理 Props/State 变化 |
| 卸载 | componentWillUnmount | 清理定时器、取消订阅 |
useEffect 映射
函数组件通过 useEffect 模拟生命周期:
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// 挂载时 + 更新时(componentDidMount + componentDidUpdate)
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]); // 仅在 userId 变化时重新执行
// 仅挂载时执行一次(componentDidMount)
useEffect(() => {
const timer = setInterval(() => console.log('心跳'), 5000);
// 返回清理函数(componentWillUnmount)
return () => clearInterval(timer);
}, []);
// 任意状态变化时执行(无依赖数组)
useEffect(() => {
document.title = user ? `用户:${user.name}` : '加载中...';
});
return <div>{/* 渲染逻辑 */}</div>;
}关键对应关系:
componentDidMount→useEffect(fn, [])componentDidUpdate→useEffect(fn, [dep1, dep2])componentWillUnmount→useEffect(() => { return () => cleanup; }, [])
React 18 新特性概览
React 18 引入了多项重要改进,进一步提升了用户体验和开发体验。
Automatic Batching
React 18 默认启用自动批处理。在此之前,只有在 React 事件处理函数中的多次 setState 才会被批处理;在 Promise、setTimeout、原生事件等场景中不会批处理。React 18 在所有场景中自动批处理状态更新:
// React 18 中,以下每次点击只会触发一次重新渲染
function AutoBatchDemo() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
function handleClick() {
// 这两个 setState 被自动批处理,只触发一次重渲染
setCount(c => c + 1);
setText('updated');
}
return <button onClick={handleClick}>点击</button>;
}
// 即使在 Promise 或 setTimeout 中也会批处理
fetch('/api/data').then(() => {
setCount(c => c + 1); // 这些更新会被合并到一次渲染
setText('loaded'); // 不会触发两次渲染
});Transitions
startTransition 让开发者区分紧急更新和非紧急更新。紧急更新(如打字输入)立即响应,非紧急更新(如根据输入过滤列表)可被延迟:
import { startTransition, useState } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleChange(e) {
// 紧急更新——输入框立刻响应
setQuery(e.target.value);
// 非紧急更新——过滤结果可以被中断
startTransition(() => {
setResults(filterItems(e.target.value));
});
}
return (
<div>
<input value={query} onChange={handleChange} />
<ResultList items={results} />
</div>
);
}useId
useId 用于在服务端渲染(SSR)和客户端生成唯一且稳定的 ID,解决组件复用时的 ID 冲突问题:
import { useId } from 'react';
function FormField({ label }) {
const id = useId(); // 生成唯一 ID
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
);
}
// 页面中多个 FormField 实例的 ID 不会冲突
// 在 SSR 环境下客户端和服务器端生成的 ID 一致其他值得关注的新特性
useDeferredValue:与startTransition类似,但用于将 Props 值延迟。useSyncExternalStore:用于在 React 外部状态库与并发渲染之间建立兼容。- Suspense 改进:不再受限于
lazy,可在任何位置使用 Suspense。 - 新的 Root API:
createRoot取代ReactDOM.render。
// React 18 新的根节点创建方式
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
// 旧方式(React 17 及之前)
// ReactDOM.render(<App />, document.getElementById('root'));总结
本文从 JSX 语法出发,逐步深入 React 的核心机制:虚拟 DOM 的 Diff 算法与 Reconciliation 过程、组件化设计思想、Props 与 State 的数据流管理、生命周期方法及其 Hooks 映射关系,最后概览了 React 18 带来的自动批处理、Transitions 和 useId 等重要新特性。
React 的核心哲学可以概括为三句话:
- 声明式:描述"UI 应该是什么样子",而不是"如何操作 DOM"。
- 组件化:将 UI 拆分为独立、可复用的模块。
- 单向数据流:数据自上而下流动,事件自下而上冒泡。
掌握这些核心概念,是深入 React 生态体系(如路由、状态管理、服务端渲染等)的基础。
在线 Demo
以下 Demo 使用纯 JavaScript 模拟了 React 的组件化思想,包含计数器与 TodoList 组件,展示了组件间通信的模式: