React 面试高频题可以从 核心概念、Hooks、性能优化、状态管理、路由、工程化 等方面展开,以下是精选的高频题目及详细解析,覆盖面试中最常考察的知识点:
一、核心概念
1. React 的核心特点是什么?
- 组件化:将 UI 拆分为独立、可复用的组件,降低复杂度。
- 声明式编程:只描述 UI 应该是什么样子,而非如何更新 DOM,减少命令式操作。
- 虚拟 DOM:通过内存中的虚拟 DOM 树缓存 UI 状态,对比新旧虚拟 DOM 差异,批量更新真实 DOM,提升性能。
- 单向数据流:数据从父组件向下传递,子组件通过回调函数通知父组件修改数据,避免数据混乱。
- JSX 语法:将 HTML 与 JavaScript 结合,直观描述 UI 结构,编译后转为
React.createElement调用。
2. 虚拟 DOM 的工作原理?
- 生成虚拟 DOM:组件渲染时,React 将 JSX 转换为虚拟 DOM(一个 JavaScript 对象树,描述节点类型、属性、子节点)。
- 对比新旧虚拟 DOM:当状态变化时,生成新的虚拟 DOM,通过 Diff 算法 对比新旧树的差异(只对比同层节点,降低复杂度)。
- 批量更新真实 DOM:收集所有差异(“补丁”),一次性更新真实 DOM,减少 DOM 操作次数(DOM 操作是性能瓶颈)。
3. React 的生命周期(旧版 class 组件)?
分为三个阶段:
-
挂载阶段:组件创建并插入 DOM。
constructor:初始化状态、绑定事件。componentWillMount(已废弃):挂载前执行,不推荐操作 DOM。render:返回 JSX,纯函数(无副作用)。componentDidMount:挂载后执行,可操作 DOM、发起网络请求。
-
更新阶段:组件状态 / 属性变化时触发。
componentWillReceiveProps(已废弃):接收新 props 时执行。shouldComponentUpdate:判断是否需要重新渲染(返回false则跳过更新,优化性能)。componentWillUpdate(已废弃):更新前执行。render:重新渲染。componentDidUpdate:更新后执行,可操作更新后的 DOM。
-
卸载阶段:组件从 DOM 中移除。
componentWillUnmount:卸载前执行,清理定时器、事件监听、网络请求等副作用。
4. React 事件系统的特点?
-
合成事件:React 封装了原生 DOM 事件,提供统一的事件对象(
SyntheticEvent),跨浏览器兼容。 -
事件委托:所有事件都委托到
document节点(或根组件),减少 DOM 事件绑定次数,提升性能。 -
异步批量处理:React 会批量处理事件回调中的状态更新(例如,多次
setState合并为一次渲染)。 -
与原生事件的区别:
- 合成事件的
this需通过bind或箭头函数绑定(class 组件)。 - 原生事件需手动移除监听,合成事件在组件卸载时自动清理。
- 合成事件的
二、Hooks 相关
1. 为什么需要 Hooks?
-
解决 class 组件的问题:
- 避免复杂的生命周期(如
componentDidMount中混杂网络请求和 DOM 操作)。 - 解决 HOC 和 Render Props 导致的 “嵌套地狱”。
- 无需绑定
this,简化代码。
- 避免复杂的生命周期(如
-
让函数组件拥有状态和副作用:函数组件原本是纯函数,Hooks 让其可以管理状态(
useState)、执行副作用(useEffect)等。
2. 常用 Hooks 及其作用?
-
useState:管理组件状态,返回[状态值, 更新函数]。- 示例:
const [count, setCount] = useState(0);
- 示例:
-
useEffect:处理副作用(网络请求、DOM 操作、定时器等),相当于componentDidMount、componentDidUpdate、componentWillUnmount的集合。- 依赖数组为空:仅在挂载时执行。
- 依赖数组包含变量:变量变化时执行。
- 无依赖数组:每次渲染后执行。
- 返回清理函数:组件卸载或依赖变化前执行(清理副作用)。
-
useRef:创建一个可变的 ref 对象,用于存储 DOM 元素、定时器 ID 等,更新 ref 不会触发组件重新渲染。- 示例:
const inputRef = useRef(null);(用于获取输入框 DOM 节点)。
- 示例:
-
useContext:获取 Context 中的数据,避免多层组件传递 props(“prop drilling”)。 -
useReducer:用于复杂状态管理(如状态依赖 previous state、多个子值),类似 Redux 的 reducer。 -
useMemo:缓存计算结果,避免每次渲染重复计算(依赖数组变化时才重新计算)。 -
useCallback:缓存函数引用,避免每次渲染创建新函数(用于传递给子组件,配合React.memo优化性能)。
3. Hooks 的使用规则?
- 只能在函数组件或自定义 Hooks 的顶层调用(不能在条件语句、循环、嵌套函数中调用)。
- 只能在 React 函数组件或自定义 Hooks 中使用(不能在普通 JavaScript 函数中使用)。
- 遵循命名规范:自定义 Hooks 必须以
use开头(如useLocalStorage)。
4. useState 和 useReducer 的区别?
-
useState:适用于简单状态(如单个数字、字符串),更新函数是直接替换状态。 -
useReducer:适用于复杂状态(如对象、数组,或状态更新依赖 previous state),通过 dispatch 动作(action)触发状态更新,逻辑更清晰、可预测。- 示例:
const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; default: return state; } }; const [state, dispatch] = useReducer(reducer, { count: 0 }); dispatch({ type: 'INCREMENT' });
- 示例:
5. useEffect 的依赖数组?
- 依赖数组是
useEffect的第二个参数,用于控制副作用的执行时机。 - 若依赖数组为空(
[]):副作用仅在组件挂载时执行一次,卸载时执行清理函数。 - 若依赖数组包含变量(如
[count]):副作用在组件挂载时执行,且每次count变化时重新执行。 - 若不传递依赖数组:副作用在每次组件渲染后(包括初始渲染和更新渲染)都执行。
- 注意:依赖数组必须包含所有在
useEffect中使用的外部变量(否则会导致闭包问题,使用旧的变量值)。
三、性能优化
1. React 性能优化的常用手段?
-
减少不必要的渲染:
- 用
React.memo包装纯函数组件(浅比较 props,避免父组件更新导致子组件不必要渲染)。 - 用
useMemo缓存计算结果,useCallback缓存函数引用(配合React.memo使用)。 - class 组件中重写
shouldComponentUpdate(返回false跳过更新)。
- 用
-
虚拟列表:长列表场景下(如 thousands 条数据),使用
react-window或react-virtualized只渲染可视区域的列表项,减少 DOM 节点数量。 -
懒加载:
- 路由懒加载:
React.lazy(() => import('./Page'))配合Suspense实现按需加载。 - 图片懒加载:使用
react-lazyload或原生IntersectionObserverAPI。
- 路由懒加载:
-
避免直接修改状态:React 状态更新是不可变的(如修改数组 / 对象需创建新实例),否则 React 无法检测状态变化。
- 错误示例:
state.array.push(1);(直接修改原数组)。 - 正确示例:
setState(prev => [...prev.array, 1]);(创建新数组)。
- 错误示例:
-
批量更新状态:React 会自动批量处理同一事件回调中的多个
setState,若需手动批量更新(如异步操作中),可使用ReactDOM.unstable_batchedUpdates。 -
使用生产环境构建:通过
npm run build生成优化后的生产包(移除开发环境的警告、调试代码)。
2. React.memo 的作用?
-
React.memo是一个高阶组件(HOC),用于包装纯函数组件。 -
作用:浅比较组件的 props,若 props 未变化,则跳过组件的重新渲染,提升性能。
-
注意:
-
仅比较 props,不比较 state(组件自身 state 变化仍会触发渲染)。
-
浅比较:对于对象 / 数组,只比较引用是否相同,不深比较内部值(若需深比较,可传递第二个参数
areEqual函数)。 -
示例:
const MyComponent = React.memo((props) => { // 组件逻辑 }, (prevProps, nextProps) => { // 自定义比较逻辑,返回 true 表示 props 相同,不重新渲染 return prevProps.count === nextProps.count; });
-
四、状态管理
1. React 状态管理方案对比(Context + useReducer vs Redux vs MobX)?
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Context + useReducer | 中小型应用,状态共享范围小(如主题、用户信息) | 原生支持,无需第三方库,轻量 | 不支持中间件(如日志、异步),状态复杂时冗余 |
| Redux | 大型应用,状态复杂、共享范围广 | 生态完善(中间件、DevTools),可预测性强 | 配置繁琐,样板代码多 |
| MobX | 中大型应用,追求开发效率 | 响应式编程,自动追踪状态依赖,代码简洁 | 灵活性高导致规范难统一,调试相对复杂 |
2. Redux 的核心概念和工作流程?
-
核心概念:
Store:存储应用状态的容器(唯一)。State:Store 中存储的状态数据(只读)。Action:描述状态变化的普通对象(必须有type字段,如{ type: 'INCREMENT' })。Reducer:纯函数,接收state和action,返回新的state((state, action) => newState)。Dispatch:触发状态更新的函数(store.dispatch(action))。
-
工作流程:
- 组件通过
store.dispatch(action)发送一个动作。 - Redux 调用 Reducer,传入当前
state和action,计算新的state。 - Store 更新
state,并通知所有订阅了 Store 的组件。 - 组件重新渲染。
- 组件通过
3. Redux 中间件的作用?
-
中间件是 Redux 处理副作用(如异步请求、日志、崩溃监控)的扩展点,位于
dispatch(action)和reducer之间。 -
常用中间件:
redux-thunk:支持异步 Action(Action 可以是函数,接收dispatch和getState)。redux-saga:处理复杂异步逻辑(如并发请求、取消请求),基于 Generator 函数。redux-logger:打印 Action 和状态变化日志,方便调试。
五、路由
1. React Router 的核心组件?
BrowserRouter:使用 HTML5 History API 实现路由(URL 无#),需要后端配置支持(所有路由指向 index.html)。HashRouter:使用 URL 的#后面的部分作为路由(锚点路由),无需后端配置。Route:定义路由规则(path路径,component组件,exact精确匹配)。Switch:只渲染第一个匹配的Route(避免多个路由同时匹配)。Link:路由跳转组件(类似<a>标签,无页面刷新)。Redirect:重定向路由(如未登录时重定向到登录页)。useHistory/useNavigate:编程式导航(如登录后跳转首页)。
2. React Router 中 history 对象的常用方法?
push(path):跳转到指定路径(添加新历史记录)。replace(path):替换当前路径(不添加新历史记录)。go(n):前进 / 后退 n 步(如go(-1)后退一页)。goBack():后退一页(等价于go(-1))。goForward():前进一页(等价于go(1))。
六、工程化与实践
1. React 项目的目录结构?
常见目录结构(基于 create-react-app):
src/
├── assets/ # 静态资源(图片、样式文件)
├── components/ # 公共组件(可复用的UI组件)
│ ├── Button/
│ │ ├── index.js
│ │ └── Button.css
├── pages/ # 页面组件(路由对应的页面)
│ ├── Home/
│ ├── About/
├── hooks/ # 自定义Hooks
├── context/ # Context相关文件
├── redux/ # Redux相关文件(store、reducers、actions)
├── router/ # 路由配置
├── utils/ # 工具函数
├── services/ # API请求封装
├── App.js # 根组件
└── index.js # 入口文件
2. 如何处理 React 中的跨域问题?
-
开发环境:使用
create-react-app内置的代理(修改package.json):"proxy": "http://localhost:3001" // 后端接口地址或使用
http-proxy-middleware自定义代理规则。 -
生产环境:
- 后端配置 CORS(允许跨域域名)。
- 使用 Nginx 反向代理(将前端请求转发到后端接口)。
3. React 中如何实现组件通信?
- 父传子:通过
props传递数据 / 函数。 - 子传父:父组件传递回调函数,子组件调用该函数传递数据。
- 兄弟组件:通过父组件中转(子 1 传父,父传子 2),或使用 Context/Redux。
- 跨层级组件:使用 Context + useContext,或 Redux/MobX 等状态管理库。
- 非关系组件:使用事件总线(如
events库),或状态管理库。
4. React 错误边界(Error Boundary)的作用?
-
捕获子组件树中的 JavaScript 错误(渲染、生命周期、事件回调中的错误),并显示备用 UI(避免整个应用崩溃)。
-
只能捕获子组件的错误,不能捕获自身、异步代码(如
setTimeout)、事件回调中的错误。 -
实现方式(class 组件):
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; // 更新状态,显示备用UI } componentDidCatch(error, errorInfo) { console.log(error, errorInfo); // 日志上报 } render() { if (this.state.hasError) { return <h1>Something went wrong.</h1>; // 备用UI } return this.props.children; } } -
使用:
<ErrorBoundary><ChildComponent /></ErrorBoundary>
总结
React 面试高频题主要围绕 核心原理(虚拟 DOM、生命周期)、Hooks 用法与规则、性能优化手段、状态管理方案、路由配置、组件通信 等方面。准备时需结合实际项目经验,理解每个知识点的底层逻辑和实际应用场景,避免死记硬背。