React 介绍与核心概念
React 介绍及特性
React 是由 Facebook(Meta)开发并开源的 JavaScript 库,用于构建用户界面(UI)。它的核心特性包括:
- 声明式编程:开发者只需描述 UI 应该是什么样子,React 负责高效地更新和渲染。当数据变化时,React 自动更新对应的组件。
- 组件化:UI 被拆分成独立、可复用的组件,每个组件管理自己的状态和渲染逻辑,极大地提高了代码的可维护性和复用性。
- 一次学习,随处编写:React 可以渲染到多种平台(Web、移动端 React Native、VR、服务端等)。
与传统 MVC 的关系、MVVM 模式
MVC(Model-View-Controller):传统 MVC 中,Model 管理数据,View 负责展示,Controller 处理用户输入并更新 Model 和 View。
React 在传统 MVC 中的定位是 View 层——它专注于 UI 的渲染,不内置 Model 和 Controller。但在实际项目中,React 配合状态管理库(如 Redux)可以实现完整的 MVC 模式。
MVVM(Model-View-ViewModel):MVVM 中 ViewModel 作为 View 和 Model 之间的桥梁,View 通过数据绑定自动同步数据。React 并不是严格的 MVVM 框架,但配合 Hooks 或状态管理可以实现类似 MVVM 的数据流:组件(View)通过 Hooks(ViewModel)操作数据(Model),数据变化自动触发 UI 重新渲染。
React 的三大核心概念
- 组件(Component):React 应用由组件组成,组件是 UI 的基本构建块。可以是函数组件或类组件。
- 状态(State):组件内部管理的数据,状态变化会触发组件重新渲染。
- 属性(Props):父组件向子组件传递数据的只读媒介,是组件间单向数据流的基础。
虚拟 DOM 的好处
虚拟 DOM(Virtual DOM)是 React 的核心机制之一,它是真实 DOM 的 JavaScript 对象表示(轻量级副本)。
- 性能优化:直接操作真实 DOM 代价高昂。React 先在虚拟 DOM 上进行变更,再通过 Diff 算法计算最小变更集,最后批量更新真实 DOM。
- 跨平台能力:虚拟 DOM 是平台无关的 JavaScript 对象,可以渲染到不同平台(DOM、Native、Canvas 等)。
- 开发体验:开发者无需手动操作 DOM,只需描述 UI 状态,React 自动处理更新。
// 虚拟 DOM 的简单示例
const virtualDOM = {
type: 'div',
props: { className: 'container' },
children: [
{ type: 'h1', props: {}, children: ['Hello World'] }
]
};React Diff 算法
React 的 Diff 算法用于比较新旧虚拟 DOM 树,找出最小差异并高效更新真实 DOM。其核心策略基于三个假设:
- Tree Diff:只对同层节点进行比较,复杂度从 O(n³) 降到 O(n)。
- Component Diff:相同类型的组件生成相似的树结构,不同类型的组件直接替换整棵子树。
- Element Diff:同一层级的同组子节点,通过唯一 key 来识别,实现插入、删除、移动的最小化。
// key 帮助 React 识别列表中的每个元素
{list.map(item => (
<li key={item.id}>{item.name}</li>
))}客观题练习
📝 以下关于虚拟 DOM 的描述,哪项是错误的?
- A. 虚拟 DOM 是真实 DOM 的 JavaScript 对象表示
- B. 虚拟 DOM 的 Diff 算法复杂度是 O(n³)
- C. 虚拟 DOM 可以实现跨平台渲染
- D. React 通过批量更新减少真实 DOM 操作
✅ 答案:B(Diff 算法通过同层比较策略将复杂度从 O(n³) 降到了 O(n))
📝 React 中,用于唯一标识列表元素的属性是?
- A. id
- B. ref
- C. key
- D. index
✅ 答案:C(key 帮助 React 识别哪些元素发生了变化、添加或删除)
📝 关于 React 在 MVC 中的定位,以下说法正确的是?
- A. React 是一个完整的 MVC 框架
- B. React 主要关注 View 层
- C. React 是 MVVM 框架
- D. React 替代了后端 MVC
✅ 答案:B(React 专注于 UI 渲染,定位为 View 层)
React 脚手架及使用
create-react-app(CRA)
create-react-app 是 React 官方提供的脚手架工具,用于快速创建单页 React 应用,内置了 webpack、Babel、ESLint 等配置。
# 创建项目
npx create-react-app my-app
# 使用 TypeScript 模板
npx create-react-app my-app --template typescript
# 进入项目并启动
cd my-app
npm start # 启动开发服务器,http://localhost:3000项目结构:
my-app/
├── node_modules/ # 依赖包
├── public/
│ ├── index.html # 入口 HTML 模板
│ └── favicon.ico
├── src/
│ ├── App.js # 根组件
│ ├── App.css
│ ├── index.js # 入口 JS 文件
│ ├── index.css
│ └── reportWebVitals.js
├── package.json # 项目配置与依赖
└── README.md客观题练习
📝 使用 create-react-app 创建 TypeScript 项目,正确的命令是?
- A. npx create-react-app my-app –ts
- B. npx create-react-app my-app –template typescript
- C. npx create-react-app my-app –typescript
- D. npx create-react-app my-app -t ts
✅ 答案:B
📝 CRA 创建的项目中,入口 JS 文件是?
- A. src/App.js
- B. public/index.html
- C. src/index.js
- D. package.json
✅ 答案:C(src/index.js 是 webpack 打包入口,通过 ReactDOM.render 渲染 App 组件到 public/index.html 的根节点)
JSX / TSX 的理解与使用
JSX(JavaScript XML)
JSX 是 JavaScript 的语法扩展,允许在 JS 代码中编写类似 HTML 的标记。它是 React.createElement() 的语法糖。
核心规则:
- 必须有一个根元素(或使用
<>...</>Fragment) - 使用
{}嵌入 JavaScript 表达式 - 使用
className代替class,htmlFor代替for - 样式对象使用驼峰命名:
backgroundColor而非background-color - 自闭合标签必须以
/>结尾
import React from 'react';
function App() {
const name = 'React';
const style = { color: 'blue', fontSize: '20px' };
return (
<div className="app">
<h1 style={style}>Hello, {name}!</h1>
{2 + 2} {/* 表达式求值 */}
<input type="text" /> {/* 自闭合 */}
</div>
);
}TSX(TypeScript JSX)
TSX 是 TypeScript 中的 JSX,增加了类型检查,提高代码的健壮性和可维护性。
import React, { FC } from 'react';
interface AppProps {
title: string;
count?: number; // 可选属性
}
const App: FC<AppProps> = ({ title, count = 0 }) => {
return (
<div>
<h1>{title}</h1>
<p>Count: {count}</p>
</div>
);
};
export default App;客观题练习
📝 JSX 中设置 CSS 类名,正确的属性是?
- A. class
- B. className
- C. cssClass
- D. styleClass
✅ 答案:B(使用 className 替代 HTML 的 class 属性)
📝 JSX 中嵌入 JavaScript 表达式使用什么符号?
- A. {{ }}
- B. { }
- C. (( ))
- D. [[ ]]
✅ 答案:B(花括号 {} 中可以嵌入任意有效的 JavaScript 表达式)
📝 以下哪个是 JSX 中正确的注释写法?
- A. // 注释
- B. {/* 注释 */}
- C.
- D. # 注释
✅ 答案:B
函数组件
函数组件是使用 JavaScript 函数定义的 React 组件,是 React 16.8+ 引入 Hooks 后推荐的组件编写方式。
组件概念
函数组件接收 props 作为参数,返回 JSX 描述 UI。它是无副作用的纯函数——相同输入始终返回相同输出。
// 基础函数组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// 箭头函数写法
const Welcome = ({ name }) => <h1>Hello, {name}</h1>;事件绑定
React 中事件使用驼峰命名(如 onClick、onChange),通过 {} 绑定处理函数。event 是 React 的合成事件(SyntheticEvent)。
注意:应传递函数引用而非调用结果(onClick={handleClick} 而非 onClick={handleClick()})。
function Button() {
const handleClick = (event) => {
console.log('按钮被点击', event.target);
};
const handleChange = (e) => {
console.log('输入值:', e.target.value);
};
return (
<div>
<button onClick={handleClick}>点击我</button>
<input onChange={handleChange} placeholder="输入内容" />
</div>
);
}组件状态(useState)
使用 useState Hook 管理函数组件内部的状态。setState 支持直接传值和函数式更新两种方式。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(prev => prev - 1)}>-1</button>
</div>
);
}生命周期(useEffect)
函数组件没有传统的生命周期方法,而是用 useEffect Hook 处理副作用,模拟 componentDidMount、componentDidUpdate、componentWillUnmount。
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// componentDidMount + componentDidUpdate(userId 变化时)
fetchUser(userId).then(data => setUser(data));
// componentWillUnmount(清理函数)
return () => {
console.log('组件卸载或 userId 变化前清理');
};
}, [userId]); // 依赖数组:仅 userId 变化时重新执行
return <div>{user?.name}</div>;
}客观题练习
📝 关于函数组件的描述,错误的是?
- A. 函数组件可以使用 Hooks 管理状态
- B. 函数组件必须使用 return 返回 JSX
- C. 函数组件不能使用生命周期
- D. 函数组件通过 props 接收外部数据
✅ 答案:C(useEffect Hook 可以模拟所有类组件生命周期)
📝 useEffect 的第二个参数为空数组 [] 时,effect 何时执行?
- A. 每次渲染后都执行
- B. 仅在组件挂载时执行一次
- C. 仅在组件卸载时执行
- D. 从不执行
✅ 答案:B(空依赖数组表示不依赖任何值,只在首次渲染后执行一次)
📝 函数组件中,以下哪个是正确的事件绑定方式?
- A. onClick={handleClick()}
- B. onClick=”handleClick()”
- C. onClick={handleClick}
- D. onclick={handleClick}
✅ 答案:C(传递函数引用,事件名使用驼峰 onClick)
类组件
类组件是使用 ES6 class 定义的 React 组件,继承自 React.Component。在 Hooks 出现前是管理状态的主要方式。
组件概念与结构
import React, { Component } from 'react';
class Welcome extends Component {
// 类组件必须实现 render 方法
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}事件绑定与 this
类组件中事件处理需要关注 this 的绑定问题。两种方式:构造函数中 bind 或使用箭头函数。
class Toggle extends Component {
constructor(props) {
super(props);
this.state = { isOn: true };
// 方式一:在构造函数中绑定 this
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({ isOn: !this.state.isOn });
}
// 方式二:箭头函数(自动绑定 this)
handleClickArrow = () => {
this.setState(prev => ({ isOn: !prev.isOn }));
};
render() {
return (
<button onClick={this.handleClick}>
{this.state.isOn ? 'ON' : 'OFF'}
</button>
);
}
}State 与 setState
setState 是异步的,React 会批量处理状态更新以优化性能。依赖前一个状态时,应使用函数式更新。
class Counter extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
// 函数式更新:获取最新状态
this.setState(prev => ({ count: prev.count + 1 }));
};
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={this.increment}>+1</button>
</div>
);
}
}生命周期
类组件有三个主要生命周期阶段:
挂载阶段(Mounting):
constructor()—— 初始化 state 和绑定方法render()—— 渲染 UIcomponentDidMount()—— 组件挂载后,适合发起网络请求、订阅
更新阶段(Updating):
render()—— 重新渲染componentDidUpdate(prevProps, prevState)—— 更新后,可比较前后值
卸载阶段(Unmounting):
componentWillUnmount()—— 组件卸载前,清理定时器、取消订阅
class LifecycleDemo extends Component {
componentDidMount() {
console.log('组件已挂载');
this.timer = setInterval(() => console.log('tick'), 1000);
}
componentDidUpdate(prevProps) {
if (this.props.id !== prevProps.id) {
console.log('id 变化,重新请求数据');
}
}
componentWillUnmount() {
console.log('组件即将卸载');
clearInterval(this.timer);
}
render() {
return <div>Lifecycle Demo</div>;
}
}客观题练习
📝 类组件中,以下哪个生命周期方法在组件挂载后只执行一次?
- A. render
- B. componentDidUpdate
- C. componentDidMount
- D. componentWillUnmount
✅ 答案:C
📝 关于 setState 的描述,正确的是?
- A. setState 是同步执行的
- B. setState 会立即更新 this.state
- C. setState 可能是异步的,React 会批量处理
- D. setState 只能在类组件中使用
✅ 答案:C(setState 是异步的,React 批量处理以优化性能)
📝 类组件中解决 this 绑定的方式不包括?
- A. 构造函数中使用 bind
- B. 使用箭头函数定义方法
- C. 在 render 中使用 bind
- D. 使用 useEffect 绑定
✅ 答案:D(useEffect 是函数组件 Hook,类组件中不存在)
Props 与 State
Props 的使用
Props(Properties)是父组件向子组件传递数据的只读媒介,遵循单向数据流——数据从父到子。子组件不能修改 props。
// 父组件
function Parent() {
const user = { name: 'Alice', age: 25 };
return <Child name={user.name} age={user.age} />;
}
// 子组件
function Child({ name, age }) {
return <p>{name} is {age} years old</p>;
}
// 带默认值的 Props
Child.defaultProps = {
age: 18
};Props 类型校验(PropTypes)
import PropTypes from 'prop-types';
function User({ name, age, hobbies }) {
return /* ... */;
}
User.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
hobbies: PropTypes.arrayOf(PropTypes.string)
};State 的使用与 setState 异步性
State 是组件内部管理的可变数据。关键点:setState 是异步的,依赖当前值时必须用函数式更新。
// 类组件
class MyComponent extends Component {
state = { count: 0 };
increment = () => this.setState(prev => ({ count: prev.count + 1 }));
}
// 函数组件
function MyComponent() {
const [count, setCount] = useState(0);
const increment = () => setCount(prev => prev + 1);
}
// 错误用法(可能得到过期值)
this.setState({ count: this.state.count + 1 });
// 正确用法(函数形式获取最新 state)
this.setState(prev => ({ count: prev.count + 1 }));
setCount(prev => prev + 1);客观题练习
📝 关于 Props 的说法,哪项是正确的?
- A. 子组件可以直接修改父组件传入的 props
- B. Props 只能在类组件中使用
- C. Props 是只读的,用于父组件向子组件传递数据
- D. Props 和 State 完全一样
✅ 答案:C(Props 是只读的,遵循单向数据流)
📝 连续调用两次 setState,以下写法正确的是?
- A. this.setState({count: this.state.count + 1}); this.setState({count: this.state.count + 1});
- B. this.setState(prev => ({count: prev.count + 1})); this.setState(prev => ({count: prev.count + 1}));
- C. this.state.count += 2;
- D. setState 不能连续调用
✅ 答案:B(函数式更新能获取最新 state,两次都会基于最新值 +1)
📝 PropTypes 的作用是?
- A. 提高组件渲染性能
- B. 对组件接收的 props 进行类型检查
- C. 自动生成文档
- D. 替换 TypeScript
✅ 答案:B(PropTypes 在开发环境下对 props 进行运行时类型检查并输出警告)
React Refs 与事件处理
Refs
Refs 提供了一种直接访问 DOM 节点或 React 元素实例的方式。使用场景:管理焦点、文本选择、触发动画、集成第三方 DOM 库。尽量避免过度使用 Refs。
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus(); // 自动聚焦
}, []);
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={() => inputRef.current?.focus()}>
聚焦输入框
</button>
</div>
);
}类组件中使用 createRef:
class MyComponent extends Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
componentDidMount() {
this.inputRef.current.focus();
}
render() {
return <input ref={this.inputRef} />;
}
}React 事件处理
React 使用合成事件(SyntheticEvent),是对浏览器原生事件的跨浏览器包装。事件委托到根节点,通过事件冒泡处理。
function Form() {
const handleSubmit = (e) => {
e.preventDefault(); // 阻止默认提交
console.log('表单提交');
};
return (
<form onSubmit={handleSubmit}>
<input onChange={(e) => console.log(e.target.value)} />
<button type="submit">提交</button>
</form>
);
}客观题练习
📝 React 中使用 Refs 的常见场景不包括?
- A. 管理焦点
- B. 触发动画
- C. 替代 state 管理数据
- D. 集成第三方 DOM 库
✅ 答案:C(Refs 不应替代 state/props 来管理数据)
📝 React 的合成事件(SyntheticEvent)描述正确的是?
- A. 合成事件与原生事件完全无关
- B. 合成事件直接绑定在每个 DOM 元素上
- C. 合成事件是原生事件的跨浏览器包装,通过事件委托实现
- D. 合成事件不能调用 preventDefault
✅ 答案:C(合成事件提供与原生事件一致的 API,通过事件委托优化性能)
React 组件通信
父子组件通信
父 → 子(Props):父组件通过 Props 向子组件传递数据。子 → 父(回调函数):父组件将回调函数作为 Props 传给子组件,子组件调用回调传回数据。
// 父组件
function Parent() {
const [message, setMessage] = useState('');
const handleChildData = (data) => {
setMessage(data);
};
return (
<div>
<Child onSend={handleChildData} />
<p>来自子组件: {message}</p>
</div>
);
}
// 子组件
function Child({ onSend }) {
return (
<button onClick={() => onSend('Hello from Child')}>
发送消息给父组件
</button>
);
}跨组件通信(Context)
Context 提供了一种在组件树中无需逐层传递 Props就能共享数据的方式。适用于全局数据(主题、语言、用户认证等)。
import { createContext, useContext, useState } from 'react';
// 1. 创建 Context
const ThemeContext = createContext('light');
// 2. Provider 提供数据
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
// 3. 中间组件无需传递 props
function Toolbar() {
return <ThemedButton />;
}
// 4. useContext 消费数据
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
style={{ background: theme === 'dark' ? '#333' : '#fff' }}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
当前主题: {theme}
</button>
);
}客观题练习
📝 React 中子组件向父组件传递数据的正确方式是?
- A. 子组件直接修改父组件的 state
- B. 通过回调函数(父组件将函数作为 props 传给子组件)
- C. 使用全局变量
- D. 子组件不能向父组件传递数据
✅ 答案:B
📝 以下关于 Context 的描述,错误的是?
- A. Context 可以避免 props 逐层传递
- B. Context 适用于主题、语言等全局数据
- C. Context.Provider 可以嵌套使用
- D. Context 只能传递字符串类型的数据
✅ 答案:D(Context 可以传递任意类型的值,包括对象、函数等)
📝 在函数组件中消费 Context 值,应使用哪个 Hook?
- A. useState
- B. useEffect
- C. useContext
- D. useReducer
✅ 答案:C
React Router
SPA 的理解
SPA(Single Page Application,单页应用)是一种 Web 应用架构,整个应用只有一个 HTML 页面。页面切换通过 JavaScript 动态替换内容,无需完整刷新。
- 优点:用户体验流畅、页面切换快、前后端分离、减少服务器压力
- 缺点:首屏加载较慢、SEO 不友好(需 SSR 解决)、需要前端路由管理
路由的两种模式
- Hash 模式:URL 中带
#,通过 hashchange 事件监听。兼容性好,不需要服务器配置。 - History 模式:URL 干净,通过 HTML5 History API(pushState、popState)实现。需要服务器配置 fallback 到 index.html。
react-router-dom 的使用
import { BrowserRouter, Routes, Route, Link,
useParams, useNavigate } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/user/123">用户123</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<UserPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
// 获取路由参数
function UserPage() {
const { id } = useParams();
return <p>用户 ID: {id}</p>;
}
// 编程式导航
function Home() {
const navigate = useNavigate();
return (
<div>
<button onClick={() => navigate('/about')}>
跳转到关于页
</button>
</div>
);
}客观题练习
📝 SPA 的优点不包括以下哪项?
- A. 用户体验流畅
- B. 首屏加载快
- C. 前后端分离
- D. 页面切换快
✅ 答案:B(SPA 首屏加载通常较慢,需要加载整个应用框架)
📝 React Router 中获取动态路由参数 /user/:id 中的 id,应使用?
- A. useNavigate
- B. useLocation
- C. useParams
- D. useRoute
✅ 答案:C
📝 History 模式与 Hash 模式的主要区别是?
- A. Hash 模式性能更好
- B. History 模式 URL 中没有 # 号,需要服务器配置
- C. Hash 模式不支持动态路由
- D. History 模式只能用于 React
✅ 答案:B(History 模式 URL 更干净,但刷新时会向服务器请求该路径,需配置 fallback)
React Hooks
Hooks 的理解及出现原因
Hooks 是 React 16.8 引入的特性,允许在函数组件中使用状态和其他 React 特性。
- 组件间复用状态逻辑困难:HOC 和 render props 会导致”嵌套地狱”。Hooks 从组件中提取逻辑,便于复用。
- 复杂组件难以理解:类组件中生命周期方法包含不相关的逻辑,Hooks 将逻辑按功能拆分。
- this 的困扰:类组件需要绑定 this。函数组件无 this 问题。
Hooks 使用规则:
- 只能在函数组件的顶层调用,不能在循环、条件或嵌套函数中调用
- 只能在 React 函数组件或自定义 Hook 中调用
useState — 管理状态
const [state, setState] = useState(initialValue);
// 基本使用
const [name, setName] = useState('');
// 函数式更新(依赖前一个状态时推荐)
const [count, setCount] = useState(0);
setCount(prev => prev + 1);
// 惰性初始化
const [data, setData] = useState(() => expensiveComputation());useEffect — 处理副作用
useEffect(() => {
// 副作用逻辑
return () => {
// 清理函数(可选)
};
}, [依赖数组]);
// 数据请求(仅在挂载时执行)
useEffect(() => {
fetch('/api/user').then(res => res.json()).then(setUser);
}, []); // 空数组
// 订阅与清理
useEffect(() => {
const subscription = api.subscribe(id);
return () => subscription.unsubscribe();
}, [id]);useCallback — 缓存函数引用
避免子组件不必要的重新渲染,返回缓存的函数。配合 React.memo 使用。
const memoizedCallback = useCallback(
() => doSomething(a, b),
[a, b],
);
// 典型场景:配合 React.memo 避免子组件重渲染
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // 永远不变的函数引用
return <Child onClick={handleClick} />;
}
const Child = React.memo(({ onClick }) => {
console.log('Child 渲染');
return <button onClick={onClick}>Click</button>;
});useMemo — 缓存计算结果
仅在依赖变化时重新计算,返回缓存的值。适用于昂贵的计算。
const memoizedValue = useMemo(
() => expensiveComputation(deps),
[deps]
);
// 示例:过滤列表
const [users, setUsers] = useState([]);
const [query, setQuery] = useState('');
const filteredUsers = useMemo(
() => users.filter(u => u.name.includes(query)),
[users, query]
);useRef — 可变引用
常用于访问 DOM 和保存不触发重新渲染的可变值。
// 访问 DOM
function TextInput() {
const inputRef = useRef(null);
useEffect(() => inputRef.current?.focus(), []);
return <input ref={inputRef} />;
}
// 保存可变值(不触发重渲染)
function Timer() {
const intervalRef = useRef(null);
const [count, setCount] = useState(0);
const start = () => {
intervalRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
};
const stop = () => clearInterval(intervalRef.current);
return /* ... */;
}useContext — 消费 Context
const ThemeContext = createContext('light');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>按钮</button>;
}useReducer — 复杂状态管理
useState 的替代方案,适合管理复杂的、下一个状态依赖前一个状态的状态逻辑。
const [state, dispatch] = useReducer(reducer, initialState);
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload };
default:
throw new Error('Unknown action type');
}
}
// 使用
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
dispatch({ type: 'increment' });
dispatch({ type: 'reset', payload: 10 });客观题练习
📝 以下关于 Hooks 使用规则的说法,正确的是?
- A. 可以在循环中调用 useState
- B. Hooks 只能在函数组件顶层调用
- C. 可以在条件语句中使用 useEffect
- D. 普通 JavaScript 函数中可以调用 Hooks
✅ 答案:B
📝 useCallback 和 useMemo 的主要区别是?
- A. useCallback 缓存函数,useMemo 缓存计算结果
- B. useCallback 缓存计算结果,useMemo 缓存函数
- C. 二者完全相同
- D. useCallback 只能在类组件中使用
✅ 答案:A
📝 useRef 创建的 ref 对象,修改其 current 属性会触发组件重新渲染吗?
- A. 会
- B. 不会
- C. 取决于值是否改变
- D. 只在类组件中会
✅ 答案:B(修改 ref.current 不会触发组件重新渲染)
📝 useReducer 相比 useState 更适合什么场景?
- A. 简单的基础类型状态
- B. 多个子值、状态逻辑复杂的场景
- C. 所有场景都一样
- D. 只能在 Redux 中使用
✅ 答案:B
Redux 与 Recoil
Redux
Redux 是 JavaScript 状态管理容器,提供可预测的状态管理。适用于大型应用中跨组件共享的复杂状态。
三大原则:
- 单一数据源:整个应用的 State 存储在唯一的 Store 中。
- State 只读:唯一改变 State 的方式是发起 Action。
- 纯函数执行修改:Reducer 是纯函数,接收旧 State 和 Action,返回新 State。
核心概念:Store(存储状态)、Action(描述事件)、Reducer(更新规则)、Dispatch(触发更新)
import { createStore } from 'redux';
// 1. Action Types
const INCREMENT = 'INCREMENT';
// 2. Action Creator
const increment = () => ({ type: INCREMENT });
// 3. Reducer
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 };
default:
return state;
}
};
// 4. Store
const store = createStore(counterReducer);
store.subscribe(() => console.log(store.getState()));
store.dispatch(increment());React-Redux(Redux Toolkit):
import { Provider, useSelector, useDispatch } from 'react-redux';
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1; },
decrement: state => { state.value -= 1; },
incrementByAmount: (state, action) => {
state.value += action.payload;
}
}
});
const store = configureStore({
reducer: { counter: counterSlice.reducer }
});
function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(counterSlice.actions.increment())}>
+1
</button>
</div>
);
}
// 根组件
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}Recoil
Recoil 是 Facebook 推出的轻量级状态管理库。核心概念:Atom(状态单元)和 Selector(派生状态)。
import { RecoilRoot, atom, selector,
useRecoilState, useRecoilValue } from 'recoil';
// 1. 定义 Atom
const textState = atom({
key: 'textState', // 全局唯一 key
default: '',
});
// 2. 定义 Selector(派生状态)
const charCountState = selector({
key: 'charCountState',
get: ({ get }) => {
const text = get(textState);
return text.length;
},
});
// 3. 使用
function TextInput() {
const [text, setText] = useRecoilState(textState);
return (
<input value={text}
onChange={(e) => setText(e.target.value)} />
);
}
function CharCount() {
const count = useRecoilValue(charCountState);
return <p>字符数: {count}</p>;
}
function App() {
return (
<RecoilRoot>
<TextInput />
<CharCount />
</RecoilRoot>
);
}客观题练习
📝 Redux 三大原则不包括?
- A. 单一数据源
- B. State 只读
- C. 使用纯函数执行修改
- D. 状态分散管理
✅ 答案:D(Redux 强调单一数据源,而非分散管理)
📝 Redux Toolkit 中 createSlice 的作用是?
- A. 创建 React 组件
- B. 同时生成 action creators 和 reducer
- C. 创建路由配置
- D. 创建 CSS 样式
✅ 答案:B(createSlice 简化了 Redux 的使用,自动生成 action 和 reducer)
📝 Recoil 中,Atom 和 Selector 的关系是?
- A. Selector 是 Atom 的另一种写法
- B. Selector 是基于 Atom 或其他 Selector 的派生状态
- C. Atom 和 Selector 互不相关
- D. Selector 替代 Atom
✅ 答案:B(Selector 类似 Vue 的 computed,是派生状态)
总结
| 知识点 | 核心要点 |
|---|---|
| React 核心概念 | 组件、State、Props;虚拟 DOM;Diff 算法(同层比较、key) |
| 函数组件 | useState 状态、useEffect 生命周期、事件驼峰绑定、无 this |
| 类组件 | constructor/render/componentDidMount/componentWillUnmount;this 绑定问题 |
| 组件通信 | 父→子 Props;子→父 回调函数;跨组件 Context(Provider + useContext) |
| React Router | SPA 概念;Hash/History 模式;Routes/Route/Link/useParams/useNavigate |
| Hooks | useState/useEffect/useCallback/useMemo/useRef/useContext/useReducer |
| 状态管理 | Redux:Store/Action/Reducer/Dispatch;Recoil:Atom/Selector |
主观题实战:实现一个任务管理组件
题目要求
请使用 React + Hooks 实现一个任务管理(Todo List)组件,满足以下功能:
- 展示任务列表,每条任务包含:标题、完成状态、创建时间
- 可以添加新任务(输入框 + 添加按钮,回车也可添加)
- 可以切换任务的完成状态(勾选复选框或点击文字)
- 可以删除单个任务
- 底部显示统计:总计 / 已完成 / 未完成 数量
- 支持按完成状态筛选:全部 / 已完成 / 未完成
- 空列表时显示友好的空状态提示
参考答案
import React, { useState, useMemo, useCallback } from 'react';
// ========== 类型定义 ==========
// 在实际项目中这些可以用 TypeScript interface 定义
// interface Todo {
// id: number;
// title: string;
// completed: boolean;
// createdAt: Date;
// }
// ========== TodoList 组件 ==========
function TodoList() {
// ---------- 状态管理 ----------
const [todos, setTodos] = useState([]); // 任务列表
const [inputValue, setInputValue] = useState(''); // 输入框值
const [filter, setFilter] = useState('all'); // 筛选条件: all | completed | active
// ---------- 添加任务 ----------
const addTodo = useCallback(() => {
// 去除首尾空格,空内容不添加
const trimmed = inputValue.trim();
if (!trimmed) return;
// 创建新任务对象
const newTodo = {
id: Date.now(), // 使用时间戳作为唯一 ID
title: trimmed,
completed: false,
createdAt: new Date().toLocaleString(),
};
// 函数式更新:基于前一个状态追加
setTodos(prev => [newTodo, ...prev]); // 新任务添加到列表顶部
setInputValue(''); // 清空输入框
}, [inputValue]);
// ---------- 切换完成状态 ----------
const toggleTodo = useCallback((id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed } // 翻转完成状态
: todo
)
);
}, []);
// ---------- 删除任务 ----------
const deleteTodo = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
// ---------- 键盘事件(回车添加) ----------
const handleKeyDown = useCallback((e) => {
if (e.key === 'Enter') {
addTodo();
}
}, [addTodo]);
// ---------- 派生数据:筛选后的任务列表 ----------
const filteredTodos = useMemo(() => {
switch (filter) {
case 'completed':
return todos.filter(t => t.completed);
case 'active':
return todos.filter(t => !t.completed);
default:
return todos;
}
}, [todos, filter]);
// ---------- 派生数据:统计数据 ----------
const stats = useMemo(() => ({
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length,
}), [todos]);
// ========== 渲染 ==========
return (
<div className="todo-container">
<h2>📋 任务管理</h2>
{/* ---- 输入区域 ---- */}
<div className="todo-input-area">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="输入任务,按回车添加..."
/>
<button onClick={addTodo}>添加</button>
</div>
{/* ---- 筛选按钮 ---- */}
<div className="todo-filters">
{['all', 'active', 'completed'].map(type => (
<button
key={type}
className={filter === type ? 'active' : ''}
onClick={() => setFilter(type)}
>
{type === 'all' ? `全部(${stats.total})` :
type === 'active' ? `未完成(${stats.active})` :
`已完成(${stats.completed})`}
</button>
))}
</div>
{/* ---- 任务列表 ---- */}
{filteredTodos.length === 0 ? (
/* 空状态 */
<p className="empty-state">
{todos.length === 0
? '✨ 还没有任务,添加一个吧!'
: '没有匹配的任务'}
</p>
) : (
<ul className="todo-list">
{filteredTodos.map(todo => (
<li
key={todo.id}
className={`todo-item ${todo.completed ? 'completed' : ''}`}
>
{/* 复选框 */}
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{/* 任务内容(点击也可切换) */}
<span
className="todo-title"
onClick={() => toggleTodo(todo.id)}
>
{todo.title}
</span>
{/* 创建时间 */}
<span className="todo-time">{todo.createdAt}</span>
{/* 删除按钮 */}
<button
className="delete-btn"
onClick={() => deleteTodo(todo.id)}
>
删除
</button>
</li>
))}
</ul>
)}
{/* ---- 底部统计 ---- */}
<div className="todo-footer">
总计 {stats.total} 项 | 已完成 {stats.completed} | 未完成 {stats.active}
</div>
</div>
);
}
export default TodoList;关键注释说明
- useState:管理 todos 数组、inputValue 输入框值和 filter 筛选条件三个独立状态。每项关注一个关注点。
- useCallback:addTodo、toggleTodo、deleteTodo 三个核心操作都使用了 useCallback 缓存函数引用,避免子组件不必要的重新渲染。
- useMemo:filteredTodos(筛选结果)和 stats(统计数据)都是派生数据,使用 useMemo 缓存,仅在依赖变化时重新计算,避免每次渲染都重新过滤。
- 函数式 setState:addTodo 中使用
setTodos(prev => [...])确保基于最新状态追加,防止并发状态更新丢失数据。 - 不变性(Immutability):toggleTodo 使用 map 创建新数组而非直接修改原对象;deleteTodo 使用 filter 创建新数组。这确保 React 能正确检测状态变化。
- 空状态处理:列表为空时显示友好提示,区分了”还没有任务”和”当前筛选无匹配”两种情况。
- 键盘事件:绑定 onKeyDown 监听回车键,提升用户体验。
考察知识点总结
- useState — 组件状态管理
- useCallback — 事件处理函数缓存
- useMemo — 派生数据(筛选 + 统计)
- 函数式 setState — 确保状态更新的正确性
- 不可变数据操作 — map / filter / 展开运算符
- 条件渲染 — 空状态、筛选结果
- 列表渲染 — key 的正确使用
- 表单处理 — 受控组件 + 键盘事件