237 lines
11 KiB
Markdown
237 lines
11 KiB
Markdown
# React 深度面试题及解析 (Vue 3 开发者视角版)
|
||
|
||
> 本文档专为熟悉 Vue 3 + TypeScript 的开发者编写。我们将通过对比 Vue 3 的核心概念(响应式、Composition API、Diff 算法等)来深度解析 React 的原理,帮助你快速建立映射关系并掌握 React 高频面试点。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [核心设计理念对比](#1-核心设计理念对比)
|
||
2. [Hooks 与 Composition API](#2-hooks-与-composition-api)
|
||
3. [Fiber 架构与并发模式](#3-fiber-架构与并发模式)
|
||
4. [状态管理与组件通信](#4-状态管理与组件通信)
|
||
5. [性能优化机制](#5-性能优化机制)
|
||
6. [Diff 算法深度解析](#6-diff-算法深度解析)
|
||
7. [TypeScript 实战差异](#7-typescript-实战差异)
|
||
|
||
---
|
||
|
||
## 1. 核心设计理念对比
|
||
|
||
### Q1: React 的"不可变数据" (Immutable) 与 Vue 的"可变响应式" (Mutable) 有什么本质区别?为什么 React 需要它?
|
||
|
||
**Vue 3 视角解析:**
|
||
在 Vue 3 中,我们习惯直接修改对象 `state.count++`,Proxy 会拦截这个操作并自动触发更新。这是 **"Mutable + 细粒度依赖收集"**。
|
||
|
||
**React 原理:**
|
||
React 是 **"Immutable + 全量检测"**(组件级)。
|
||
- **不可变性**:React 中不能直接修改 state (`this.state.count++` 是无效的),必须调用 `setState` 传入一个新的值。
|
||
- **为什么**:React 没有细粒度的依赖收集系统。当状态变化时,React 默认不知道具体哪个属性变了,它只知道"组件需要更新了"。通过比较 `oldState === newState` (浅比较) 来决定是否需要重新渲染。如果数据是可变的,引用没变但内容变了,React 就无法快速感知变化,或者需要昂贵的深比较。
|
||
|
||
**面试回答要点:**
|
||
1. **数据流向**:React 强调单向数据流和不可变性,通过 `setState` 触发更新,生成全新的 Virtual DOM 树。
|
||
2. **更新策略**:Vue 是"推"(Push)模式,依赖变了自动推送到组件;React 是"拉"(Pull)模式,状态变了,React 重新执行组件函数,产出新 UI。
|
||
3. **心智模型**:React 组件本质是 `UI = f(state)`,每次 Render 都是一次全新的函数调用,闭包在其中扮演核心角色(Capture Value 特性)。
|
||
|
||
---
|
||
|
||
### Q2: JSX 与 Vue Template 的编译结果有什么不同?
|
||
|
||
**Vue 3 视角解析:**
|
||
Vue Template 编译成 Render Function,但 Vue 做了大量**编译时优化**(PatchFlags、静态提升、Block Tree),能静态分析出哪些节点是动态的。
|
||
|
||
**React 原理:**
|
||
JSX 本质是 `React.createElement` (或 `_jsx`) 的语法糖。
|
||
- **灵活性**:JSX 是完全的 JavaScript,拥有 JS 的全部能力(变量、逻辑控制)。
|
||
- **优化难度**:因为太灵活(动态性太强),React 很难像 Vue 那样做极致的编译时优化(虽然 React Compiler/React Forget 正在尝试解决这个问题)。React 更多依赖运行时的 Fiber 架构来调度更新。
|
||
|
||
**代码对比:**
|
||
|
||
```typescript
|
||
// React JSX
|
||
const element = <div className="foo">{isShow && <Span />}</div>;
|
||
// 编译为: React.createElement('div', { className: 'foo' }, isShow && React.createElement(Span))
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Hooks 与 Composition API
|
||
|
||
### Q3: `useEffect` 的依赖数组 (Dependency Array) 为什么容易产生闭包陷阱?与 Vue `watch` 有何不同?
|
||
|
||
**Vue 3 视角解析:**
|
||
Vue 的 `watchEffect` 自动收集依赖,`watch` 也可以直观地监听 ref。Vue 的组件 setup 只运行一次,闭包问题较少。
|
||
|
||
**React 原理:**
|
||
React 函数组件**每次渲染都会重新执行**。
|
||
- **闭包陷阱**:如果在 `useEffect` 中使用了某个 state 但没放入依赖数组,`useEffect` 内部引用的就是**上一次渲染时的旧变量**(闭包捕获了旧值)。
|
||
- **Stale Closure**:这是 React Hooks 最核心的痛点之一。
|
||
|
||
**示例:**
|
||
|
||
```typescript
|
||
function Counter() {
|
||
const [count, setCount] = useState(0);
|
||
|
||
useEffect(() => {
|
||
const timer = setInterval(() => {
|
||
// 错误:这里的 count 永远是 0 (第一次渲染时的闭包)
|
||
console.log(count);
|
||
// 修正:setCount(c => c + 1) 或将 count 加入依赖数组
|
||
}, 1000);
|
||
return () => clearInterval(timer);
|
||
}, []); // [] 导致 effect 只执行一次,捕获了初始作用域
|
||
}
|
||
```
|
||
|
||
**面试回答要点:**
|
||
1. **执行机制**:Hooks 依赖于函数组件的多次执行,利用闭包保存状态。
|
||
2. **依赖数组**:必须诚实地列出所有依赖,否则会读取到旧值。
|
||
3. **对比 Vue**:Vue 的 setup 仅执行一次,响应式数据是引用的 Proxy,不存在"旧值"问题,心智负担更小;React 需要开发者手动维护依赖。
|
||
|
||
### Q4: 为什么 React Hooks 不能写在条件语句(if/for)里?
|
||
|
||
**Vue 3 视角解析:**
|
||
Vue Composition API (`ref`, `reactive`) 可以随便写,因为 setup 只跑一次,变量声明了就在那。
|
||
|
||
**React 原理:**
|
||
React 内部通过**链表**(Linked List)来存储 Hooks 的状态。
|
||
- **顺序很重要**:React 没有名字来区分 `useState(1)` 和 `useState(2)`,它完全依赖**调用顺序**来对应状态。
|
||
- 如果放在 `if` 里,某次渲染跳过了一个 Hook,后面的 Hook 拿到的状态就会错位(比如把 `name` 的 state 给了 `age`)。
|
||
|
||
**源码简化逻辑:**
|
||
```javascript
|
||
// 伪代码
|
||
let hooks = [];
|
||
let currentHookIndex = 0;
|
||
|
||
function useState(initial) {
|
||
const hook = hooks[currentHookIndex] || { state: initial };
|
||
hooks[currentHookIndex] = hook;
|
||
currentHookIndex++; // 索引自增,依赖顺序
|
||
return [hook.state, setState];
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Fiber 架构与并发模式
|
||
|
||
### Q5: 什么是 React Fiber?它解决了什么问题?(对比 Vue 的更新机制)
|
||
|
||
**Vue 3 视角解析:**
|
||
Vue 的更新是**细粒度**的。组件级 Watcher 知道具体哪个组件变了,更新过程通常很快,不需要"时间切片"这种复杂机制。
|
||
|
||
**React 原理:**
|
||
React 的更新通常是**全量递归**(从根节点或 Context Provider 开始)。在 React 15(Stack Reconciler)时代,一旦开始 Diff,就必须递归到底,中间无法中断。如果树很深,JS 线程被占用超过 16ms,页面就会掉帧卡顿。
|
||
|
||
**Fiber 架构:**
|
||
1. **数据结构**:将递归的树结构转变为**链表**结构(Fiber Node)。这使得遍历可以**暂停、中止、恢复**。
|
||
2. **时间切片 (Time Slicing)**:将渲染任务拆分成小块。浏览器空闲时(`requestIdleCallback` 概念)执行一部分 Diff,有高优先级任务(如用户输入)插队时,暂停低优先级任务。
|
||
3. **双缓存 (Double Buffering)**:在内存中构建好新的 Fiber 树(workInProgress tree),构建完成后一次性替换 Current tree,减少页面闪烁。
|
||
|
||
**面试回答要点:**
|
||
- **核心目标**:实现**并发渲染 (Concurrent Rendering)**,解决 CPU 密集型更新导致的页面卡顿。
|
||
- **实现方式**:将同步的递归 Diff 改为异步的可中断遍历。
|
||
|
||
---
|
||
|
||
## 4. 状态管理与组件通信
|
||
|
||
### Q6: Redux/Zustand 与 Vuex/Pinia 的区别?
|
||
|
||
**Vue 3 视角解析:**
|
||
Pinia 本质是基于 Proxy 的全局响应式对象,非常直观,修改 state 直接赋值即可。
|
||
|
||
**React 原理:**
|
||
Redux 是典型的**单向数据流** + **不可变数据**。
|
||
- **Action -> Reducer -> New Store**。
|
||
- 必须返回新的 State 对象,不能直接修改。
|
||
- **Context API**:React 自带的跨组件通信,但有性能缺陷(Provider 更新,所有 Consumer 强制重渲染),通常配合 `useMemo` 优化,或者使用 Zustand/Recoil 等库。
|
||
|
||
**Zustand (推荐)**:
|
||
Zustand 的用法非常像 Vue 3 的 Composition API + Pinia,去除了 Redux 的样板代码,支持直接修改(通过 immer)或返回新对象,是目前 React 生态中最符合直觉的库。
|
||
|
||
---
|
||
|
||
## 5. 性能优化机制
|
||
|
||
### Q7: `useMemo` 和 `useCallback` 是做什么的?Vue 为什么很少需要它们?
|
||
|
||
**Vue 3 视角解析:**
|
||
Vue 的 `computed` 自动缓存,组件更新也是自动精确控制的。子组件 props 没变,Vue 默认就不会去递归更新子组件(除非插槽等情况)。
|
||
|
||
**React 原理:**
|
||
React 组件默认行为:**父组件更新,所有子组件无条件重新渲染**。
|
||
- **性能浪费**:即使子组件 props 没变,也会运行。
|
||
- **React.memo**:高阶组件,用于包裹子组件,做 Props 的浅比较(类似 Vue 的默认行为)。
|
||
- **useCallback**:
|
||
- 问题:父组件每次 Render,定义的函数 `const handleClick = () => {}` 都是**新引用**。
|
||
- 后果:传给子组件时,`React.memo` 发现 props.onClick 变了,导致子组件重渲染。
|
||
- 解决:`useCallback` 缓存函数引用,只有依赖变了才生成新函数。
|
||
- **useMemo**:缓存计算结果(类似 Vue `computed`),避免每次 Render 都进行昂贵计算。
|
||
|
||
**面试回答要点:**
|
||
- React 的优化是**手动挡**(开发者决定何时缓存),Vue 是**自动挡**(响应式系统自动处理)。
|
||
- 滥用 `useMemo` 也有开销,只在昂贵计算或引用稳定性关键时使用。
|
||
|
||
---
|
||
|
||
## 6. Diff 算法深度解析
|
||
|
||
### Q8: React Diff 算法与 Vue Diff 算法的区别?
|
||
|
||
**Vue 3 视角解析:**
|
||
Vue 2 使用双端 Diff,Vue 3 使用**最长递增子序列 (LIS)** 算法处理乱序移动,效率极高。
|
||
|
||
**React 原理:**
|
||
React Fiber 的 Diff 算法(Reconciliation)相对简单,采用**单向遍历**。
|
||
1. **仅右移**:React 在对比数组列表时,采用 `lastPlacedIndex` 指针。如果新节点在旧集合中存在且位置靠后,则不动;如果位置靠前,则向后移动。
|
||
2. **为什么不用双端?**:Fiber 结构是单向链表(Sibling 指针),很难像数组那样方便地从尾部开始对比(没有反向指针)。
|
||
3. **Key 的重要性**:和 Vue 一样,Key 是识别节点的唯一标识。没有 Key,React 只能按索引对比,导致状态错乱或性能低下。
|
||
|
||
**总结对比:**
|
||
- **Vue**:双端比较 / 最长递增子序列 -> 移动次数最少,算法复杂度稍高但 DOM 操作最少。
|
||
- **React**:单向遍历 / 右移策略 -> 算法简单,但在特定逆序场景下 DOM 移动次数可能多于 Vue。
|
||
|
||
---
|
||
|
||
## 7. TypeScript 实战差异
|
||
|
||
### Q9: React.FC 还需要用吗?Hooks 怎么定义泛型?
|
||
|
||
**Vue 3 视角解析:**
|
||
Vue `defineComponent` 或 `<script setup lang="ts">` 自动推导类型,Props 定义非常清晰。
|
||
|
||
**React 最佳实践:**
|
||
1. **React.FC (FunctionComponent)**:
|
||
- **不推荐**(React 18 以前包含隐式 `children`,容易导致类型不严谨;React 18 后移除了,但手动写 Props 类型更纯粹)。
|
||
- **推荐写法**:直接定义 Props 接口。
|
||
```typescript
|
||
interface Props {
|
||
title: string;
|
||
children?: React.ReactNode; // 显式声明 children
|
||
}
|
||
const MyComp = ({ title, children }: Props) => { ... }
|
||
```
|
||
|
||
2. **Hooks 泛型**:
|
||
```typescript
|
||
// useState
|
||
const [user, setUser] = useState<User | null>(null);
|
||
|
||
// useRef (DOM)
|
||
const inputRef = useRef<HTMLInputElement>(null);
|
||
|
||
// forwardRef (难点)
|
||
const MyInput = forwardRef<HTMLInputElement, Props>((props, ref) => { ... });
|
||
```
|
||
|
||
---
|
||
|
||
## 总结:给 Vue 开发者的 React 学习建议
|
||
|
||
1. **忘掉"自动"**:习惯手动管理依赖(useEffect deps)、手动优化性能(memo/useCallback)。
|
||
2. **拥抱"函数式"**:理解闭包、纯函数、不可变数据。
|
||
3. **深入 Fiber**:这是 React 区别于其他框架最核心的底层技术,也是高级面试必问。
|