React 八股(新手向)
一.JSX
1.认识JSX
JSX 是 JavaScript 的语法扩展,让你能在 JS 中写类似 HTML 的代码。
本质:React.createElement() 的语法糖。
我们对比一下jsx和原生js
// 用 JSX(清晰)
const page = (
<div>
<h1>标题</h1>
<ul>
{list.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
// 不用 JSX(噩梦,千万别这样写)
const page = React.createElement(
'div',
null,
React.createElement('h1', null, '标题'),
React.createElement(
'ul',
null,
list.map(item => React.createElement('li', { key: item.id }, item.name))
)
);
优势 |
说明 |
|---|---|
声明式 |
一眼看出 UI 结构,像 HTML 一样直观 |
开发效率 |
不用手写嵌套的
|
错误提示 |
编译时就能发现语法错误(比如标签没闭合) |
2.JSX的核心规则
2.1.只能返回一个根元素
// ❌ 错误
return (
<h1>标题</h1>
<p>内容</p>
)
// ✅ 正确:用 div 包裹
return (
<div>
<h1>标题</h1>
<p>内容</p>
</div>
)
// ✅ 正确:用 Fragment(不产生额外 DOM 节点)
return (
<>
<h1>标题</h1>
<p>内容</p>
</>
)
2.2所有标签必须闭合
// ❌ 错误
<img src="xxx">
<input type="text">
// ✅ 正确
<img src="xxx" />
<input type="text" />
<div></div> // 双标签也要闭合
2.3. 属性名用驼峰命名
2.4
HTML 写法 |
JSX 写法 |
|---|---|
|
|
|
|
|
|
2.4. 嵌入 JS 表达式用 {}
const name = '小明';
const isLogin = true;
return (
<div>
<h1>你好,{name}</h1> {/* 变量 */}
<p>{isLogin ? '已登录' : '未登录'}</p> {/* 三元表达式 */}
<p>{1 + 2 + 3}</p> {/* 计算 */}
</div>
);
{} 里能放什么样的值?
任何 JS 表达式:变量、函数调用、计算、三元、map 等(有返回值)。不能放 if/for/while 等语句。
3.为什么 JSX 里要用 className 而不是 class?
因为 JSX 本质是 JS,
class是 JS 的保留关键字(定义类的),所以 React 团队选了className。
二.组件
2.1组件是什么?
组件 = 一个函数或类,接收输入(props),返回 JSX(描述 UI)。
2.2组件两种形式
2.2.1函数组件
// 函数组件:就是一个返回 JSX 的函数
function Greeting(props) {
return <h1>你好,{props.name}</h1>;
}
// 箭头函数写法
const Greeting = (props) => {
return <h1>你好,{props.name}</h1>;
};
除此之外还有类组件
面试一句话总结:
函数组件更轻量、更函数式,配合 Hooks 能实现类组件的全部能力。React 官方推荐新项目使用函数组件 + Hooks。
2.3组件核心概念
2.3.1Props(父传子,只读)
// 父组件
function Parent() {
return <Child name="小明" age={18} />;
}
// 子组件接收 props
function Child(props) {
return <div>{props.name},{props.age}岁</div>;
}
// 解构写法(推荐)
function Child({ name, age }) {
return <div>{name},{age}岁</div>;
}
Props 核心规则:
-
只读:子组件不能修改 props
-
任何类型都能传:字符串、数字、对象、数组、函数、JSX
2.3.2. State(组件内部数据,可变)
// 函数组件用 useState
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
2.3.3.Children(组件嵌套)
function Card({ children, title }) {
return (
<div className="card">
<h3>{title}</h3>
<div>{children}</div>
</div>
);
}
// 使用
<Card title="温馨提示">
<p>这是一段内容</p>
<button>确定</button>
</Card>
2.4.组件的生命周期(理解核心,面试常考)
React 在每个阶段都提供了一些“钩子”,让你可以插入自己的代码。这些钩子就叫生命周期方法。
2.4.1为什么需要生命周期?
因为你想在特定时刻做特定的事:
-
出生时:发请求拿数据、启动定时器、连接 websocket
-
更新时:根据新的 props 重新计算某些值、打印日志
-
死亡时:清理定时器、取消请求、断开连接(防止内存泄漏)
如果没有生命周期,你很难控制这些时机。
2.4.2类组件的生命周期(最经典)但是是早期类组件时代概念,我们这里用函数组件,所以不进行赘述
2.4.3函数组件中生命周期被useEffect模拟
2.4.3.1. 组件第一次出现时(挂载)
useEffect(() => {
console.log('组件出现了,只执行一次');
// 在这里做初始化工作
}, []);
那个 [] 就是“空数组”,意思是不依赖任何数据,所以只执行一次。
2.4.3.2 组件因为数据变化重新显示时(更新)
useEffect(() => {
console.log('messages 变了,组件更新了');
// 滚动到底部
scrollToBottom();
}, [messages]); // 依赖 messages,只有 messages 变化才执行
2.4.3.3 组件从页面上消失时(卸载)
useEffect(() => {
const timer = setInterval(() => {}, 1000);
// 这个函数会在组件消失前执行
return () => {
clearInterval(timer);
console.log('组件消失了,清理工作');
};
}, []);
关键: return 一个函数,这个函数就是“清理函数”,在组件消失前自动调用。
总结
你想做什么 |
写法 |
|---|---|
组件出现时做一次 |
|
某个数据变化时做 |
|
组件消失前做清理 |
|
三.State
State 是组件内部的数据,当 state 变化时,组件会重新渲染(函数重新执行,UI 更新)。
3.1更新State的两种方式
3.1.1直接传值
setCount(count + 1);
3.1.2传入函数—— 当新值依赖于旧值时推荐
setCount(prevCount => prevCount + 1);
为什么要用函数式更新?
如果连续调用多次,直接传值可能会因为异步批量更新导致覆盖。函数式更新能确保拿到的是上一次最新的值
// ❌ 点击一次,count 只增加 1
setCount(count + 1);
setCount(count + 1);
// ✅ 点击一次,count 增加 2
setCount(prev => prev + 1);
setCount(prev => prev + 1);
3.2.State异步更新
在 React 事件处理函数中,setState 是异步的,不会立刻更新 state,而是会批量处理
function handleClick() {
setCount(count + 1);
console.log(count); // 还是旧值,不会立刻变成新值
}
React 为了性能,会把多个 setState 合并成一次更新,避免频繁渲染。
面试回答:
setState在事件处理中是异步的,如果你需要在更新后立即获取新值,可以用useEffect监听那个 state。
3.3对象类型State
const [user, setUser] = useState({ name: '小明', age: 18 });
// 更新时:必须替换整个对象,不能直接修改
setUser({ ...user, age: 19 }); // ✅ 正确:展开旧对象,覆盖 age
// 错误:setUser({ age: 19 }) // ❌ 会丢失 name
核心规则:
-
不能直接修改原对象(
user.age = 19无效,React 不会渲染) -
必须创建一个新对象传给 setter
3.4数组类型State
const [list, setList] = useState(['a', 'b']);
// 添加
setList([...list, 'c']);
// 删除(filter)
setList(list.filter(item => item !== 'b'));
// 修改某个元素(map)
setList(list.map(item => item === 'a' ? 'A' : item));
永远不要用 push、pop、splice 直接修改原数组,要用 [...]、filter、map 返回新数组。
3.5.State的不可变原则
一句话:永远不要直接修改 state 的值,必须用 setter 函数传入一个新的值
// ❌ 错误
state.count = 1;
setState(state); // 不会触发渲染
// ✅ 正确
setState({ ...state, count: 1 });
为什么?
React 通过浅比较判断 state 是否变化。如果直接修改原对象,引用不变,React 认为没变,就不渲染。
3.6.State提升(Lifting State Up)
当两个组件需要共享同一份 state 时,把 state 放到它们的共同父组件中,通过 props 传递下去。
function Parent() {
const [value, setValue] = useState('');
return (
<>
<ChildA value={value} setValue={setValue} />
<ChildB value={value} />
</>
);
}
四.Props
Props 是父组件传递给子组件的数据,全称 Properties。
-
单向数据流:父 → 子,只能从上往下传
-
只读:子组件不能修改 props(改了也没用,父组件传什么就是什么)
// 父组件传递
<Child name="小明" age={18} />
// 子组件接收
function Child({ name, age }) {
return <div>{name}, {age}岁</div>;
}
4.1.Props传递方式
4.1.1. 传递各种数据类型
<Child
name="字符串"
age={18} // 数字(要花括号)
isLogin={true} // 布尔
user={{ id: 1 }} // 对象(双花括号)
onEvent={() => {}} // 函数
list={[1, 2, 3]} // 数组
/>
4.1.2. 传递 JSX(children 特殊 prop)
function Card({ children }) {
return <div className="card">{children}</div>;
}
<Card>
<h3>标题</h3>
<p>内容</p>
</Card>
这里的 <h3> 和 <p> 会作为 children 传递给 Card 组件。
4.2.子组件接收 Props 的几种写法
// 写法1:直接接收 props 对象
function Child(props) {
return <div>{props.name}</div>;
}
// 写法2:解构(最推荐,清晰)
function Child({ name, age }) {
return <div>{name}, {age}</div>;
}
// 写法3:解构 + 默认值
function Child({ name = '匿名', age = 0 }) {
return <div>{name}, {age}</div>;
}
4.3.Props不可变
function Child({ count, setCount }) {
// ❌ 错误:不能这样改
count = count + 1;
// ✅ 正确:如果父组件传了 setCount 函数,子组件可以调用它
// 这叫“子传父”模式:父传函数给子,子调用函数并传参数
return <button onClick={() => setCount(count + 1)}>+1</button>;
}
面试回答:Props 是只读的,React 要求组件像纯函数一样,不能修改入参。如果要改变数据,应该通过父组件传递的更新函数来实现,即“状态提升”。
4.4子传父
因为数据只能父传子,子想传数据给父,需要通过父传给子的函数来实现。
function Parent() {
const [msg, setMsg] = useState('');
const handleChildMsg = (childData) => {
setMsg(childData);
};
return (
<div>
<Child onSend={handleChildMsg} />
<p>来自子组件:{msg}</p>
</div>
);
}
function Child({ onSend }) {
return (
<button onClick={() => onSend('Hello 父组件')}>
发送给父
</button>
);
}
流程:父组件定义 handleChildMsg → 传给 Child 的 onSend prop → 子组件点击时调用 onSend('数据') → 父组件的 setMsg 执行 → 数据更新,UI 变化。
4.5Props 和 State 的区别
| 维度 | Props | State |
|---|---|---|
| 数据来源 | 父组件传入 | 组件内部自己定义 |
| 可变性 | 只读,不能修改 | 可用 setter 修改 |
| 谁控制 | 父组件控制 | 组件自己控制 |
| 变化后 | 子组件重新渲染 | 组件重新渲染 |
| 使用场景 | 组件配置、父传子数据 | 组件内部动态数据 |
一句话:Props 是外面给的,State 是自己管的。
五.渲染流程
5.1什么是虚拟 DOM?
虚拟 DOM = 一个普通的 JS 对象,用来描述真实 DOM 的结构。
例如,这段 JSX:
<div className="box" id="main">
<span>Hello</span>
</div>
对应的虚拟 DOM(简化版)长这样
{
type: 'div',
props: {
className: 'box',
id: 'main',
children: {
type: 'span',
props: { children: 'Hello' }
}
}
}
特点:
-
轻量(没有真实 DOM 几百个属性)
-
可以快速创建、销毁、比较
-
跨平台(因为只是 JS 对象)
5.2完整渲染流程
当组件的 state 或 props 发生变化时,React 会执行以下流程:
| 步骤 | 名称 | 做什么 | 是否操作真实 DOM |
|---|---|---|---|
| 1 | 触发 | state/props 变化,标记组件需要更新 | 否 |
| 2 | 渲染(Render) | 执行组件函数,生成新的虚拟 DOM 树 | 否 |
| 3 | 比对(Diff) | 比较新旧两棵虚拟 DOM 树,找出差异(变更集) | 否 |
| 4 | 提交(Commit) | 将差异一次性更新到真实 DOM | 是 |
关键点:前三步都不碰真实 DOM,只在最后一步批量更新。这就是 React 性能优化的基础。
5.3Diff算法
Diff 算法的任务:快速找出新旧两棵虚拟 DOM 树的不同之处。
React 用了三个简单的假设来让 Diff 变得很快:
-
不同标签名 = 不同结构(比如
div变成p,直接删掉旧的,建新的) -
同一层级比较,不会跨层移动(只比较父节点下的同一层子节点)
-
列表元素靠
key区分(告诉 React 哪个节点是同一个)
具体规则(背这三句就够了):
-
根节点标签变了:整个组件销毁重建(包括它的所有子节点)。
-
根节点标签没变:只更新变化的属性(比如
className),然后继续比较子节点。 -
子节点是列表:React 按顺序比较,但有
key的话,可以准确匹配新旧节点,避免不必要的重建。
5.4渲染、虚拟 DOM、Diff 的关系
state / props 变化
↓
【触发】标记需要更新
↓
【渲染】执行组件 → 生成新虚拟 DOM 树
↓
【Diff】比较新树和旧树(同层比较 + key) → 得到变更集合
↓
【提交】将变更集合应用到真实 DOM
一句话总结:
虚拟 DOM 是用 JS 对象模拟真实 DOM;React 通过 Diff 算法比较新旧虚拟 DOM 树,找出最小变更集,最后批量更新真实 DOM,从而提升性能。
为什么虚拟 DOM + Diff 能提升性能?
-
把多次变化合并成一次真实 DOM 更新。
-
尽可能复用已有的 DOM 节点,只更新必要部分。
-
开发者不用手动优化 DOM 操作,React 帮你做。
六.Hooks
6.1useState(高频,但容易答漏,前面已经总结过)
Q1:setCount 是同步还是异步?
在 React 事件处理中是异步(批量更新);在
setTimeout、原生事件中同步(但不要依赖这种差异,统一当作异步写)。
Q2:函数式更新是什么?为什么需要?
setCount(prev => prev + 1)可以拿到上一次的最新值,避免因批量更新导致覆盖。
Q3:state 是对象/数组时如何正确更新?
必须创建新对象/新数组,不能直接修改原对象。
setUser({ ...user, age: 19 });setList([...list, newItem])。
Q4:多个 state 还是单个 state 对象?
经常一起更新用对象;独立更新用多个
useState。
6.2useEffect(前面已说)
6.3useRef
这是什么?
你可以把 useRef 想象成一个抽屉:
-
这个抽屉里可以放任何东西(数字、字符串、DOM 元素、对象……)
-
关键特点:你往抽屉里换东西,React 不会重新刷页面(不会重新渲染)
-
而
useState里的东西一变,页面就会重新渲染
什么时候用?
-
想操作真实的 DOM 元素(比如让输入框自动获得焦点、播放视频、测量高度)
-
想存一个值,但它的变化不需要让页面重新刷新(比如定时器的 id、上一次的滚动位置)
怎么用?(代码示例)
案例1:让输入框自动获得焦点
import { useRef, useEffect } from 'react';
function MyInput() {
const inputRef = useRef(null); // 创建一个抽屉,起名叫 inputRef
useEffect(() => {
// 组件第一次显示后,让输入框获得焦点
inputRef.current.focus();
}, []);
return <input ref={inputRef} />; // 把这个输入框和抽屉关联起来
}
解释:ref={inputRef} 把真实 DOM 元素放进抽屉,之后就可以用 inputRef.current 操作它。
案例2:存定时器 id,避免重新渲染
function Timer() {
const timerRef = useRef(null); // 抽屉专门放定时器id
const [count, setCount] = useState(0);
function start() {
timerRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
}
function stop() {
clearInterval(timerRef.current);
}
return (
<div>
<p>{count}</p>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</div>
);
}
解释:timerRef 里的 id 变了,页面不会重新渲染,这正好符合需求(我们只想让 count 变化时渲染)。
和 useState 的区别一句话
useState变了 → 页面刷新;useRef变了 → 页面不刷新。
6.4.useContext
这是什么?
解决传东西传得很累的问题。
假设你有一个爷爷组件,他想给曾孙组件一个玩具。如果用 props:
-
爷爷 → 爸爸 → 儿子 → 孙子 → 曾孙
每一层都要手动传递,中间的人即使不需要这个玩具也要帮忙传。很烦。
useContext 相当于一个公共的快递柜:
-
爷爷把玩具放进快递柜(Provider)
-
曾孙直接去快递柜取(useContext),不需要经过中间的人
什么时候用?
当很多层都要用同一个数据(比如主题颜色、当前登录的用户、语言设置),而且中间很多组件不需要知道这个数据时。
怎么用?(三步)
-
创建快递柜(Context)
-
在爷爷那里提供数据(用 Provider 包裹)
-
在曾孙那里取数据(调用 useContext)
完整示例:主题切换
import { createContext, useContext, useState } from 'react';
// 1. 创建快递柜(Context)
const ThemeContext = createContext();
function Grandpa() {
const [theme, setTheme] = useState('light');
// 2. 把数据和改数据的方法放进快递柜,包裹下面的所有组件
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Father />
</ThemeContext.Provider>
);
}
function Father() {
return <Son />;
}
function Son() {
return <GrandSon />;
}
function GrandSon() {
// 3. 曾孙直接从快递柜拿数据,不需要爷爷一层层传
const { theme, setTheme } = useContext(ThemeContext);
return (
<div style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
切换主题
</button>
</div>
);
}
解释:爷爷的 Provider 把 { theme, setTheme } 放进公共区域,曾孙直接用 useContext 取出来用。中间的父亲、儿子完全不需要知道这件事。
注意
-
如果快递柜里的值变了(比如
theme从 light 变 dark),所有取数据的组件都会重新渲染。 -
适用于中等规模、不频繁变化的数据。非常频繁变化的大应用建议用 Zustand/Redux。
6.5useReducer
这是什么?
useReducer 是 useState 的升级版,用来管理比较复杂的状态。
什么时候觉得 useState 麻烦?
-
你有好几个相关的状态,比如一个表单有姓名、年龄、邮箱,而且更新规则复杂(比如年龄不能小于0,邮箱要校验)
-
你的状态更新依赖上一次的状态,而且有多个不同的更新“动作”
useReducer 就是把所有更新规则集中到一个小管家(reducer 函数)里,你只需要告诉小管家“我想做什么动作”,小管家帮你算出新状态。
生活类比
想象你有一个银行账户(状态):
-
直接操作(
useState):你自己记着余额,每次取钱存钱自己算,容易出错。 -
useReducer:你雇了一个会计。你只跟会计说“我要存100”或“我要取50”,会计按照规则算出新余额,告诉你。你不需要知道怎么算的。
怎么用?(代码示例:计数器升级版)
普通 useState 写法(简单情况):
const [count, setCount] = useState(0);
setCount(count + 1);
如果规则多一点:比如允许 +1、-1、重置、加任意数字。用 useState 就要写多个 setter,逻辑分散。
用 useReducer:
import { useReducer } from 'react';
// 1. 写一个小管家(reducer 函数)
// 它接收当前状态和一个动作,返回新状态
function countReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
case 'add':
return { count: state.count + action.payload };
default:
return state;
}
}
function Counter() {
// 2. 使用 useReducer,传入管家和初始状态
const [state, dispatch] = useReducer(countReducer, { count: 0 });
return (
<div>
<p>{state.count}</p>
{/* 3. 派发动作,让管家干活 */}
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
<button onClick={() => dispatch({ type: 'add', payload: 5 })}>+5</button>
</div>
);
}
解释:
-
dispatch({ type: 'increment' })就是告诉会计“我要加1” -
会计(reducer)根据规则计算新状态,React 自动更新页面
什么时候真的需要用?
-
状态更新逻辑超过 3 种不同操作
-
下一个状态依赖前一个状态,而且计算比较复杂
-
比如购物车(添加、删除、改数量、清空)、表单(多个字段校验)
useReducer vs useState 选择
-
简单情况:用
useState(80% 的情况) -
复杂情况:用
useReducer(让代码更清晰、可预测)
总结
Hook |
一句话解释(通俗) |
|---|---|
|
让组件记住一个会变的值(比如按钮点了几下),值变了页面自动刷新。 |
|
等组件渲染完后去做一件事(比如发请求、改标题、开定时器),还能在组件消失前收拾干净。 |
|
放东西的抽屉,换东西页面不刷新;常用来拿 DOM 或存定时器 id。 ✅(你已写) |
|
公共快递柜,避免 props 层层传递;适合主题、用户等全局数据。 ✅(你已写) |
|
雇一个会计管理复杂状态;你只告诉它动作,它按规则算新状态。 ✅(你已写) |
|
把计算结果缓存起来,依赖没变就不重新算(省力气)。 |
|
把函数缓存起来,依赖没变就不重新创建函数(让子组件少渲染)。 |
自定义 Hook |
把自己重复用的逻辑(比如发请求、拿窗口宽度)包成一个以
|
更多推荐



所有评论(0)