【前端高频面试题】- React篇 - 指南
2025-09-22 10:23 tlnshuju 阅读(0) 评论(0) 收藏 举报【前端高频面试题】- React篇
1. 请说说 React 的核心概念有哪些?
React 核心概念围绕“组件化”和“高效渲染”设计,核心包括以下几点:
- 组件(Component):React 应用的基本构建块,分为函数组件(推荐)和类组件,负责封装 UI 结构和逻辑,实现复用。
- JSX:JavaScript 语法扩展,允许在 JS 中直接写类似 HTML 的代码,直观描述 UI 结构,最终会被编译为
React.createElement()
调用。 - 虚拟 DOM(Virtual DOM):用 JS 对象模拟真实 DOM 结构,避免频繁操作真实 DOM(性能开销大),通过 Diff 算法对比新旧虚拟 DOM,只更新差异部分。
- State & Props:组件的数据来源。State 是组件内部可修改的状态;Props 是父组件传递给子组件的只读数据,实现组件通信。
- 生命周期(类组件)/ Hooks(函数组件):管理组件从创建、更新到销毁的过程。Hooks(如
useState
、useEffect
)是函数组件中替代生命周期的更简洁方案。 - 单向数据流:数据只能从父组件通过 Props 向下传递,子组件若需修改数据,需通过父组件传递的回调函数触发,避免数据混乱。
2. 什么是 JSX?为什么 React 要使用 JSX?
JSX 是 React 定义 UI 的核心语法,面试需明确其本质和优势:
- 定义:JSX(JavaScript XML)是 JavaScript 的语法扩展,允许在 JavaScript 代码中嵌入类似 HTML 的标签,用于描述 React 组件的 UI 结构。
- 注意:JSX 不是 HTML,也不是模板语言,最终会被 Babel 编译为
React.createElement(type, props, children)
函数调用,生成虚拟 DOM 对象。
- 注意:JSX 不是 HTML,也不是模板语言,最终会被 Babel 编译为
- 使用 JSX 的原因:
- 直观易读:类似 HTML 的语法,开发者无需切换“JS 逻辑”和“HTML 结构”思维,降低学习和开发成本。
- 融合 JS 逻辑:JSX 中可直接通过
{}
嵌入 JS 表达式(如变量、函数调用、条件判断),实现“UI 与逻辑的紧密结合”(如{user.name}
渲染用户名称)。 - React 优化支持:JSX 编译后生成的虚拟 DOM 结构,是 React Diff 算法和高效渲染的基础,避免手动编写
React.createElement
冗余代码。
3. 虚拟 DOM 是什么?为什么 React 需要虚拟 DOM?
虚拟 DOM 是 React 性能优化的核心,需解释“是什么”和“为什么”:
- 定义:虚拟 DOM(Virtual DOM)是用 JavaScript 对象描述真实 DOM 节点的轻量级结构,包含节点的标签名、属性、子节点等信息(如
{ type: 'div', props: { className: 'box' }, children: [] }
)。 - 为什么需要虚拟 DOM:
- 减少真实 DOM 操作:真实 DOM 是浏览器渲染核心,操作成本极高(修改 DOM 会触发重排/重绘)。虚拟 DOM 作为“中间层”,先在 JS 中对比新旧虚拟 DOM 的差异,只将差异部分更新到真实 DOM,大幅减少操作次数。
- 跨平台支持:虚拟 DOM 脱离浏览器环境,React 可基于虚拟 DOM 适配不同平台(如 React Native 用虚拟 DOM 映射原生组件,而非浏览器 DOM)。
- 批量更新优化:React 可将多次状态更新合并为一次虚拟 DOM 对比,避免频繁触发真实 DOM 更新,进一步提升性能。
4. State 和 Props 的区别是什么?
State 和 Props 是组件数据的核心,面试需明确“来源、可变性、用途”三方面差异:
对比维度 | State | Props |
---|---|---|
数据来源 | 组件内部定义,由组件自身管理 | 父组件传递,组件外部输入 |
可变性 | 可变(需通过 setState /useState 触发更新,不可直接修改) | 只读(子组件不能修改 Props,若需修改需通知父组件更新) |
用途 | 管理组件内部动态状态(如输入框值、弹窗显隐) | 实现组件间通信(父传子)、传递静态/动态数据 |
更新触发 | 组件内部调用更新方法(如 setState )触发自身重渲染 | 父组件更新 Props 时,子组件被动重渲染 |
5. React 类组件的生命周期有哪些?函数组件用什么替代生命周期?
类组件生命周期是基础,需分阶段梳理;函数组件需关联 Hooks 替代方案:
(1)类组件生命周期(3 个阶段)
- 挂载阶段(组件创建到 DOM 渲染):
constructor
:初始化 State、绑定函数(如this.handleClick = this.handleClick.bind(this)
)。render
:返回 JSX 结构,生成虚拟 DOM(不能调用setState
,否则会触发无限重渲染)。componentDidMount
:组件挂载到真实 DOM 后执行,常用于初始化操作(如发起异步请求、绑定事件监听)。
- 更新阶段(组件状态/Props 变化):
shouldComponentUpdate(nextProps, nextState)
:返回布尔值,决定组件是否需要重渲染(默认true
,可手动优化性能,如对比新旧 Props/State)。render
:重新生成虚拟 DOM,对比差异。componentDidUpdate(prevProps, prevState)
:组件更新后执行,常用于根据更新后的 DOM 做操作(如根据新 Props 请求数据,需判断prevProps !== this.props
避免重复请求)。
- 卸载阶段(组件从 DOM 移除):
componentWillUnmount
:组件卸载前执行,用于清理资源(如取消异步请求、移除事件监听、清除定时器),避免内存泄漏。
(2)函数组件替代方案
函数组件无生命周期,通过 Hooks 实现相同逻辑:
useEffect
:覆盖挂载、更新、卸载阶段,如:- 模拟
componentDidMount
:useEffect(() => { /* 初始化操作 */ }, [])
(依赖空数组,只执行一次)。 - 模拟
componentDidUpdate
:useEffect(() => { /* 更新后操作 */ }, [deps])
(依赖指定变量,变量变化时执行)。 - 模拟
componentWillUnmount
:useEffect(() => { return () => { /* 清理操作 */ } }, [])
(返回清理函数,组件卸载时执行)。
- 模拟
6. useState
的使用方法及注意事项是什么?
useState
是函数组件管理状态的基础,需讲清“用法”和“坑点”:
使用方法:
- 导入:
import { useState } from 'react'
。 - 初始化:
const [state, setState] = useState(初始值)
,其中:state
:当前状态值(如输入框内容、列表数据)。setState
:更新状态的函数(必须调用此函数更新,不能直接修改state
)。
- 示例:
const [count, setCount] = useState(0); setCount(count + 1);
(点击按钮时更新计数)。
- 导入:
注意事项:
- 初始值只在首次渲染生效:若初始值是复杂计算(如
useState(heavyCompute())
),首次渲染后不会再执行,若需动态初始值,可传函数(useState(() => heavyCompute())
)。 setState
是异步的:在合成事件(如onClick
)、useEffect
回调中,setState
会批量更新,无法立即获取最新state
(如setCount(count + 1); console.log(count)
仍输出旧值),需通过回调函数获取最新值(setCount(prev => prev + 1)
)。- 更新对象/数组需浅拷贝:
state
是引用类型时,不能直接修改原数据(如const [user, setUser] = useState({ name: 'a' }); user.name = 'b'
无效),需浅拷贝后更新(setUser({ ...user, name: 'b' })
)。
- 初始值只在首次渲染生效:若初始值是复杂计算(如
7. useEffect
的作用及依赖项如何设置?
useEffect
是函数组件处理“副作用”的核心,需明确“作用”和“依赖项规则”:
作用:
副作用指“组件渲染外的操作”(如数据请求、DOM 操作、事件监听、定时器),useEffect
用于统一管理这些操作,并控制其执行时机(挂载、更新、卸载)。基本语法:
useEffect(() => { // 副作用逻辑(如发起请求、绑定监听) return () => { // 清理逻辑(如取消请求、移除监听),组件卸载或依赖变化前执行 }; }, [deps]); // 依赖项数组,控制 useEffect 何时执行
依赖项设置规则:
- 空数组
[]
:只执行一次(组件挂载时执行副作用,卸载时执行清理),模拟componentDidMount
+componentWillUnmount
。 - 指定依赖
[a, b]
:副作用和清理逻辑会在组件挂载时执行,且当a
或b
变化时重新执行(模拟componentDidUpdate
)。 - 不写依赖项:每次组件渲染(包括初始渲染和更新)都会执行副作用和清理逻辑,可能导致性能问题,不推荐。
- 依赖项必须完整:
useEffect
内部使用的所有外部变量(如state
、props
、函数)都必须加入依赖数组,否则会触发“闭包陷阱”(获取到过时的变量值)。
- 空数组
8. React 中组件通信的方式有哪些?
组件通信是实战核心,需按“组件关系”分类说明,覆盖常用场景:
父组件 → 子组件(父传子):
- 方式:通过 Props 传递数据(如
const Parent = () => <Child name="小明" age={18} />
),子组件通过参数接收const Child = (props) => <div>{props.name}</div>
。 - 适用:直接父子关系,传递简单数据或回调函数。
- 方式:通过 Props 传递数据(如
子组件 → 父组件(子传父):
- 方式:父组件传递回调函数给子组件,子组件调用该函数并传递数据(如
Parent
传onChange={handleChildData}
,Child
中onClick={() => onChange(childData)}
)。 - 适用:直接父子关系,子组件需通知父组件更新状态(如输入框子组件传递输入值)。
- 方式:父组件传递回调函数给子组件,子组件调用该函数并传递数据(如
兄弟组件通信:
- 方式:共同父组件中转(兄弟 A 传数据给父,父再通过 Props 传给兄弟 B);或用 Context API(兄弟层级深时)。
- 适用:同一父组件下的兄弟组件(如两个按钮组件联动)。
跨层级组件通信(祖孙/深层):
- 方式 1:Context API(创建全局 Context,祖先组件提供数据,深层子组件消费数据,避免 Props 透传)。
- 方式 2:状态管理库(如 Redux、Zustand,中大型应用中管理全局状态,如用户信息、购物车)。
- 适用:组件层级深(如 3 层以上),且需共享数据(如主题色、登录状态)。
非嵌套组件通信:
- 方式:事件总线(如
mitt
库,订阅/发布模式,组件 A 发布事件,组件 B 订阅事件);或状态管理库。 - 适用:无直接层级关系的组件(如两个页面组件)。
- 方式:事件总线(如
9. 列表渲染中 key
的作用是什么?为什么不能用 index 作为 key?
key
是列表渲染的性能关键,需解释“作用”和“index 隐患”:
key 的作用:
React 列表渲染时,通过key
识别每个列表项的唯一性,用于 Diff 算法对比新旧列表:- 若
key
相同,React 认为是同一元素,仅更新其 Props/State(复用 DOM)。 - 若
key
不同,React 会销毁旧元素并创建新元素(避免复用错误)。
- 核心目的:减少不必要的 DOM 销毁和创建,提升列表更新性能。
- 若
为什么不能用 index 作为 key?
当列表存在增删、排序操作时,index 会发生变化,导致key
不稳定,引发问题:- DOM 复用错误:若删除列表第一项,后续项的 index 会向前偏移(如原 index=1 变为 index=0),React 会认为“原 index=1 的项是新的 index=0 的项”,复用旧 DOM,导致 UI 显示错误(如输入框内容错乱)。
- 性能下降:index 变化会导致 React 误判“所有后续项都是新元素”,触发大量 DOM 销毁和创建,违背
key
的性能优化初衷。
- 推荐:用列表数据的唯一标识(如后端返回的
id
)作为key
。
10. React 事件处理和原生 DOM 事件有什么区别?
React 事件是“合成事件”,需对比核心差异:
对比维度 | React 合成事件(SyntheticEvent) | 原生 DOM 事件 |
---|---|---|
绑定方式 | 驼峰命名(如 onClick ),写在组件标签上 | 小写命名(如 onclick ),或 addEventListener 绑定 |
事件对象 | 合成事件对象(兼容所有浏览器,封装原生 Event) | 原生 Event 对象(浏览器差异大) |
事件委托 | 事件委托到 document (React 17 后委托到根组件),统一管理事件 | 可委托到任意 DOM 元素(需手动调用 addEventListener ) |
阻止默认行为 | 直接调用 e.preventDefault() (无兼容问题) | 部分浏览器需先调用 e.returnValue = false (如 IE) |
事件池(17 前) | 合成事件对象会被回收,事件回调后无法访问(需用 e.persist() 保留) | 原生事件对象不会被回收,可一直访问 |
11. React 中条件渲染的方式有哪些?
条件渲染是控制 UI 显示的基础,需列举常用方式及适用场景:
if-else 语句:
- 用法:在组件函数中用
if-else
判断,返回不同 JSX。 - 示例:
if (isLogin) { return <UserInfo />; } else { return <LoginBtn />; }
。 - 适用:逻辑复杂的条件(如多个分支判断),不适合嵌入 JSX。
- 用法:在组件函数中用
三元运算符:
- 用法:在 JSX 中通过
{ 条件 ? 渲染内容1 : 渲染内容2 }
实现。 - 示例:
<div>{ isLogin ? '欢迎回来' : '请登录' }</div>
。 - 适用:简单二选一条件,可直接嵌入 JSX,代码简洁。
- 用法:在 JSX 中通过
逻辑与
&&
:- 用法:
{ 条件 && 渲染内容 }
,条件为true
时渲染内容,false
时不渲染(返回null
)。 - 示例:
<div>{ hasMessage && <Message count={5} /> }</div>
(有消息时显示消息组件)。 - 适用:“渲染或不渲染”的场景,避免三元运算符的冗余
: null
。
- 用法:
switch-case 语句:
- 用法:定义函数用
switch
判断状态,返回对应 JSX。 - 示例:
const renderTab = (tab) => { switch(tab) { case 'home': return ; case 'profile': return ; default: return ; } };
- 适用:多个互斥条件(如标签页切换)。
- 用法:定义函数用
元素变量:
- 用法:先定义变量存储要渲染的元素,再在 JSX 中使用。
- 示例:
let content; if (isLogin) { content = ; } else { content = ; } return { content };
- 适用:条件逻辑复杂,需拆分代码提高可读性。
12. useContext
的作用及使用步骤是什么?
useContext
是跨层级通信的基础方案,需讲清“作用”和“三步使用法”:
作用:解决“Props 透传”问题(深层子组件需数据时,无需通过中间无关组件层层传递 Props),实现跨层级组件共享数据(如主题色、登录用户信息)。
使用步骤(3 步):
- 创建 Context:用
React.createContext()
创建 Context 对象,可指定默认值(仅在无匹配 Provider 时生效)。import { createContext } from 'react'; const ThemeContext = createContext('light'); // 默认主题:浅色
- 提供 Context 数据:用
Context.Provider
组件包裹子组件树,通过value
属性传递数据(所有子组件可消费该数据)。const App = () => { const [theme, setTheme] = useState('dark'); // 当前主题:深色 return ({/* 子组件 */}{/* 深层子组件 */} ); };
- 消费 Context 数据:在深层子组件中用
useContext(Context)
获取数据,无需 Props 传递。import { useContext } from 'react'; const Content = () => { const { theme, setTheme } = useContext(ThemeContext); return ( ); };
- 创建 Context:用
注意事项:
- Context 更新会触发所有消费它的组件重渲染,可配合
React.memo
优化(避免无关组件重渲染)。 - 默认值不是“ fallback ”,若 Provider 的
value
为undefined
,消费组件会使用默认值,而非undefined
。
- Context 更新会触发所有消费它的组件重渲染,可配合
13. React 中的状态管理方案有哪些?各自适用场景是什么?
状态管理需按“应用规模”分类,覆盖实习常考方案:
useState + useContext
(轻量方案):- 原理:
useState
管理状态,useContext
实现跨层级传递。 - 适用场景:小型应用、简单全局状态(如主题、用户登录状态),无需引入额外库,成本低。
- 缺点:状态复杂时(如多组件修改同一状态),逻辑分散,难以维护;Context 更新会触发大量组件重渲染(需手动优化)。
- 原理:
Redux(中大型应用方案):
- 原理:基于“单一数据源”“状态只读”“ reducer 纯函数更新”三大原则,通过
store
存储状态,action
描述修改意图,reducer
处理状态更新。 - 配套库:
react-redux
(连接 React 和 Redux)、redux-thunk
(处理异步 action)、redux-saga
(复杂异步逻辑)。 - 适用场景:中大型应用、状态复杂且多组件共享(如电商购物车、订单状态),需统一管理状态和副作用,便于调试(用 Redux DevTools 追踪状态变化)。
- 缺点:配置繁琐,学习成本高,小型应用没必要使用。
- 原理:基于“单一数据源”“状态只读”“ reducer 纯函数更新”三大原则,通过
Zustand(轻量替代方案):
- 原理:基于 React Context,但 API 更简洁,无需 Provider 包裹,直接创建 store,组件用
useStore
钩子获取/修改状态。 - 特点:支持异步逻辑(无需额外库)、内置状态选择器(避免不必要重渲染)、体积小(约 1KB)。
- 适用场景:中小型应用,想替代 Redux 的繁琐配置,同时需要全局状态管理(如后台管理系统的表格筛选状态)。
- 优点:学习成本低,代码简洁,性能优于
useState + useContext
。
- 原理:基于 React Context,但 API 更简洁,无需 Provider 包裹,直接创建 store,组件用
Recoil/Jotai(细粒度状态管理):
- 原理:将全局状态拆分为“原子(atom)”,组件只订阅需要的原子状态,原子更新时只触发订阅它的组件重渲染。
- 适用场景:需要细粒度控制重渲染的场景(如大型表单、多维度筛选列表),避免全局状态更新导致的性能问题。
14. useEffect
和 useLayoutEffect
的区别是什么?
两者都是处理副作用的 Hooks,核心差异在“执行时机”和“用途”:
对比维度 | useEffect | useLayoutEffect |
---|---|---|
执行时机 | 真实 DOM 更新后执行(异步),不阻塞浏览器渲染 | 真实 DOM 更新前执行(同步),阻塞浏览器渲染 |
是否阻塞渲染 | 不阻塞,浏览器先渲染页面,再执行 useEffect | 阻塞,需等 useLayoutEffect 执行完才渲染页面 |
适用场景 | 大部分副作用(如数据请求、事件监听、日志上报),不依赖 DOM 最新状态 | 需操作 DOM 并立即看到效果(如获取 DOM 尺寸后调整样式、避免页面闪烁) |
示例 | 发起接口请求获取列表数据 | 渲染后立即获取 DOM 宽度,设置子组件位置 |
- 注意:
useLayoutEffect
会阻塞渲染,若内部逻辑耗时,会导致页面卡顿,优先使用useEffect
,仅在需要同步操作 DOM 时用useLayoutEffect
。
15. React 性能优化的常用方法有哪些?
性能优化是面试重点,需分“组件优化”“列表优化”“状态优化”等维度梳理:
组件层面优化:
React.memo
包裹函数组件:对函数组件进行浅比较,若 Props 未变化,避免组件重渲染(如const Child = React.memo(({ name }) => <div>{name}</div>)
)。useMemo
缓存计算结果:避免每次渲染都执行昂贵的计算(如const sortedList = useMemo(() => list.sort(), [list])
,仅list
变化时重新排序)。useCallback
缓存函数:避免父组件渲染时生成新函数,导致子组件(React.memo
包裹)不必要重渲染(如const handleClick = useCallback(() => {}, [])
)。
列表层面优化:
- 用唯一
key
优化列表:避免用 index 作为 key,减少 DOM 复用错误和不必要重渲染(见问题 9)。 - 虚拟列表(Virtual List):处理长列表(如 1000+ 条数据)时,只渲染可视区域的元素,隐藏非可视区域元素(用
react-window
或react-virtualized
库),大幅减少 DOM 节点数量。
- 用唯一
状态层面优化:
- 避免不必要的状态更新:只在数据变化时调用
setState
,如输入框 onChange 中避免频繁更新(可配合防抖节流)。 - 拆分状态粒度:将复杂状态拆分为多个独立状态,避免更新一个状态导致整个组件重渲染(如
const [name, setName] = useState(''); const [age, setAge] = useState(18)
,而非const [user, setUser] = useState({ name: '', age: 18 })
)。
- 避免不必要的状态更新:只在数据变化时调用
其他优化:
- 懒加载组件:用
React.lazy
+Suspense
实现组件懒加载,仅在组件需要渲染时才加载其 JS 代码(如路由组件:const Home = React.lazy(() => import('./Home'))
),减少首屏加载时间。 - 减少不必要的 DOM 操作:避免在
render
中创建 DOM 元素(如document.createElement
),尽量通过 JSX 描述 UI。 - 生产环境构建:使用
create-react-app
的npm run build
生成生产环境代码(去除开发环境调试代码、压缩 JS/CSS),减小包体积,提升加载速度。
- 懒加载组件:用
16. React 表单处理的方式有哪些?(受控组件 vs 非受控组件)
表单处理是实战必备,需对比“受控”和“非受控”的核心差异:
(1)受控组件(Controlled Component)
- 定义:表单元素(如 input、select)的值由 React State 完全控制,State 更新 → 表单值更新,表单值变化 → 触发事件同步 State。
- 核心逻辑:通过
value
绑定 State,onChange
事件同步 State。 - 示例(输入框):
const [inputValue, setInputValue] = useState(''); return (setInputValue(e.target.value)} // 同步 State /> );
- 优点:状态可控,易实现实时验证(如输入密码时提示强度)、表单联动(如选择省份后加载城市列表)、数据统一管理。
- 缺点:需编写
onChange
事件,频繁更新 State(简单表单略繁琐)。 - 适用场景:复杂表单(如注册表单、多字段联动表单),需实时反馈或数据校验。
(2)非受控组件(Uncontrolled Component)
- 定义:表单元素的值由 DOM 自身控制,React 不通过 State 同步,而是通过
ref
获取 DOM 元素的值。 - 核心逻辑:用
useRef
创建 ref 绑定表单元素,提交时通过ref.current.value
获取值。 - 示例(输入框):
const inputRef = useRef(null); const handleSubmit = () => { console.log('输入值:', inputRef.current.value); // 获取值 }; return ({/* defaultValue 设初始值 */} );
- 优点:代码简单,无需频繁更新 State,性能略好(减少渲染次数)。
- 缺点:状态不可控,难实现实时验证,需操作 DOM(依赖 ref)。
- 适用场景:简单表单(如登录表单、搜索框),无需实时反馈,仅提交时获取值。
17. 错误边界(Error Boundary)的作用及使用方法是什么?
错误边界是 React 处理组件错误的机制,需讲清“作用”“用法”和“限制”:
作用:捕获子组件树中的 JavaScript 错误(如渲染错误、生命周期错误、子组件构造函数错误),防止错误导致整个应用崩溃,同时显示“备用 UI”(如“页面加载失败,请刷新”)。
使用方法(仅类组件支持):
- 创建类组件,实现两个生命周期方法:
static getDerivedStateFromError(error)
:静态方法,接收错误对象,返回新 State(用于切换到备用 UI),无副作用(不能调用setState
或请求数据)。componentDidCatch(error, errorInfo)
:实例方法,接收错误和错误信息(如组件栈),用于错误日志上报(如发送到 Sentry),可包含副作用。
- 用该组件包裹可能出错的子组件。
- 创建类组件,实现两个生命周期方法:
示例:
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; // 初始无错误 } // 错误发生后更新 State,显示备用 UI static getDerivedStateFromError(error) { return { hasError: true }; } // 上报错误日志 componentDidCatch(error, errorInfo) { console.error('组件错误:', error, errorInfo); // 上报到日志平台:fetch('/log', { method: 'POST', body: JSON.stringify({ error, errorInfo }) }); } render() { if (this.state.hasError) { return 页面加载失败,请刷新重试; // 备用 UI } return this.props.children; // 无错误时渲染子组件 } } // 使用:包裹可能出错的组件 const App = () => ({/* 可能出错的组件 */} );
注意事项(错误边界不能捕获的错误):
- 事件处理中的错误(如
onClick
回调中的错误)。 - 异步代码中的错误(如
setTimeout
、fetch
、Promise
回调)。 - 服务端渲染(SSR)中的错误。
- 错误边界自身的错误(需外层错误边界捕获)。
- 函数组件不能直接作为错误边界(需用类组件包裹)。
- 事件处理中的错误(如
18. React Router(v6)的核心概念及基础使用是什么?
React Router 是单页应用(SPA)路由的核心,v6 是当前主流版本,需讲清“核心概念”和“基础用法”:
(1)核心概念
BrowserRouter
/HashRouter
:路由容器,包裹整个应用,决定路由模式:BrowserRouter
:基于 HTML5 History API(URL 无#
,如https://xxx/home
),需服务端配置支持(避免刷新 404)。HashRouter
:基于 URL 哈希(URL 含#
,如https://xxx/#/home
),无需服务端配置,兼容性好。
Routes
:替换 v5 的Switch
,用于包裹Route
组件,只渲染匹配的第一个Route
(避免多个路由同时匹配)。Route
:路由规则,定义“路径 → 组件”的映射:path
:URL 路径(如/home
、/user/:id
,:id
是路由参数)。element
:路径匹配时渲染的组件(如<Route path="/home" element={<Home />}
)。
Link
:导航组件,替换原生<a>
标签,实现无刷新导航(避免页面重载),如<Link to="/home">首页</Link>
。useNavigate
:编程式导航钩子,替换 v5 的useHistory
,用于通过代码跳转(如登录后跳首页),如const navigate = useNavigate(); navigate('/home');
。useParams
:获取路由参数(如/user/:id
中的id
),如const { id } = useParams();
。useLocation
:获取当前路由位置信息(如pathname
、search
、state
),如const location = useLocation(); console.log(location.pathname);
(当前路径)。
(2)基础使用步骤(v6)
- 安装:
npm install react-router-dom
。 - 配置路由容器:在根组件(如
index.js
)用BrowserRouter
包裹App
:import { BrowserRouter } from 'react-router-dom'; ReactDOM.createRoot(document.getElementById('root')).render( );
- 定义路由规则:在
App.js
中用Routes
和Route
定义路由:import { Routes, Route, Link } from 'react-router-dom'; import Home from './Home'; import User from './User'; import NotFound from './NotFound'; const App = () => ( {/* 导航栏 */} 首页 用户中心 {/* 路由匹配 */} } /> {/* 首页 */} } /> {/* 用户中心(带参数) */} } /> {/* 404 页面 */} );
- 组件中使用路由钩子:在
User.js
中获取路由参数和导航:import { useParams, useNavigate } from 'react-router-dom'; const User = () => { const { id } = useParams(); // 获取路由参数 id(123) const navigate = useNavigate(); // 编程式导航 return ( 用户 ID:{id} ); };
19. React 中如何处理异步请求(如接口调用)?
异步请求是实战核心,需讲清“常用方式”和“注意事项”:
(1)在 useEffect
中处理(最常用)
- 核心逻辑:在
useEffect
回调中发起异步请求(用fetch
或axios
),请求成功后更新 State,同时处理loading
(加载中)、error
(错误)状态,避免内存泄漏(取消未完成的请求)。 - 示例(用 axios 请求用户列表):
import { useState, useEffect } from 'react'; import axios from 'axios'; const UserList = () => { const [list, setList] = useState([]); // 列表数据 const [loading, setLoading] = useState(false); // 加载状态 const [error, setError] = useState(null); // 错误状态 useEffect(() => { // 创建 AbortController 用于取消请求(避免内存泄漏) const controller = new AbortController(); const signal = controller.signal; // 异步请求函数 const fetchUserList = async () => { setLoading(true); // 开始加载 try { // 发起请求,传入 signal 用于取消 const res = await axios.get('https://api.example.com/users', { signal }); setList(res.data); // 成功:更新列表 setError(null); // 清空错误 } catch (err) { // 排除“请求被取消”的错误 if (err.name !== 'CanceledError') { setError('请求失败,请重试'); // 失败:设置错误 setList([]); // 清空列表 } } finally { setLoading(false); // 结束加载(无论成功/失败) } }; fetchUserList(); // 组件卸载或依赖变化前,取消未完成的请求 return () => controller.abort(); }, []); // 依赖空数组:只请求一次 // 渲染逻辑 if (loading) return 加载中...; if (error) return {error}; return ( {list.map((user) => ( {user.name} ))} ); };
(2)用状态管理库处理(如 Redux)
- 核心逻辑:将异步请求逻辑放在 Redux 的
action
中(用redux-thunk
或redux-saga
),请求状态(loading
/data
/error
)存储在store
中,组件通过useSelector
获取状态,useDispatch
触发请求。 - 适用场景:中大型应用,多组件需要共享异步请求的数据(如购物车列表、商品详情),便于统一管理和复用请求逻辑。
(3)自定义 Hooks 封装(复用请求逻辑)
- 核心逻辑:将异步请求逻辑封装为自定义 Hooks(如
useFetch
),组件直接调用 Hooks 获取loading
/data
/error
,减少重复代码。 - 示例(
useFetch
自定义 Hooks):import { useState, useEffect } from 'react'; import axios from 'axios'; // 自定义 Hooks:封装请求逻辑 const useFetch = (url) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const fetchData = async () => { setLoading(true); try { const res = await axios.get(url, { signal: controller.signal }); setData(res.data); setError(null); } catch (err) { if (err.name !== 'CanceledError') setError(err.message); } finally { setLoading(false); } }; fetchData(); return () => controller.abort(); }, [url]); return { data, loading, error }; // 返回状态供组件使用 }; // 组件中使用 const UserList = () => { const { data: list, loading, error } = useFetch('https://api.example.com/users'); // 渲染逻辑... };
(4)注意事项
- 处理
loading
和error
状态:避免用户看到空白页面或错误信息未提示,提升体验。 - 防止内存泄漏:用
AbortController
(fetch
/axios
支持)或clearTimeout
取消未完成的请求/定时器,避免组件卸载后请求成功触发setState
。 - 避免重复请求:通过
useEffect
依赖项控制请求时机(如只在url
变化时请求),或用缓存(如SWR
/React Query
库)优化。
20. 虚拟 DOM 的 Diff 算法原理是什么?(React 的 Reconciliation 过程)
Diff 算法是 React 高效渲染的核心,需解释“核心原则”和“执行过程”:
(1)核心设计原则(减少计算复杂度)
React Diff 算法基于两个假设,将时间复杂度从 O(n³)(全量对比)优化到 O(n)(线性对比):
- 同层比较:只对比 DOM 树的同一层级节点,不跨层级比较(如父节点和子节点不直接对比)。若某一层级节点类型变化,直接销毁该节点及所有子节点,重建新节点树(减少跨层级对比的成本)。
- 类型相同的组件:若两个组件类型相同(如都是
<User />
),则认为其结构相似,继续对比其 Props 和子节点;若类型不同(如<User />
变为<Product />
),则销毁旧组件及子树,创建新组件及子树(避免复杂的内部对比)。 - 列表 key 唯一性:列表节点需用
key
标识唯一性,Diff 时通过key
快速找到可复用的节点,仅进行移动、插入、删除操作(避免重新创建所有列表项)。
(2)Diff 执行过程(Reconciliation 调和阶段)
React 的渲染过程分为“调和阶段(Reconciliation)”和“提交阶段(Commit)”,Diff 算法在调和阶段执行:
生成新旧虚拟 DOM:
- 初始渲染:React 将组件 JSX 编译为初始虚拟 DOM。
- 更新渲染:组件 State/Props 变化后,重新生成新的虚拟 DOM。
对比新旧虚拟 DOM(Diff 核心):
- 根节点对比:
- 若根节点类型不同(如
div
变为span
):销毁旧虚拟 DOM 树,创建新虚拟 DOM 树,进入提交阶段更新真实 DOM。 - 若根节点类型相同:对比其属性(如
className
、style
),记录属性差异;然后递归对比其所有子节点。
- 若根节点类型不同(如
- 子节点对比(分两种情况):
- 无 key 列表:按索引顺序对比子节点,若子节点数量变化或类型变化,后续节点全部重新创建(性能差)。
- 有 key 列表:通过
key
建立新旧子节点的映射关系,找到可复用节点,计算节点的移动、插入、删除操作(如用“最长递增子序列”算法优化移动次数),大幅提升性能。
- 根节点对比:
生成更新队列:对比完成后,React 将所有差异(如属性变化、节点新增/删除/移动)整理成更新队列,传递到提交阶段。
提交阶段(Commit):React 根据更新队列,操作真实 DOM(只更新差异部分),并执行
componentDidMount
/componentDidUpdate
等生命周期函数或useEffect
清理/副作用逻辑。
21. React 18 的核心新特性有哪些?
React 18 是重要版本,需掌握核心特性(体现对 React 生态的关注):
并发渲染(Concurrent Rendering):
- 定义:React 18 引入的核心特性,允许 React中断、暂停、恢复或放弃渲染,不再是“同步渲染到底”。
- 作用:优先响应高优先级任务(如用户输入、点击),延迟低优先级任务(如列表筛选、数据加载),避免页面卡顿,提升用户体验(如用户输入时,列表渲染不会阻塞输入响应)。
- 注意:并发渲染是“底层能力”,需通过高层 API(如
startTransition
)触发。
自动批处理(Automatic Batching):
- 定义:将多个
setState
更新合并为一次渲染,减少 DOM 操作次数(优化性能)。 - 变化:React 17 及之前,仅在“合成事件”(如
onClick
)和useEffect
中支持批处理;React 18 中,所有场景(包括setTimeout
、Promise
回调、fetch
回调)都支持自动批处理。 - 示例:
// React 18 中,以下两个 setState 会合并为一次渲染 setTimeout(() => { setCount(c => c + 1); setName('小明'); }, 1000);
- 取消批处理:用
ReactDOM.flushSync()
强制立即更新(如ReactDOM.flushSync(() => setCount(c => c + 1))
)。
- 定义:将多个
过渡更新(Transitions):
- 定义:区分“紧急更新”和“非紧急更新”,
startTransition
标记的更新为“非紧急更新”,优先执行紧急更新(如用户输入)。 - 作用:避免非紧急更新阻塞紧急更新,导致页面卡顿(如搜索框输入时,实时筛选列表是“非紧急更新”,用
startTransition
标记,确保输入流畅)。 - 示例:
import { startTransition } from 'react'; const handleInput = (value) => { // 紧急更新:更新输入框值(优先执行) setInputValue(value); // 非紧急更新:筛选列表(延迟执行,不阻塞输入) startTransition(() => { setFilteredList(list.filter(item => item.includes(value))); }); };
- 配套 Hook:
useTransition
,返回[isPending, startTransition]
,isPending
表示过渡更新是否正在进行(可显示加载状态)。
- 定义:区分“紧急更新”和“非紧急更新”,
Suspense 增强:
- 作用:
Suspense
用于“等待异步操作完成后渲染组件”,React 18 扩展了其能力:- 支持服务端渲染(SSR)流式渲染:服务端可分段发送 HTML(先发送骨架屏,再发送异步组件内容),提升首屏加载体验。
- 支持客户端异步数据加载:配合
use
Hook(React 18 新增,用于读取 Promise),可在客户端用Suspense
等待接口请求完成(如const data = use(fetchData())
,Suspense
显示加载态)。
- 作用:
useId
Hook:- 作用:生成跨服务端和客户端的唯一 ID,解决服务端渲染(SSR)中“客户端 ID 与服务端 ID 不匹配”的问题(如表单 label 的
for
属性、无障碍 ARIA 属性)。 - 示例:
import { useId } from 'react'; const Input = () => { const id = useId(); // 服务端和客户端生成相同 ID return ( 用户名: ); };
- 作用:生成跨服务端和客户端的唯一 ID,解决服务端渲染(SSR)中“客户端 ID 与服务端 ID 不匹配”的问题(如表单 label 的
22. 自定义 Hooks 的定义、规则及示例是什么?
自定义 Hooks 是复用逻辑的核心,需讲清“定义”“规则”和“实战示例”:
(1)定义
自定义 Hooks 是封装可复用逻辑的函数,名称必须以 use
开头(React 识别 Hooks 的标识),内部可调用其他 React Hooks(如 useState
、useEffect
、useContext
),最终返回组件需要的数据或方法。
(2)核心规则(必须遵守,否则导致 Bug)
名称必须以
use
开头:- 原因:React 通过名称识别 Hooks,确保 Hooks 规则(如不能在条件中调用)生效,若名称不以
use
开头,React 无法检测违规使用。 - 错误示例:
const fetchData = () => { ... }
(应改为const useFetch = () => { ... }
)。
- 原因:React 通过名称识别 Hooks,确保 Hooks 规则(如不能在条件中调用)生效,若名称不以
只能在函数组件或其他自定义 Hooks 中调用:
- 禁止在
if-else
、for
循环、普通函数(非组件/非 Hooks)中调用,避免 Hooks 执行顺序混乱(React 依赖 Hooks 执行顺序维护状态)。 - 错误示例:
const Component = () => { if (isLogin) { const { data } = useFetch('/user'); // 错误:在 if 中调用 Hooks } };
- 禁止在
可传递参数和返回值:
- 根据复用逻辑需求,自定义 Hooks 可接收参数(如请求 URL、配置项),返回组件需要的状态(如
data
/loading
/error
)或方法(如refresh
刷新函数)。
- 根据复用逻辑需求,自定义 Hooks 可接收参数(如请求 URL、配置项),返回组件需要的状态(如
(3)实战示例:useLocalStorage
(封装本地存储逻辑)
需求:封装“读取/写入 localStorage”的逻辑,支持组件同步 localStorage 数据(如记住用户名、主题设置)。
import { useState, useEffect } from 'react';
// 自定义 Hooks:封装 localStorage 逻辑
const useLocalStorage = (key, initialValue) => {
// 1. 初始化状态:优先从 localStorage 读取,无则用初始值
const [value, setValue] = useState(() => {
try {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : initialValue;
} catch (err) {
// 处理异常(如 localStorage 被禁用)
console.error('读取 localStorage 失败:', err);
return initialValue;
}
});
// 2. 监听 value 变化,同步到 localStorage
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (err) {
console.error('写入 localStorage 失败:', err);
}
}, [key, value]); // 依赖 key 和 value,变化时同步
// 3. 返回 value 和更新方法(与 useState 用法一致)
return [value, setValue];
};
// 组件中使用
const LoginForm = () => {
// 用 useLocalStorage 替代 useState,自动同步 localStorage
const [username, setUsername] = useLocalStorage('username', '');
return (setUsername(e.target.value)}
placeholder="输入用户名"
/>
记住的用户名:{username}
);
};
- 优势:组件无需重复编写
localStorage.getItem
/setItem
逻辑,且数据自动同步,复用性强。
23. 什么是 Props Drilling(Props 透传)?如何解决?
Props Drilling 是实际开发中的常见问题,需解释“定义”和“解决方案”:
(1)定义
Props Drilling(Props 透传/Props 钻取)指:当组件层级较深时,父组件的 Props 需要经过中间多个“无关组件”层层传递,才能到达深层子组件的现象。中间组件本身不需要这些 Props,仅起到“传递”作用,导致代码冗余、维护成本高。
- 示例:
App(父)→ Header(中间,无关)→ Nav(中间,无关)→ UserAvatar(深层,需要 user 数据)
,App
的user
Props 需通过Header
、Nav
传递给UserAvatar
,Header
和Nav
本身不需要user
。
(2)解决方案(按场景选择)
使用 Context API:
- 适用场景:中小型应用,跨层级传递少量全局状态(如主题、用户信息)。
- 原理:创建全局 Context,父组件用
Context.Provider
提供数据,深层子组件用useContext
直接获取数据,跳过中间组件。 - 优点:无需 Props 透传,代码简洁;无需引入额外库。
- 缺点:Context 更新会触发所有消费组件重渲染(需配合
React.memo
优化);状态复杂时逻辑分散。
使用状态管理库:
- 适用场景:中大型应用,状态复杂且多组件共享(如购物车、订单状态)。
- 方案:用 Redux、Zustand、Recoil 等库,将状态存储在全局 Store 中,任何组件(无论层级)都可直接从 Store 获取/修改状态,完全避免 Props 透传。
- 优点:状态集中管理,便于调试和维护;支持复杂异步逻辑。
- 缺点:小型应用成本高(如 Redux 配置繁琐)。
组件组合(Component Composition):
- 适用场景:深层子组件与父组件存在直接逻辑关联,但中间层组件无需感知数据(如弹窗内的按钮需调用父组件的关闭方法)。
- 原理:父组件直接将深层子组件作为
children
传递给中间组件,跳过 Props 透传(中间组件只需渲染children
即可)。 - 示例:
// 父组件:直接传递子组件(含所需数据/方法) const App = () => { const [isOpen, setIsOpen] = useState(false); return {/* 中间组件 Modal 只需渲染 children */}setIsOpen(false)} /> {/* 深层子组件直接获取方法 */} ; }; // 中间组件:无需接收/传递 onClose,仅渲染 children const Modal = ({ children }) => {children}; // 深层子组件:直接使用父组件传递的 onClose const ModalContent = ({ onClose }) => ;
- 优点:避免 Props 透传,逻辑更清晰;不依赖全局状态,适合局部组件通信。
24. useMemo
和 useCallback
的作用及使用场景是什么?
两者均用于缓存,需明确“缓存内容”“使用场景”和“性能优化核心”:
(1)useMemo
- 作用:缓存计算结果(值),避免组件每次渲染时重复执行昂贵的计算(如大数据排序、复杂过滤)。
- 语法:
const memoizedValue = useMemo(() => 计算函数, [依赖项数组])
- 依赖项变化时,重新执行计算函数并更新缓存;否则直接返回缓存值。
- 示例:
// 缓存排序结果(仅 list 变化时重新排序) const sortedList = useMemo(() => { return list.sort((a, b) => a.price - b.price); // 假设 list 是大数据数组 }, [list]);
- 适用场景:
- 计算逻辑耗时(如 O(n²) 复杂度的操作)。
- 计算结果作为 Props 传递给子组件,且子组件用
React.memo
优化(避免因计算结果引用变化导致子组件重渲染)。
(2)useCallback
- 作用:缓存函数引用,避免组件每次渲染时创建新的函数实例(即使函数逻辑相同)。
- 语法:
const memoizedCallback = useCallback(() => 函数逻辑, [依赖项数组])
- 依赖项变化时,创建新函数并更新缓存;否则返回缓存的函数引用。
- 示例:
// 缓存回调函数(仅 userId 变化时创建新函数) const handleDelete = useCallback((id) => { setList(list.filter(item => item.id !== id)); }, [list]);
- 适用场景:
- 函数作为 Props 传递给子组件,且子组件用
React.memo
优化(避免因函数引用变化导致子组件不必要重渲染)。 - 函数作为
useEffect
的依赖项(避免因函数重新创建导致useEffect
频繁执行)。
- 函数作为 Props 传递给子组件,且子组件用
(3)核心注意事项
- 不要过度使用:缓存本身有性能开销(存储和对比依赖项),简单计算或函数无需缓存(如
() => setCount(c + 1)
无需useCallback
)。 - 依赖项必须完整:
useMemo
/useCallback
内部使用的变量必须加入依赖数组,否则可能获取到过时的值(闭包陷阱)。
25. React 中如何实现组件的懒加载(代码分割)?
组件懒加载是优化首屏加载速度的关键,需讲清“核心 API”和“使用步骤”:
(1)核心原理
通过“代码分割(Code Splitting)”将应用代码拆分为多个小块(chunk),仅在组件需要渲染时才加载对应代码,减少首屏加载的 JS 体积,提升加载速度。
(2)实现方式(React.lazy
+ Suspense
)
React.lazy
:动态导入组件,返回一个“懒加载组件”(仅在首次渲染时加载组件代码)。- 语法:
const LazyComponent = React.lazy(() => import('./LazyComponent'))
- 注意:
import
必须返回一个default export
的组件(不支持命名导出,需在组件文件中用export default
)。
- 语法:
Suspense
:配合React.lazy
使用,在懒加载组件加载完成前显示“加载占位符”(如骨架屏、加载动画),避免页面空白。- 语法:
<Suspense fallback={<Loading />}><LazyComponent /></Suspense>
fallback
:必填,接收 JSX 作为加载时的占位内容。
- 语法:
(3)实战示例(路由级懒加载)
路由组件通常是代码分割的最佳场景(用户不会同时访问所有页面):
import { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Loading from './Loading'; // 加载占位组件
// 懒加载路由组件(仅访问对应路由时加载代码)
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const User = lazy(() => import('./pages/User'));
const App = () => (
{/* Suspense 包裹所有懒加载组件,统一显示加载态 */}
}>
} />
} />
} />
);
(4)注意事项
- 错误处理:懒加载可能失败(如网络错误),需用错误边界(Error Boundary) 捕获加载错误,显示友好提示(如“页面加载失败,请重试”)。
- 服务端渲染(SSR):
React.lazy
不支持服务端渲染,需用loadable-components
等库替代。 - 拆分粒度:避免过度拆分(如每个组件拆分为单独 chunk),可能导致请求数量过多;建议按路由或功能模块拆分。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/909252.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!