React 基础到进阶面试教程¶
适合:刚学完 React 基础、准备前端面试、需要系统复习 React 核心概念的人。
写法:每个问题都按「概念解释 → 关键点 → 简单示例 / 面试回答」整理。
一、React 基础¶
1. React 是什么?核心思想是什么?¶
React 是 Facebook 开源的一个用于构建用户界面的 JavaScript 库,主要用于构建单页应用中的视图层。
React 的核心思想是:
- 组件化
- 页面被拆分成一个个独立组件。
-
每个组件负责自己的结构、样式、状态和逻辑。
-
声明式 UI
- 开发者只需要描述“页面应该长什么样”。
-
React 会根据数据变化自动更新 DOM。
-
数据驱动视图
- 页面不是手动操作 DOM 更新的。
-
而是通过 state / props 的变化触发重新渲染。
-
虚拟 DOM
- React 会先在内存中生成虚拟 DOM。
- 再通过 diff 算法比较变化,最后只更新真正需要变化的 DOM。
面试回答:
React 是一个用于构建用户界面的 JavaScript 库,核心思想是组件化、声明式 UI 和数据驱动视图。开发者通过 state 和 props 描述界面状态,React 负责根据数据变化高效更新页面。
2. React 的设计理念是什么?¶
React 的设计理念主要包括:
- 声明式
- 不直接告诉浏览器每一步怎么操作 DOM。
-
只描述当前状态下 UI 应该是什么样。
-
组件化
- 把复杂页面拆成多个可复用的小组件。
-
降低代码复杂度,提高维护性。
-
一次学习,多端使用
- React 本身只关注 UI。
- React DOM 用于 Web。
-
React Native 用于移动端。
-
单向数据流
- 数据通常从父组件流向子组件。
-
数据变化更容易追踪。
-
函数式思想
- UI 可以理解为 state 的函数:
UI = f(state)
3. React 是 MVC 吗?¶
严格来说,React 不是完整的 MVC 框架。
MVC 包括:
- Model:数据层
- View:视图层
- Controller:控制层
React 主要负责 View 层,也就是用户界面的渲染。
但是 React 组件中也可能包含状态和事件逻辑,所以在实际开发中,React 不只是单纯的 View,但它本身不是完整的 MVC 框架。
面试回答:
React 不是完整的 MVC 框架,它更偏向于 View 层,用于构建 UI。数据管理、路由、请求等能力通常需要配合 Redux、React Router、Axios 等工具实现。
4. 什么是声明式 UI?¶
声明式 UI 指的是开发者只描述“结果是什么”,不需要关心“具体怎么一步步实现”。
传统命令式写法:
const div = document.createElement('div')
div.innerText = 'Hello'
document.body.appendChild(div)
React 声明式写法:
function App() {
return <div>Hello</div>
}
React 会根据 JSX 自动创建和更新 DOM。
面试回答:
声明式 UI 是指开发者只描述某个状态下页面应该展示什么,而不用手动操作 DOM。React 会根据状态变化自动完成 UI 更新。
5. 什么是组件?¶
组件是 React 应用的基本组成单位。
一个组件可以包含:
- UI 结构
- 状态 state
- 属性 props
- 事件处理
- 生命周期 / Hooks 逻辑
示例:
function UserCard(props) {
return (
<div>
<h3>{props.name}</h3>
<p>{props.age}</p>
</div>
)
}
组件的好处:
- 复用性高
- 结构清晰
- 方便维护
- 方便拆分复杂页面
6. 函数组件和类组件的区别?¶
函数组件¶
function Hello() {
return <div>Hello</div>
}
特点:
- 写法简单
- 通过 Hooks 管理状态和副作用
- 现在 React 官方更推荐
类组件¶
class Hello extends React.Component {
render() {
return <div>Hello</div>
}
}
特点:
- 使用 class 语法
- 通过 this.state 管理状态
- 通过生命周期方法处理副作用
区别总结:
| 对比项 | 函数组件 | 类组件 |
|---|---|---|
| 写法 | function | class |
| 状态 | useState | this.state |
| 生命周期 | useEffect 等 Hooks | componentDidMount 等 |
| this | 没有 this 问题 | 需要处理 this |
| 推荐程度 | 推荐 | 老项目常见 |
7. 为什么现在推荐函数组件?¶
主要原因:
- 写法更简单
- 不需要处理 this 指向
- Hooks 可以复用逻辑
- 更适合 React 未来的发展方向
- 代码更容易拆分和测试
类组件中的逻辑复用比较麻烦,常见方式有 HOC、Render Props,但代码容易嵌套复杂。
Hooks 出现后,可以通过自定义 Hook 抽离逻辑:
function useUser() {
const [user, setUser] = useState(null)
useEffect(() => {
// 请求用户信息
}, [])
return user
}
8. JSX 是什么?¶
JSX 是 JavaScript XML 的缩写,是一种 JavaScript 的语法扩展。
它允许我们在 JavaScript 中写类似 HTML 的结构。
const element = <h1>Hello React</h1>
JSX 不是浏览器原生支持的语法,需要通过 Babel 编译成 JavaScript。
9. JSX 为什么不是 HTML?¶
JSX 看起来像 HTML,但它本质上是 JavaScript。
区别:
- class 要写成 className
- for 要写成 htmlFor
- 事件使用驼峰命名,例如 onClick
- 可以使用 JavaScript 表达式
- 标签必须闭合
- 只能返回一个根节点
示例:
<label htmlFor="name" className="label">
用户名
</label>
10. JSX 最终会被编译成什么?¶
旧版 React 中,JSX 会被编译成 React.createElement。
const element = <h1>Hello</h1>
会被编译成:
const element = React.createElement('h1', null, 'Hello')
React 17 之后引入了新的 JSX 转换方式,不一定需要手动引入 React,但本质上仍然会被编译成创建 React Element 的代码。
二、JSX & 渲染¶
1. JSX 中如何写条件渲染?¶
常见方式有三种。
if 判断¶
function App({ isLogin }) {
if (isLogin) {
return <div>欢迎回来</div>
}
return <div>请登录</div>
}
三元表达式¶
{isLogin ? <div>欢迎回来</div> : <div>请登录</div>}
&& 短路渲染¶
{isLogin && <div>欢迎回来</div>}
注意:
{count && <div>有数据</div>}
如果 count 是 0,页面可能会渲染出 0。
更安全写法:
{count > 0 && <div>有数据</div>}
2. JSX 中如何循环渲染列表?¶
使用数组的 map 方法。
const users = [
{ id: 1, name: 'Tom' },
{ id: 2, name: 'Jack' }
]
function UserList() {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
React 中循环渲染时,每一项都需要加 key。
3. key 的作用是什么?¶
key 是 React 用来识别列表中每个元素身份的标识。
作用:
- 帮助 React 判断哪些元素新增了
- 帮助 React 判断哪些元素删除了
- 帮助 React 判断哪些元素移动了
- 提高 diff 性能
- 避免组件状态错乱
示例:
<li key={user.id}>{user.name}</li>
面试回答:
key 的作用是帮助 React 在 diff 过程中识别列表元素的唯一身份,从而更准确地复用 DOM 和组件实例,提升性能并避免状态错乱。
4. key 可以用 index 吗?¶
可以,但不推荐。
只有在以下情况可以使用 index:
- 列表是静态的
- 不会新增、删除、排序
- 每一项没有内部状态
不推荐原因:
如果列表发生删除或排序,index 会变化,React 可能错误复用组件,导致页面状态错乱。
错误示例:
{list.map((item, index) => (
<input key={index} defaultValue={item.name} />
))}
如果删除第一项,后面的 input 可能复用错误。
推荐使用稳定唯一 id:
{list.map(item => (
<input key={item.id} defaultValue={item.name} />
))}
5. 为什么 key 不能乱用?¶
因为 key 影响 React 对组件的复用。
如果 key 每次都随机:
<li key={Math.random()}>{item.name}</li>
React 会认为每次都是全新的元素,导致:
- 无法复用 DOM
- 组件频繁卸载和重新创建
- 性能变差
- 组件内部状态丢失
所以 key 必须稳定、唯一、可预测。
6. Fragment 是什么?¶
Fragment 是 React 提供的一个占位组件,用来包裹多个元素,但不会生成额外 DOM。
function App() {
return (
<>
<h1>标题</h1>
<p>内容</p>
</>
)
}
等价于:
<React.Fragment>
<h1>标题</h1>
<p>内容</p>
</React.Fragment>
适合避免多余的 div。
7. dangerouslySetInnerHTML 是什么?¶
dangerouslySetInnerHTML 用于在 React 中直接插入 HTML 字符串。
function App() {
return (
<div dangerouslySetInnerHTML={{ __html: '<strong>Hello</strong>' }} />
)
}
为什么叫 dangerously?
因为如果 HTML 字符串来自用户输入,可能造成 XSS 攻击。
例如用户输入:
<img src=x onerror=alert(1)>
如果直接渲染,就可能执行恶意脚本。
使用时必须保证内容安全,最好先做过滤。
8. JSX 中为什么必须包一层?¶
React 组件的 return 只能返回一个 React Element。
错误写法:
return (
<h1>标题</h1>
<p>内容</p>
)
正确写法:
return (
<>
<h1>标题</h1>
<p>内容</p>
</>
)
原因:
JSX 最终会被编译成函数调用,一个函数不能直接返回多个并列值。
三、状态与数据流¶
1. 什么是 state?¶
state 是组件内部的数据状态。
当 state 变化时,React 会重新渲染组件。
function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
)
}
state 适合保存会影响页面显示的数据,例如:
- 输入框内容
- 是否登录
- 弹窗是否显示
- 列表数据
- 当前选中的 tab
2. state 和 props 的区别?¶
| 对比项 | state | props |
|---|---|---|
| 来源 | 组件内部 | 父组件传入 |
| 是否可修改 | 可通过 setState / setX 修改 | 子组件不能直接修改 |
| 作用 | 管理组件自身状态 | 父子组件传递数据 |
| 是否触发渲染 | 会 | 会 |
示例:
function Parent() {
const [name, setName] = useState('Tom')
return <Child name={name} />
}
function Child(props) {
return <div>{props.name}</div>
}
3. state 为什么不能直接修改?¶
错误写法:
state.count = 1
函数组件中:
count = count + 1
原因:
- React 无法知道数据变化
- 不会触发重新渲染
- 可能破坏不可变数据原则
- 影响性能优化和 diff 判断
正确写法:
setCount(count + 1)
对象更新:
setUser({
...user,
name: 'Jack'
})
数组更新:
setList([...list, newItem])
4. setState 是同步还是异步?¶
React 中 setState / setX 更准确地说是“调度一次状态更新”,不是立即修改变量。
在 React 18 中,大多数情况下会自动批处理。
示例:
setCount(count + 1)
console.log(count)
这里打印的还是旧值。
因为当前这次函数执行时,count 的值已经固定。状态更新会在后续重新渲染时生效。
面试回答:
setState 不是简单的同步或异步问题,它是一次状态更新调度。React 会根据场景进行批处理,更新后触发重新渲染。在当前执行上下文中通常拿到的是旧值。
5. setState 批量更新原理?¶
React 会把同一轮事件中的多个状态更新合并处理,减少重复渲染。
setCount(count + 1)
setName('Tom')
setAge(20)
React 不会每调用一次 set 就立即渲染一次,而是合并后统一渲染。
好处:
- 减少渲染次数
- 提高性能
- 保证状态更新的一致性
React 18 后,自动批处理范围更广,包括 Promise、setTimeout、原生事件等。
6. 多次 setState 会怎样?¶
直接使用旧值¶
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
如果 count 初始为 0,最终结果通常是 1。
因为三次使用的都是同一个旧 count。
使用函数形式¶
setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)
最终结果是 3。
因为每次都会基于上一次更新后的值继续计算。
7. 为什么 setState 要用函数形式?¶
当新状态依赖旧状态时,应该使用函数形式。
setCount(prev => prev + 1)
原因:
- 避免闭包中的旧值问题
- 多次更新时结果更准确
- 在批量更新中更安全
适合场景:
- 计数器 +1
- 数组追加
- 根据旧对象修改字段
setList(prev => [...prev, newItem])
8. props 是只读的吗?¶
是的。
子组件不能直接修改 props。
错误写法:
function Child(props) {
props.name = 'Jack'
}
React 数据流是单向的,props 只能由父组件传入,子组件只能读取。
如果子组件想修改父组件数据,需要父组件传递修改函数。
9. 如何子组件修改父组件状态?¶
父组件把修改 state 的函数传给子组件,子组件调用这个函数。
function Parent() {
const [name, setName] = useState('Tom')
return <Child changeName={setName} />
}
function Child({ changeName }) {
return (
<button onClick={() => changeName('Jack')}>
修改父组件状态
</button>
)
}
本质:
子组件不能直接修改父组件 state,只能通过调用父组件传下来的函数,让父组件自己修改。
10. 什么是受控组件?¶
受控组件指表单元素的值由 React state 控制。
function App() {
const [value, setValue] = useState('')
return (
<input
value={value}
onChange={e => setValue(e.target.value)}
/>
)
}
特点:
- 表单值存储在 state 中
- React 控制输入框显示
- 方便校验、联动、提交
11. 什么是非受控组件?¶
非受控组件指表单值由 DOM 自己管理,React 不直接控制。
通常通过 ref 获取值。
function App() {
const inputRef = useRef(null)
const handleSubmit = () => {
console.log(inputRef.current.value)
}
return (
<>
<input ref={inputRef} />
<button onClick={handleSubmit}>提交</button>
</>
)
}
特点:
- 写法简单
- React 不实时管理输入值
- 适合简单表单或文件上传
12. 表单受控与非受控的区别?¶
| 对比项 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据来源 | React state | DOM |
| 获取方式 | state | ref |
| 是否实时控制 | 是 | 否 |
| 校验联动 | 方便 | 不方便 |
| 代码量 | 较多 | 较少 |
推荐:
- 复杂表单用受控组件
- 简单表单或文件上传可用非受控组件
四、Hooks¶
1. 什么是 Hooks?¶
Hooks 是 React 16.8 引入的特性,允许函数组件使用 state、生命周期、副作用等能力。
常见 Hooks:
- useState
- useEffect
- useMemo
- useCallback
- useRef
- useContext
- useReducer
示例:
const [count, setCount] = useState(0)
Hooks 出现之前,函数组件没有状态,只能做展示组件。
2. 为什么要有 Hooks?¶
Hooks 解决了类组件的一些问题:
- this 指向复杂
- 生命周期中逻辑容易分散
- 逻辑复用困难
- 类组件代码较重
- 组件嵌套容易复杂
Hooks 的优势:
- 函数组件也能有状态
- 逻辑可以通过自定义 Hook 复用
- 代码更简洁
- 更容易组合逻辑
3. useState 原理?¶
useState 用于在函数组件中声明状态。
const [count, setCount] = useState(0)
React 内部会为每个组件维护一个 Hooks 链表或队列。
每次渲染时,React 按照 Hooks 调用顺序找到对应状态。
这也是为什么 Hook 不能写在条件语句中。
简单理解:
hooks[0] = count
hooks[1] = name
hooks[2] = effect
如果调用顺序变了,React 就找错状态了。
4. useEffect 的执行时机?¶
useEffect 用于处理副作用。
副作用包括:
- 请求数据
- 设置定时器
- 订阅事件
- 手动操作 DOM
- 修改 document.title
执行时机:
useEffect(() => {
console.log('组件渲染后执行')
})
默认每次渲染后都会执行。
useEffect(() => {
console.log('只在挂载后执行')
}, [])
依赖数组为空,只在组件挂载后执行一次。
useEffect(() => {
console.log('count 变化后执行')
}, [count])
依赖变化时执行。
5. useEffect 的依赖数组作用?¶
依赖数组用于控制 effect 什么时候重新执行。
不传依赖数组¶
useEffect(() => {
console.log('每次渲染后执行')
})
空数组¶
useEffect(() => {
console.log('只执行一次')
}, [])
有依赖¶
useEffect(() => {
console.log('count 变化时执行')
}, [count])
如果 effect 中用到了某个外部变量,通常应该放进依赖数组中,避免闭包旧值问题。
6. useEffect 和 componentDidMount 对比?¶
类组件:
componentDidMount() {
// 组件挂载后执行
}
Hooks:
useEffect(() => {
// 组件挂载后执行
}, [])
相似点:
- 都可以在组件挂载后执行请求、订阅等逻辑。
区别:
- useEffect 更灵活,可以根据依赖变化执行。
- useEffect 还可以返回清理函数,模拟卸载逻辑。
useEffect(() => {
const timer = setInterval(() => {}, 1000)
return () => {
clearInterval(timer)
}
}, [])
7. useLayoutEffect vs useEffect?¶
二者都用于处理副作用,但执行时机不同。
useEffect¶
- 浏览器完成绘制之后执行
- 不会阻塞页面渲染
- 大多数场景推荐使用
useLayoutEffect¶
- DOM 更新之后、浏览器绘制之前执行
- 会阻塞页面绘制
- 适合读取布局、同步修改 DOM
示例:
useLayoutEffect(() => {
const width = ref.current.offsetWidth
}, [])
面试回答:
useEffect 在浏览器绘制后异步执行,useLayoutEffect 在 DOM 更新后、浏览器绘制前同步执行。一般优先用 useEffect,只有需要读取布局并同步修改 DOM 时才使用 useLayoutEffect。
8. useMemo 的作用?¶
useMemo 用于缓存计算结果。
const total = useMemo(() => {
return list.reduce((sum, item) => sum + item.price, 0)
}, [list])
只有 list 变化时才重新计算 total。
适合场景:
- 复杂计算
- 避免每次渲染都重新计算
- 配合 React.memo 避免引用变化
不适合滥用:
简单计算不需要 useMemo,否则反而增加复杂度。
9. useCallback 的作用?¶
useCallback 用于缓存函数引用。
const handleClick = useCallback(() => {
console.log(count)
}, [count])
它等价于:
const handleClick = useMemo(() => {
return () => console.log(count)
}, [count])
适合场景:
- 函数传给子组件
- 子组件使用 React.memo
- 避免子组件因为函数引用变化而重新渲染
10. useRef 的作用?¶
useRef 可以保存一个可变值,修改它不会触发重新渲染。
常见用途:
获取 DOM¶
const inputRef = useRef(null)
<input ref={inputRef} />
保存定时器 ID¶
const timerRef = useRef(null)
timerRef.current = setInterval(() => {}, 1000)
保存上一次的值¶
const prevCount = useRef(count)
useEffect(() => {
prevCount.current = count
}, [count])
特点:
.current可以被修改- 修改不会触发渲染
- 在组件整个生命周期中保持同一个引用
11. useImperativeHandle 是什么?¶
useImperativeHandle 用于配合 forwardRef,自定义父组件通过 ref 能访问到的子组件方法。
const Child = forwardRef((props, ref) => {
const inputRef = useRef(null)
useImperativeHandle(ref, () => ({
focus() {
inputRef.current.focus()
}
}))
return <input ref={inputRef} />
})
父组件:
function Parent() {
const childRef = useRef(null)
return (
<>
<Child ref={childRef} />
<button onClick={() => childRef.current.focus()}>
聚焦
</button>
</>
)
}
适合场景:
- 暴露 focus
- 暴露 reset
- 暴露表单校验方法
- 暴露弹窗打开关闭方法
注意:不要滥用,否则会破坏组件封装。
12. 自定义 Hook 如何写?¶
自定义 Hook 是以 use 开头的函数,用于复用组件逻辑。
示例:封装窗口宽度。
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth)
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
return width
}
使用:
function App() {
const width = useWindowWidth()
return <div>{width}</div>
}
自定义 Hook 的本质:
把组件中可复用的状态逻辑抽出来。
13. Hook 使用规则有哪些?¶
Hooks 有两个重要规则:
- 只能在函数组件或自定义 Hook 中使用
- 不能在条件语句、循环、嵌套函数中使用
错误写法:
if (show) {
useEffect(() => {}, [])
}
正确写法:
useEffect(() => {
if (show) {
// 逻辑写在 Hook 内部
}
}, [show])
14. 为什么不能在条件语句中使用 Hook?¶
因为 React 依赖 Hook 的调用顺序来保存状态。
错误示例:
if (flag) {
const [name, setName] = useState('Tom')
}
const [age, setAge] = useState(18)
如果 flag 一会儿 true,一会儿 false,Hook 的调用顺序就变了。
React 会把状态对应错,导致 bug。
面试回答:
React 内部根据 Hook 的调用顺序来关联每个状态。如果 Hook 写在条件语句中,某次渲染可能调用,某次不调用,顺序就会改变,React 无法正确找到对应状态。
五、生命周期(类组件 & Hooks)¶
1. 类组件生命周期有哪些?¶
类组件生命周期分为三个阶段:
挂载阶段¶
- constructor
- static getDerivedStateFromProps
- render
- componentDidMount
更新阶段¶
- static getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
卸载阶段¶
- componentWillUnmount
2. componentDidMount / componentDidUpdate / componentWillUnmount 用途?¶
componentDidMount¶
组件挂载后执行。
常用于:
- 请求数据
- 订阅事件
- 设置定时器
componentDidUpdate¶
组件更新后执行。
常用于:
- 根据 props / state 变化执行逻辑
- 操作更新后的 DOM
componentWillUnmount¶
组件卸载前执行。
常用于:
- 清除定时器
- 取消请求
- 移除事件监听
- 取消订阅
3. getDerivedStateFromProps 是什么?¶
getDerivedStateFromProps 是一个静态生命周期方法,用于根据 props 派生 state。
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.value !== prevState.value) {
return {
value: nextProps.value
}
}
return null
}
注意:
- 它是静态方法,不能使用 this。
- 使用场景较少。
- 滥用容易导致 props 和 state 数据重复,增加复杂度。
4. shouldComponentUpdate 作用?¶
shouldComponentUpdate 用于控制组件是否需要重新渲染。
shouldComponentUpdate(nextProps, nextState) {
return nextProps.count !== this.props.count
}
返回:
- true:允许更新
- false:阻止更新
作用:
- 减少不必要渲染
- 优化性能
函数组件中类似能力:
React.memo(Component)
5. React 16 之后废弃了哪些生命周期?¶
React 16 之后,一些旧生命周期被标记为不安全:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
原因:
React Fiber 支持异步渲染和可中断渲染,这些生命周期可能被多次调用或产生副作用问题。
替代方案:
- componentDidMount
- componentDidUpdate
- getDerivedStateFromProps
- getSnapshotBeforeUpdate
- useEffect
6. Hooks 如何模拟生命周期?¶
componentDidMount¶
useEffect(() => {
console.log('挂载')
}, [])
componentDidUpdate¶
useEffect(() => {
console.log('count 更新')
}, [count])
componentWillUnmount¶
useEffect(() => {
return () => {
console.log('卸载')
}
}, [])
挂载 + 更新¶
useEffect(() => {
console.log('每次渲染后执行')
})
注意:
Hooks 不是完全模拟生命周期,而是按“副作用”和“依赖”来组织逻辑。
六、组件通信¶
1. 父子组件通信方式?¶
父传子:通过 props。
function Parent() {
return <Child name="Tom" />
}
function Child({ name }) {
return <div>{name}</div>
}
子传父:父组件传函数给子组件。
function Parent() {
const [msg, setMsg] = useState('')
return <Child onChange={setMsg} />
}
function Child({ onChange }) {
return <button onClick={() => onChange('hello')}>发送</button>
}
2. 兄弟组件如何通信?¶
兄弟组件通常通过共同父组件通信。
function Parent() {
const [value, setValue] = useState('')
return (
<>
<A setValue={setValue} />
<B value={value} />
</>
)
}
A 修改父组件状态,B 接收父组件状态。
也可以使用:
- Context
- Redux
- Zustand
- EventEmitter
- URL 参数
- localStorage
3. 跨层级组件通信方式?¶
常见方式:
- props 层层传递
- Context
- Redux / Zustand
- 自定义事件
- ref 暴露方法
如果只是主题、语言、用户信息,可以用 Context。
如果是复杂业务状态,可以用 Redux / Zustand。
4. Context 的作用?¶
Context 用于跨层级传递数据,避免 props drilling。
创建:
const UserContext = React.createContext(null)
提供数据:
<UserContext.Provider value={user}>
<App />
</UserContext.Provider>
消费数据:
const user = useContext(UserContext)
适合:
- 用户信息
- 主题
- 语言
- 权限
- 全局配置
5. Context 会导致性能问题吗?¶
会。
当 Provider 的 value 变化时,所有使用这个 Context 的子组件都会重新渲染。
<UserContext.Provider value={{ user, setUser }}>
如果每次渲染都创建新对象,消费者组件可能频繁更新。
6. 如何避免 Context 频繁渲染?¶
常见方法:
- 拆分 Context
UserContext
ThemeContext
PermissionContext
- 使用 useMemo 缓存 value
const value = useMemo(() => ({ user, setUser }), [user])
- 把不常变和常变的数据分开
- 使用状态管理库
- 只在必要组件中使用 useContext
7. props drilling 如何解决?¶
props drilling 指数据需要通过多层组件传递,但中间组件并不使用这个数据。
解决方式:
- Context
- 组件组合
- 状态提升
- Redux / Zustand
- 自定义 Hook
示例:
<UserContext.Provider value={user}>
<DeepChild />
</UserContext.Provider>
8. forwardRef 是什么?¶
forwardRef 用于让父组件把 ref 传递给子组件内部的 DOM 或组件实例。
const MyInput = forwardRef((props, ref) => {
return <input ref={ref} />
})
父组件:
const inputRef = useRef(null)
<MyInput ref={inputRef} />
适合封装基础组件时使用,例如 Input、Modal、Form。
9. memo 是什么?¶
React.memo 是一个高阶组件,用于缓存函数组件的渲染结果。
const Child = React.memo(function Child({ name }) {
console.log('Child render')
return <div>{name}</div>
})
当 props 没有变化时,React 可以跳过子组件重新渲染。
注意:
- React.memo 只做 props 浅比较。
- 如果 props 是对象或函数,每次创建新引用,memo 可能失效。
10. memo 和 useMemo 的区别?¶
| 对比项 | React.memo | useMemo |
|---|---|---|
| 类型 | 高阶组件 | Hook |
| 作用 | 缓存组件渲染结果 | 缓存计算结果 |
| 使用对象 | 组件 | 值 |
| 场景 | 避免子组件重渲染 | 避免复杂计算重复执行 |
示例:
const Child = React.memo(ChildComponent)
const total = useMemo(() => calc(list), [list])
七、事件机制 & 合成事件¶
1. React 事件和原生事件区别?¶
React 事件不是直接绑定到真实 DOM 上的原生事件,而是 React 自己封装的一套事件系统。
区别:
| 对比项 | React 事件 | 原生事件 |
|---|---|---|
| 命名 | onClick | onclick |
| 兼容性 | React 统一处理 | 浏览器自己处理 |
| 事件对象 | SyntheticEvent | Event |
| 绑定方式 | JSX 属性 | addEventListener |
| 默认 this | 需要注意 | 根据调用方式 |
示例:
<button onClick={handleClick}>点击</button>
2. 什么是合成事件?¶
合成事件是 React 对浏览器原生事件的封装。
function handleClick(e) {
console.log(e)
}
这里的 e 是 SyntheticEvent,不是原生 Event。
React 封装合成事件的目的:
- 统一不同浏览器的行为
- 提供一致 API
- 优化事件绑定
- 更好配合 React 更新机制
如果需要原生事件对象:
e.nativeEvent
3. 事件冒泡和捕获?¶
事件传播有三个阶段:
- 捕获阶段:从外层到目标元素
- 目标阶段:到达触发元素
- 冒泡阶段:从目标元素向外层传播
React 默认使用冒泡事件:
<div onClick={parentClick}>
<button onClick={childClick}>点击</button>
</div>
点击 button 时,先触发 childClick,再触发 parentClick。
捕获事件:
<div onClickCapture={handleCapture}>
4. 为什么 React 要自己实现事件系统?¶
原因:
- 统一浏览器兼容性
- 减少事件监听数量
- 方便做事件委托
- 更好控制事件优先级
- 方便和 React 更新调度结合
React 不给每个 DOM 都单独绑定事件,而是通过事件委托统一处理。
5. 事件绑定在 document 上的意义?¶
React 17 之前,事件主要委托到 document 上。
React 17 之后,事件委托到 React 根容器上。
事件委托的意义:
- 减少事件监听器数量
- 提高性能
- 动态添加元素也能响应事件
- 统一管理事件传播
面试注意:
旧版本 React 事件绑定在 document 上,React 17 以后改为绑定在 root 容器上,更方便多个 React 版本共存。
6. 如何阻止事件冒泡?¶
使用:
e.stopPropagation()
示例:
function ChildClick(e) {
e.stopPropagation()
console.log('child')
}
阻止默认行为:
e.preventDefault()
例如阻止表单提交刷新页面:
<form onSubmit={e => e.preventDefault()}>
7. 合成事件和原生事件混用会怎样?¶
合成事件和原生事件混用时,要注意事件触发顺序和绑定位置。
可能出现:
- stopPropagation 不能完全阻止另一套事件系统
- 原生事件和 React 事件执行顺序不同
- document 上的原生事件可能仍然触发
建议:
- 尽量不要混用
- 必须混用时,明确事件绑定位置
- 使用 nativeEvent 时谨慎处理
八、性能优化¶
1. React 性能瓶颈通常在哪?¶
常见瓶颈:
- 组件频繁重新渲染
- 大列表渲染
- 复杂计算重复执行
- 图片资源过大
- 首屏资源体积过大
- Context 更新导致大面积渲染
- 不合理的 key
- 不必要的全局状态更新
性能优化本质:
减少不必要的渲染,减少不必要的计算,减少首屏资源体积。
2. 如何减少组件重渲染?¶
常见方法:
- React.memo
- useMemo
- useCallback
- 合理拆分组件
- state 下放
- 避免父组件频繁创建新对象和新函数
- Context 拆分
- 使用虚拟列表
示例:
const Child = React.memo(function Child({ name }) {
return <div>{name}</div>
})
3. React.memo 的使用场景?¶
适合:
- 子组件渲染成本较高
- 子组件 props 不经常变化
- 父组件频繁更新
- 子组件是纯展示组件
不适合:
- 组件很简单
- props 每次都变化
- 过度使用导致代码复杂
4. useCallback 的正确使用方式?¶
useCallback 不是为了让函数执行更快,而是为了保持函数引用稳定。
典型搭配:
const Child = React.memo(({ onClick }) => {
return <button onClick={onClick}>点击</button>
})
function Parent() {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => {
console.log('click')
}, [])
return <Child onClick={handleClick} />
}
如果子组件没有 memo,useCallback 可能意义不大。
5. useMemo 的使用场景?¶
适合:
- 复杂计算
- 过滤、排序大数组
- 生成稳定对象
- 配合 memo 避免子组件重渲染
示例:
const filteredList = useMemo(() => {
return list.filter(item => item.active)
}, [list])
6. key 对性能的影响?¶
合理 key 可以帮助 React 准确复用 DOM。
不合理 key 会导致:
- DOM 频繁销毁重建
- 组件状态丢失
- diff 判断错误
- 性能下降
推荐:
key={item.id}
避免:
key={Math.random()}
key={Date.now()}
7. 虚拟列表如何实现?¶
虚拟列表只渲染当前可视区域附近的数据,而不是一次性渲染所有列表项。
适合:
- 几千条数据
- 几万条数据
- 表格
- 长列表
核心思想:
- 计算容器高度
- 根据滚动距离计算可见数据范围
- 只渲染可见区域
- 用占位高度撑开滚动条
常用库:
- react-window
- react-virtualized
8. 懒加载组件怎么做?¶
使用动态 import。
const UserPage = React.lazy(() => import('./UserPage'))
然后配合 Suspense:
<Suspense fallback={<div>加载中...</div>}>
<UserPage />
</Suspense>
好处:
- 减少首屏包体积
- 按需加载页面
- 提升首屏速度
9. React.lazy 和 Suspense?¶
React.lazy 用于懒加载组件。
Suspense 用于在组件加载期间显示 fallback。
const About = React.lazy(() => import('./About'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<About />
</Suspense>
)
}
常用于路由懒加载。
10. 如何避免重复渲染?¶
方法:
- 状态放在最小必要范围
- 合理拆分组件
- 使用 React.memo
- 使用 useMemo / useCallback
- 避免不必要的 Context 更新
- 避免父组件每次传新对象
- key 保持稳定
- 使用状态管理库的 selector
11. 如何做首屏优化?¶
常见方案:
- 路由懒加载
- Code Splitting
- Tree Shaking
- 图片压缩
- CDN 加速
- 开启 gzip / brotli
- 减少首屏请求
- SSR / SSG
- 预加载关键资源
- 骨架屏
- 缓存静态资源
九、React Router¶
1. BrowserRouter vs HashRouter?¶
BrowserRouter¶
使用 HTML5 history API。
URL 示例:
https://example.com/user
优点:
- URL 更美观
- 更接近真实路径
缺点:
- 需要服务端配置 fallback
- 刷新页面可能 404
HashRouter¶
使用 URL hash。
URL 示例:
https://example.com/#/user
优点:
- 不需要服务端特殊配置
- 刷新不会 404
缺点:
- URL 不够美观
- SEO 不友好
2. Route / Routes 的区别?¶
React Router v6 中使用 Routes 包裹 Route。
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user" element={<User />} />
</Routes>
v5 中是 Switch:
<Switch>
<Route path="/" component={Home} />
</Switch>
v6 的变化:
- 使用 element
- 使用 Routes 替代 Switch
- 路由匹配规则更简洁
- 嵌套路由更清晰
3. useParams / useSearchParams?¶
useParams¶
用于获取动态路由参数。
<Route path="/user/:id" element={<User />} />
const { id } = useParams()
访问:
/user/100
id 就是 100。
useSearchParams¶
用于获取查询参数。
/user?id=100&name=tom
const [searchParams] = useSearchParams()
const id = searchParams.get('id')
4. useNavigate 如何使用?¶
useNavigate 用于编程式跳转。
const navigate = useNavigate()
navigate('/login')
带参数:
navigate('/user/100')
返回上一页:
navigate(-1)
替换当前历史记录:
navigate('/login', { replace: true })
5. 路由守卫如何实现?¶
React Router 没有 Vue Router 那种内置全局守卫,通常通过封装组件实现。
function AuthRoute({ children }) {
const token = localStorage.getItem('token')
if (!token) {
return <Navigate to="/login" replace />
}
return children
}
使用:
<Route
path="/dashboard"
element={
<AuthRoute>
<Dashboard />
</AuthRoute>
}
/>
6. 嵌套路由如何写?¶
父路由:
<Route path="/user" element={<UserLayout />}>
<Route path="profile" element={<Profile />} />
<Route path="setting" element={<Setting />} />
</Route>
父组件中使用 Outlet:
function UserLayout() {
return (
<div>
<h2>用户中心</h2>
<Outlet />
</div>
)
}
7. 动态路由原理?¶
动态路由是通过路径占位符匹配不同参数。
<Route path="/user/:id" element={<User />} />
当访问:
/user/1
/user/2
都会匹配同一个组件,但参数不同。
组件内部通过 useParams 获取参数。
8. 路由懒加载如何做?¶
const Home = React.lazy(() => import('./pages/Home'))
const User = React.lazy(() => import('./pages/User'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user" element={<User />} />
</Routes>
</Suspense>
)
}
十、状态管理(Redux / Zustand / Recoil)¶
1. Redux 核心思想?¶
Redux 是一个可预测的状态管理库。
核心思想:
- 全局状态集中管理
- 状态只读
- 通过 action 描述修改意图
- reducer 根据 action 返回新状态
- 单向数据流
2. Redux 三大原则?¶
- 单一数据源
-
整个应用状态存储在一个 store 中。
-
State 是只读的
- 不能直接修改 state。
-
必须通过 dispatch action 修改。
-
使用纯函数修改状态
- reducer 是纯函数。
- 输入旧 state 和 action,返回新 state。
3. Redux 的数据流?¶
流程:
- 组件触发事件
- dispatch 一个 action
- reducer 接收 action
- reducer 返回新 state
- store 更新
- 组件重新渲染
View -> dispatch(action) -> reducer -> store -> View
4. Redux 中间件是什么?¶
Redux 中间件用于增强 dispatch。
默认 Redux 只能 dispatch 普通对象。
中间件可以处理:
- 异步请求
- 日志
- 错误上报
- 权限校验
- 埋点
常见中间件:
- redux-thunk
- redux-saga
- redux-logger
5. Redux-thunk 和 redux-saga 区别?¶
redux-thunk¶
允许 dispatch 一个函数。
dispatch(async (dispatch) => {
const res = await api.getUser()
dispatch({ type: 'setUser', payload: res })
})
特点:
- 简单
- 适合中小项目
- 学习成本低
redux-saga¶
使用 Generator 管理异步流程。
特点:
- 更适合复杂异步
- 可以处理取消、竞态、监听
- 学习成本较高
对比:
| 对比项 | thunk | saga |
|---|---|---|
| 写法 | 函数 | Generator |
| 难度 | 简单 | 较高 |
| 适合 | 简单异步 | 复杂异步流程 |
| 可测试性 | 一般 | 较好 |
6. Redux Toolkit 优势?¶
Redux Toolkit 是官方推荐的 Redux 写法。
优势:
- 简化 Redux 配置
- 内置 Immer,可以写类似可变代码
- 内置 thunk
- 减少样板代码
- createSlice 自动生成 action 和 reducer
示例:
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment(state) {
state.value += 1
}
}
})
虽然看起来直接修改 state,但底层通过 Immer 生成新对象。
7. Zustand 与 Redux 对比?¶
Zustand 是一个轻量级状态管理库。
特点:
- API 简单
- 不需要 Provider
- 样板代码少
- 使用 Hook 访问状态
- 适合中小型项目
示例:
const useStore = create(set => ({
count: 0,
add: () => set(state => ({ count: state.count + 1 }))
}))
对比:
| 对比项 | Redux | Zustand |
|---|---|---|
| 复杂度 | 较高 | 较低 |
| 样板代码 | 多 | 少 |
| 生态 | 成熟 | 轻量 |
| 适合 | 大型复杂项目 | 中小项目 |
8. Recoil 是什么?¶
Recoil 是 Facebook 推出的 React 状态管理库。
核心概念:
- atom:最小状态单元
- selector:派生状态
示例:
const countState = atom({
key: 'countState',
default: 0
})
组件中使用:
const [count, setCount] = useRecoilState(countState)
特点:
- 状态粒度更细
- 更贴近 React
- 支持派生状态
- 适合复杂状态依赖
9. 状态提升 vs 全局状态?¶
状态提升¶
把多个组件共享的状态提升到最近的共同父组件。
适合:
- 共享范围小
- 只在局部页面使用
全局状态¶
把状态放到 Redux / Zustand / Context 等全局管理。
适合:
- 用户信息
- 登录状态
- 权限
- 主题
- 多页面共享数据
原则:
能局部就不要全局,避免全局状态过多导致维护困难。
十一、工程化¶
1. Webpack 与 Vite 区别?¶
Webpack¶
- 老牌打包工具
- 功能强大
- 配置灵活
- 开发启动较慢
Vite¶
- 基于原生 ESM
- 开发环境启动快
- 生产构建使用 Rollup
- 配置更简单
对比:
| 对比项 | Webpack | Vite |
|---|---|---|
| 开发启动 | 较慢 | 很快 |
| 配置复杂度 | 较高 | 较低 |
| 生态 | 非常成熟 | 越来越成熟 |
| 生产构建 | Webpack | Rollup |
2. Tree Shaking 原理?¶
Tree Shaking 是打包时删除未使用代码的优化技术。
依赖 ESM 的静态结构:
import { add } from './utils'
因为 import / export 是静态的,打包工具可以分析哪些代码没有被使用,然后删除。
注意:
- CommonJS 不利于 Tree Shaking
- 副作用代码可能影响 Tree Shaking
3. Code Splitting 是什么?¶
Code Splitting 是代码分割。
目的:
- 把一个大 bundle 拆成多个小 chunk
- 按需加载
- 减少首屏加载体积
常见方式:
import('./UserPage')
React 中:
const UserPage = React.lazy(() => import('./UserPage'))
4. chunk 是什么?¶
chunk 是打包后的代码块。
一个项目经过打包后,可能生成:
- main chunk
- vendor chunk
- async chunk
- runtime chunk
例如:
main.js
vendor.js
user-page.js
chunk 的作用是配合缓存和按需加载。
5. ESM 和 CJS 区别?¶
ESM¶
import React from 'react'
export default App
特点:
- 静态导入
- 支持 Tree Shaking
- 浏览器原生支持
- 编译时确定依赖
CJS¶
const React = require('react')
module.exports = App
特点:
- 运行时加载
- Node.js 传统模块规范
- 不利于 Tree Shaking
6. Babel 的作用?¶
Babel 是 JavaScript 编译器。
主要作用:
- 把新语法转换成旧语法
- 编译 JSX
- 支持 TypeScript 转换
- 配合 polyfill 处理兼容性
示例:
const element = <div>Hello</div>
会被 Babel 编译成 JavaScript 代码。
7. Polyfill 是什么?¶
Polyfill 是用于补充旧浏览器缺失 API 的代码。
例如旧浏览器没有 Promise,可以通过 polyfill 补上。
import 'core-js/stable'
Babel 负责转换语法,Polyfill 负责补 API。
区别:
| 工具 | 作用 |
|---|---|
| Babel | 转换语法 |
| Polyfill | 补充 API |
8. 浏览器兼容怎么处理?¶
常见方式:
- Babel 转换新语法
- core-js 提供 polyfill
- PostCSS / Autoprefixer 添加 CSS 前缀
- browserslist 配置目标浏览器
- 使用兼容性好的 API
- 做真机和多浏览器测试
9. 如何配置多环境?¶
常见环境:
- development
- test
- staging
- production
Vite 中:
.env.development
.env.production
变量命名:
VITE_API_BASE_URL=https://api.example.com
代码中使用:
import.meta.env.VITE_API_BASE_URL
Webpack 中通常通过 DefinePlugin 或 dotenv 实现。
10. 如何做前端监控?¶
前端监控包括:
- JS 错误监控
- Promise 错误监控
- 资源加载错误
- 接口错误
- 性能指标
- 用户行为日志
- 白屏监控
常见 API:
window.onerror = function(message, source, lineno, colno, error) {}
window.addEventListener('unhandledrejection', event => {})
性能监控:
- FP
- FCP
- LCP
- CLS
- INP
- TTFB
常用平台:
- Sentry
- Fundebug
- 阿里 ARMS
- 自建埋点系统
十二、React 18 新特性¶
1. 并发模式是什么?¶
React 18 引入并发渲染能力。
并发不是多线程,而是 React 可以把渲染任务拆分、暂停、恢复、放弃。
目标:
- 保持页面响应
- 优先处理用户交互
- 延后低优先级更新
- 避免长时间阻塞主线程
2. useTransition 用法?¶
useTransition 用于把某些更新标记为低优先级。
const [isPending, startTransition] = useTransition()
function handleChange(e) {
const value = e.target.value
setInput(value)
startTransition(() => {
setList(filterList(value))
})
}
input 更新是高优先级,列表过滤是低优先级。
适合:
- 搜索过滤
- 大列表更新
- 页面切换
3. useDeferredValue 作用?¶
useDeferredValue 用于延迟某个值的更新。
const deferredValue = useDeferredValue(input)
适合:
- 输入框实时输入
- 大列表根据输入过滤
- 避免输入卡顿
区别:
- useTransition 控制一段更新逻辑
- useDeferredValue 延迟某个值
4. startTransition 是什么?¶
startTransition 用于把状态更新标记为非紧急更新。
startTransition(() => {
setPage(nextPage)
})
紧急更新:
- 输入框内容
- 点击反馈
- 动画响应
非紧急更新:
- 搜索结果
- 大列表
- 页面内容切换
5. 自动批处理(Automatic Batching)?¶
React 18 中,更多场景下的 setState 会自动合并。
以前只有 React 事件中会批处理。
React 18 后:
setTimeout(() => {
setCount(c => c + 1)
setFlag(f => !f)
}, 1000)
也会自动批处理成一次渲染。
好处:
- 减少渲染次数
- 提升性能
- 行为更一致
6. React 18 对性能的提升?¶
主要来自:
- 自动批处理
- 并发渲染
- startTransition
- useDeferredValue
- Suspense 能力增强
- 更好的调度机制
注意:
React 18 不代表所有代码都会自动变快,还是要合理拆分组件和优化渲染。
7. Suspense 的新能力?¶
Suspense 最早主要用于组件懒加载。
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
React 18 中 Suspense 和并发渲染、SSR 结合更紧密,可以支持更细粒度的加载状态和流式渲染。
在普通前端项目中,最常见的用法仍然是路由懒加载。
十三、源码 & 原理¶
1. Fiber 架构是什么?¶
Fiber 是 React 16 引入的新协调架构。
它把渲染任务拆分成一个个小单元,每个组件对应一个 Fiber 节点。
Fiber 节点包含:
- 组件类型
- props
- state
- child
- sibling
- return
- effect 信息
简单理解:
Fiber 是 React 内部用来描述组件树和调度任务的数据结构。
2. Fiber 解决了什么问题?¶
老版本 React 递归更新组件树,一旦开始渲染就不能中断。
如果组件树很大,可能长时间阻塞主线程,导致页面卡顿。
Fiber 解决:
- 渲染任务可拆分
- 渲染过程可中断
- 可以恢复任务
- 可以丢弃过期任务
- 可以根据优先级调度
3. React 的调度机制?¶
React 会给不同更新分配不同优先级。
例如:
高优先级:
- 点击
- 输入
- 动画
低优先级:
- 列表过滤
- 页面内容更新
- 后台数据刷新
React 调度器会优先执行高优先级任务,低优先级任务可以被延后或中断。
4. requestIdleCallback 的作用?¶
requestIdleCallback 是浏览器提供的 API,用于在浏览器空闲时执行任务。
requestIdleCallback(() => {
// 空闲时执行
})
React Fiber 的思想和它类似:利用空闲时间处理低优先级任务。
但 React 并不完全依赖 requestIdleCallback,因为它兼容性和控制能力有限,React 有自己的 Scheduler。
5. diff 算法原理?¶
React diff 用于比较新旧虚拟 DOM 的差异。
为了提高性能,React 做了三个假设:
- 不同类型的元素会生成不同树
- 同层级比较,不跨层级移动
- 通过 key 判断列表元素身份
示例:
<div></div>
变成:
<span></span>
类型不同,直接销毁旧节点,创建新节点。
列表比较时使用 key 判断复用。
6. 协调(Reconciliation)过程?¶
协调是 React 根据新旧虚拟 DOM 计算变化的过程。
流程:
- state / props 变化
- 生成新的 React Element
- 与旧 Fiber 树比较
- 标记需要插入、更新、删除的节点
- 提交阶段更新真实 DOM
协调阶段可以被中断。
提交阶段不能被中断,因为 DOM 更新必须保持一致。
7. setState 到视图更新流程?¶
流程:
- 调用 setState / setX
- 创建 update 对象
- update 加入更新队列
- React 调度更新
- 执行 render,生成新的虚拟 DOM / Fiber
- diff 比较新旧结果
- commit 阶段更新真实 DOM
- 浏览器绘制页面
简化:
setState -> 调度 -> render -> diff -> commit -> DOM 更新
8. 为什么 React 可以中断渲染?¶
因为 Fiber 把渲染工作拆成了很多小任务。
React 可以在执行一部分后暂停,把主线程让给更紧急的任务,例如用户输入。
当空闲时再继续。
注意:
- render 阶段可以中断
- commit 阶段不能中断
因为 commit 阶段会真正修改 DOM,一旦开始必须完成。
十四、真实业务场景¶
1. 登录鉴权怎么做?¶
常见流程:
- 用户输入账号密码
- 调用登录接口
- 后端返回 token
- 前端保存 token
- 请求接口时带上 token
- 路由守卫判断是否登录
保存位置:
- localStorage
- sessionStorage
- Cookie
- 内存变量
示例:
localStorage.setItem('token', token)
请求时:
headers: {
Authorization: `Bearer ${token}`
}
2. Token 失效怎么处理?¶
常见处理:
- 响应拦截器判断 401
- 清除本地 token
- 跳转登录页
- 提示用户重新登录
Axios 示例:
axios.interceptors.response.use(
res => res,
error => {
if (error.response.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
如果有 refresh token:
- access token 过期
- 使用 refresh token 请求新 token
- 更新 token
- 重试原请求
3. 权限系统如何设计?¶
常见权限:
- 登录权限
- 路由权限
- 菜单权限
- 按钮权限
- 接口权限
前端流程:
- 登录后获取用户信息
- 获取角色和权限列表
- 根据权限生成菜单
- 根据权限生成路由
- 页面中控制按钮显示
注意:
前端权限只能控制显示,真正的安全必须由后端接口权限控制。
4. 多 Tab 状态同步?¶
常见方案:
localStorage storage 事件¶
window.addEventListener('storage', event => {
if (event.key === 'token') {
// 其他 tab 的 token 变化
}
})
适合:
- 退出登录同步
- 用户信息变化同步
- 主题变化同步
BroadcastChannel¶
const channel = new BroadcastChannel('app')
channel.postMessage({ type: 'logout' })
channel.onmessage = event => {
console.log(event.data)
}
BroadcastChannel 更适合现代浏览器中的多 Tab 通信。
5. 表单性能优化?¶
常见方法:
- 拆分表单项组件
- 避免整个表单每次重渲染
- 使用非受控组件
- 使用 react-hook-form
- 防抖校验
- 按需校验
- 大表单分步骤
- 错误信息局部更新
复杂表单中,受控组件太多可能导致输入卡顿。
react-hook-form 通过非受控思想减少重渲染。
6. 大列表渲染优化?¶
方案:
- 虚拟列表
- 分页
- 无限滚动
- 懒加载
- React.memo
- 避免复杂 item 渲染
- 图片懒加载
- 后端分页
推荐库:
- react-window
- react-virtualized
7. 前端错误监控方案?¶
监控内容:
- JS 运行错误
- Promise 未捕获错误
- 资源加载错误
- 接口请求错误
- React 组件错误
- 白屏
- 性能指标
React 组件错误可以用 ErrorBoundary:
class ErrorBoundary extends React.Component {
componentDidCatch(error, info) {
// 上报错误
}
render() {
return this.props.children
}
}
8. 图片懒加载方案?¶
原生 loading¶
<img src="a.jpg" loading="lazy" />
IntersectionObserver¶
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
observer.unobserve(img)
}
})
})
作用:
- 图片进入视口再加载
- 减少首屏请求
- 提升页面速度
9. SEO 如何做?¶
普通 React SPA 对 SEO 不友好,因为页面内容通常由 JavaScript 动态生成。
解决方案:
- SSR:服务端渲染
- SSG:静态生成
- 预渲染
- 设置 title / meta
- 使用语义化 HTML
- sitemap
- robots.txt
- 合理的 URL 结构
常用框架:
- Next.js
- Remix
10. SSR / CSR / SSG 区别?¶
CSR:客户端渲染¶
浏览器下载 JS 后,由 JS 渲染页面。
优点:
- 前后端分离
- 交互体验好
缺点:
- 首屏可能慢
- SEO 较弱
SSR:服务端渲染¶
服务端生成 HTML 返回给浏览器。
优点:
- 首屏快
- SEO 好
缺点:
- 服务端压力大
- 架构复杂
SSG:静态生成¶
构建时生成 HTML 文件。
优点:
- 访问速度快
- SEO 好
- 部署简单
缺点:
- 不适合频繁变化的动态内容
对比:
| 类型 | 渲染位置 | SEO | 首屏 | 适合场景 |
|---|---|---|---|---|
| CSR | 浏览器 | 一般 | 一般 | 后台系统 |
| SSR | 服务端 | 好 | 快 | 内容站、电商 |
| SSG | 构建时 | 好 | 很快 | 博客、文档 |
十五、面试速记总结¶
React 核心¶
React 的核心是:
组件化 + 声明式 UI + 数据驱动视图 + 虚拟 DOM
Hooks 核心¶
Hooks 解决的是:
函数组件状态管理 + 副作用管理 + 逻辑复用
性能优化核心¶
性能优化主要围绕:
减少重渲染 + 减少重复计算 + 减少首屏资源 + 大列表虚拟化
React 原理核心¶
React 更新流程:
state/props 变化
-> render
-> 生成新虚拟 DOM / Fiber
-> diff
-> commit
-> 更新真实 DOM
业务开发核心¶
真实项目中常考:
登录鉴权
权限控制
路由守卫
Token 失效
表单优化
大列表优化
错误监控
SEO
SSR/CSR/SSG
十六、推荐背诵版回答模板¶
React 是什么?¶
React 是一个用于构建用户界面的 JavaScript 库,主要负责视图层。它的核心思想是组件化、声明式 UI 和数据驱动视图。开发者通过 state 和 props 描述页面状态,React 根据数据变化自动更新 UI。
Hooks 是什么?¶
Hooks 是 React 16.8 引入的特性,让函数组件也可以使用 state、生命周期和副作用等能力。常见 Hooks 有 useState、useEffect、useMemo、useCallback、useRef 等。Hooks 的优势是代码更简洁,逻辑更容易复用。
key 的作用?¶
key 是 React 在列表 diff 时用来识别元素身份的标识。稳定唯一的 key 可以帮助 React 准确复用 DOM 和组件实例,提高性能,并避免状态错乱。
useEffect 的作用?¶
useEffect 用于处理组件中的副作用,例如请求数据、订阅事件、设置定时器、修改标题等。它可以通过依赖数组控制执行时机,也可以返回清理函数处理卸载逻辑。
React.memo 和 useMemo 区别?¶
React.memo 用于缓存组件,避免 props 没变时子组件重复渲染。useMemo 用于缓存计算结果,避免复杂计算每次渲染都重新执行。
Fiber 是什么?¶
Fiber 是 React 16 引入的新架构,是 React 内部描述组件树和调度任务的数据结构。它把渲染任务拆成小单元,使 React 可以中断、恢复和优先处理高优先级任务,从而提升页面响应性能。
十七、学习顺序建议¶
建议按下面顺序学习:
- JSX 和组件
- state 和 props
- 事件处理和表单
- Hooks
- 组件通信
- React Router
- 状态管理
- 性能优化
- 工程化
- React 18
- Fiber 和 diff 原理
- 真实业务场景
十八、常见面试答题技巧¶
回答 React 面试题时,可以按照这个结构:
1. 先说定义
2. 再说作用
3. 然后说使用场景
4. 最后说注意点或原理
例如回答 useMemo:
useMemo 是 React 提供的一个 Hook,用于缓存计算结果。
当依赖项没有变化时,它会复用上一次的计算结果,避免重复执行复杂计算。
常用于大数组过滤、排序、统计,也可以用来保持对象引用稳定。
但不应该滥用,简单计算没必要使用 useMemo,否则会增加代码复杂度。
十九、练习建议¶
学完本教程后,可以重点练习这些代码题:
- useState 多次更新输出结果
- useEffect 依赖数组执行顺序
- React.memo + useCallback 是否会重新渲染
- key 使用 index 导致的问题
- 受控组件实现登录表单
- 路由守卫实现登录鉴权
- Redux 数据流手写
- 自定义 Hook 封装请求逻辑
- 虚拟列表原理实现
- Token 失效自动跳转登录
二十、结束语¶
React 面试不是只背 API,更重要的是理解:
数据如何变化
组件如何渲染
状态如何传递
性能如何优化
真实项目如何落地
掌握这些之后,大部分 React 基础和中级面试题都可以比较稳定地回答。