REACT 高频问题
# 📌 React 高频面试问答
适合人群:准备React面试的开发者、React学习者
问题类型:基础概念、性能优化、渲染原理、常见问题
标签:FAQ - 快速回顾高频问题
# 📖 目录
# 🎯 基础概念
# Q1: React 为什么要用 Virtual DOM?
回答要点:
- 性能优化:真实 DOM 操作代价高,频繁修改会导致页面卡顿
- 抽象层:Virtual DOM 在内存中用 JS 对象表示 UI 结构
- 批量更新:更新时先计算 diff,再批量更新真实 DOM,减少重绘/回流
- 跨平台:提供统一的编程模型,可以渲染到不同平台
补充说明:
React 并不是总比手写原生 DOM 快,但它在复杂场景下能减少开发复杂度并保证性能。
代码示例:
// Virtual DOM 概念示意
const vdom = {
type: 'div',
props: { className: 'container' },
children: [
{ type: 'h1', props: {}, children: ['Hello'] },
{ type: 'p', props: {}, children: ['World'] }
]
};
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# Q2: React 的 Diff 算法是怎么工作的?
回答要点:
- 分层比较:只在同一层级比较节点,不跨层比较
- 类型判断:
- 节点类型不同 → 直接替换整棵子树
- 类型相同 → 对比 props,只更新差异部分
- 列表优化:通过
key标识子元素,尽量复用旧节点
为什么 key 很重要?
- 保证元素的唯一标识(identity),避免错误复用
- 使用 index 作为 key 可能导致顺序变化时渲染异常
- 正确做法:使用业务数据的唯一 ID
代码示例:
// ❌ 错误:使用 index 作为 key
{items.map((item, index) => (
<div key={index}>{item.name}</div>
))}
// ✅ 正确:使用唯一 ID
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# Q3: React 渲染流程分哪几个阶段?
回答要点:
Render 阶段(可中断)
- 构建 Fiber 树
- 计算新的 Virtual DOM
- 执行 Diff 算法
- 标记需要更新的节点
Commit 阶段(同步执行)
- 把差异更新到真实 DOM
- 执行副作用(useEffect、ref 更新等)
- 触发生命周期方法
补充说明:
React Fiber 引入"可中断渲染",把大任务拆分为小单元,提升页面流畅度。
流程图:
graph LR
A[触发更新] --> B[Render阶段]
B --> C[构建Fiber树]
C --> D[Diff算法]
D --> E[Commit阶段]
E --> F[更新DOM]
F --> G[执行副作用]
# 🎣 Hooks 相关
# Q4: ref 为空(ref.current 为 null),为什么?
常见原因:
# 1. 组件尚未挂载
React 在组件挂载后才会为 ref.current 赋值。
const MyComponent = () => {
const inputRef = useRef(null);
// ❌ 错误:组件渲染时访问
console.log(inputRef.current); // null
useEffect(() => {
// ✅ 正确:组件挂载后访问
console.log(inputRef.current); // <input> 元素
}, []);
return <input ref={inputRef} />;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 2. 元素未正确关联
// ❌ 错误:ref 没有传递
const inputRef = useRef(null);
return <input />;
// ✅ 正确:正确传递 ref
return <input ref={inputRef} />;
1
2
3
4
5
6
2
3
4
5
6
# 3. 条件渲染导致元素未渲染
const MyComponent = () => {
const inputRef = useRef(null);
const [showInput, setShowInput] = useState(false);
useEffect(() => {
// 如果 showInput 为 false,这里会是 null
console.log(inputRef.current);
}, []);
return showInput && <input ref={inputRef} />;
};
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 4. 自定义组件未转发 ref
// ❌ 错误:函数组件不自动处理 ref
const MyInput = (props) => <input {...props} />;
// ✅ 正确:使用 forwardRef
const MyInput = forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
// 使用
const inputRef = useRef(null);
return <MyInput ref={inputRef} />;
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# Q5: React.memo 和 useMemo 有什么区别?
回答要点:
| 特性 | React.memo | useMemo | useCallback |
|---|---|---|---|
| 作用对象 | 组件 | 值 | 函数 |
| 缓存内容 | 组件渲染结果 | 计算结果 | 函数引用 |
| 使用场景 | 避免组件重复渲染 | 缓存昂贵计算 | 缓存事件处理函数 |
| 比较方式 | props 浅比较 | 依赖数组 | 依赖数组 |
代码示例:
// React.memo - 缓存组件
const ExpensiveComponent = memo(({ data }) => {
console.log('渲染 ExpensiveComponent');
return <div>{data}</div>;
});
// useMemo - 缓存值
const expensiveValue = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);
// useCallback - 缓存函数
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Q5.1: 什么是 props 浅比较?有什么陷阱?
回答要点:
浅比较是指只比较对象的第一层属性,不会递归比较嵌套对象的内部属性。
浅比较的工作原理:
// 浅比较的逻辑(简化版)
function shallowEqual(objA, objB) {
// 1. 检查是否是同一个引用(包括基本类型)
if (objA === objB) return true;
// 2. 检查是否都是对象
if (typeof objA !== 'object' || typeof objB !== 'object') {
return false;
}
// 3. 比较对象的键数量
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
// 4. 比较每个键的值(只比较第一层)
for (let key of keysA) {
if (objA[key] !== objB[key]) return false;
}
return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
常见陷阱:
# 陷阱1:对象字面量导致不必要的渲染
const MyComponent = memo(({ config }) => {
console.log('渲染了');
return <div>Theme: {config.theme}</div>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* ❌ 每次都创建新对象,浅比较失败,导致不必要的渲染 */}
<MyComponent config={{ theme: 'dark' }} />
</div>
);
}
// ✅ 解决方案
function ParentFixed() {
const [count, setCount] = useState(0);
// 使用 useMemo 缓存对象
const config = useMemo(() => ({ theme: 'dark' }), []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<MyComponent config={config} />
{/* 现在 count 变化时,组件不会重新渲染 */}
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 陷阱2:数组字面量的问题
const ListComponent = memo(({ items }) => {
console.log('ListComponent 渲染了');
return (
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
);
});
function Parent() {
const [count, setCount] = useState(0);
// ❌ 每次都创建新数组
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ListComponent items={['apple', 'banana']} />
</div>
);
}
// ✅ 解决方案
function ParentFixed() {
const [count, setCount] = useState(0);
const items = useMemo(() => ['apple', 'banana'], []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ListComponent items={items} />
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 陷阱3:嵌套对象变化检测不到
const UserCard = memo(({ user }) => {
return (
<div>
<p>Name: {user.name}</p>
<p>City: {user.address.city}</p>
</div>
);
});
function Parent() {
const [user, setUser] = useState({
name: 'John',
address: { city: 'NYC', street: '5th Ave' }
});
const updateCity = () => {
// ❌ 直接修改嵌套对象,引用没变,浅比较认为 props 没变
user.address.city = 'LA';
setUser(user); // UserCard 不会重新渲染!
};
const updateCityCorrect = () => {
// ✅ 创建新的对象引用
setUser({
...user,
address: { ...user.address, city: 'LA' }
});
// UserCard 会重新渲染
};
return (
<div>
<button onClick={updateCityCorrect}>Update City</button>
<UserCard user={user} />
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
自定义比较函数:
// 如果需要深比较或自定义逻辑,传入第二个参数
const UserCard = memo(
({ user }) => {
return (
<div>
<p>Name: {user.name}</p>
<p>City: {user.address.city}</p>
</div>
);
},
// 自定义比较函数
(prevProps, nextProps) => {
// 返回 true 表示相等,不需要重新渲染
// 返回 false 表示不同,需要重新渲染
return (
prevProps.user.name === nextProps.user.name &&
prevProps.user.address.city === nextProps.user.address.city
);
}
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
对比表:
| 特性 | 浅比较 | 深比较 |
|---|---|---|
| 比较层级 | 只比较第一层 | 递归比较所有层级 |
| 基本类型 | ✅ 可以检测变化 | ✅ 可以检测变化 |
| 对象/数组引用 | ✅ 可以检测引用变化 | ✅ 可以检测引用变化 |
| 对象/数组内容 | ❌ 检测不到内容变化 | ✅ 可以检测内容变化 |
| 性能 | ⚡ 快速 | 🐌 较慢 |
| React 默认 | ✅ React.memo 默认使用 | ❌ 需要自定义 |
最佳实践:
- 使用
useMemo缓存对象和数组 props - 使用
useCallback缓存函数 props - 避免在 JSX 中使用对象/数组字面量
- 更新嵌套对象时,创建新的引用
- 需要深比较时,使用自定义比较函数
# ⚡ 性能优化
# Q6: 什么情况下组件会重新渲染?
回答要点:
- 自身 state 变化
- 父组件重新渲染,子组件默认会跟着渲染
- context 值改变,所有消费该 context 的组件都会重新渲染
- props 引用变化,即使值相同但引用不同也会触发渲染
避免方式:
// 1. React.memo - 阻止父组件渲染影响
const ChildComponent = memo(({ data }) => {
return <div>{data}</div>;
});
// 2. useMemo - 避免 props 引用变化
const memoizedData = useMemo(() => ({ id: 1, name: 'test' }), []);
// 3. useCallback - 避免函数引用变化
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
// 4. 拆分组件 - 减少渲染范围
const ParentComponent = () => {
const [count, setCount] = useState(0);
return (
<>
<Counter count={count} onChange={setCount} />
<ExpensiveComponent /> {/* 不受 count 影响 */}
</>
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Q7: 如何优化 React 渲染性能?
回答要点:
# 1. 减少不必要渲染
// React.memo
const MemoizedComponent = memo(MyComponent);
// useMemo
const sortedList = useMemo(() => {
return list.sort((a, b) => a.value - b.value);
}, [list]);
// useCallback
const handleSubmit = useCallback(() => {
// 处理提交
}, [dependencies]);
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 2. 懒加载和代码分割
// React.lazy + Suspense
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 3. 列表优化
// 虚拟滚动
import { FixedSizeList } from 'react-window';
const VirtualList = ({ items }) => (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 4. 拆分 state
// ❌ 不好:大对象导致整个组件重新渲染
const [state, setState] = useState({
user: {},
posts: [],
comments: []
});
// ✅ 好:拆分状态,按需更新
const [user, setUser] = useState({});
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 5. 批处理更新
// React 18 自动批处理
const handleClick = () => {
setCount(c => c + 1); // 不会立即渲染
setFlag(f => !f); // 不会立即渲染
// 批量更新,只渲染一次
};
1
2
3
4
5
6
2
3
4
5
6
# Q8: 为什么列表渲染里不要用 index 作为 key?
回答要点:
当列表顺序变化时,使用 index 作为 key 会导致:
- 输入框内容错位
- 动画或状态异常
- 性能问题:无法正确复用节点
问题示例:
// ❌ 错误:使用 index
const BadList = () => {
const [items, setItems] = useState(['A', 'B', 'C']);
const handleSort = () => {
setItems(['C', 'B', 'A']); // 排序后
};
return (
<>
{items.map((item, index) => (
<input key={index} defaultValue={item} />
))}
{/* 排序后输入框内容会错位 */}
</>
);
};
// ✅ 正确:使用唯一 ID
const GoodList = () => {
const [items, setItems] = useState([
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' },
{ id: 'c', name: 'C' }
]);
return (
<>
{items.map(item => (
<input key={item.id} defaultValue={item.name} />
))}
</>
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Q9: 如何优化一个渲染 1 万条数据的列表?
回答要点:
# 1. 虚拟列表(推荐)
import { FixedSizeList } from 'react-window';
const VirtualizedList = ({ items }) => (
<FixedSizeList
height={600}
itemCount={10000}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
Item {items[index].name}
</div>
)}
</FixedSizeList>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2. 分批渲染
const BatchRenderList = ({ items }) => {
const [displayCount, setDisplayCount] = useState(50);
useEffect(() => {
if (displayCount < items.length) {
const timer = setTimeout(() => {
setDisplayCount(count => Math.min(count + 50, items.length));
}, 0);
return () => clearTimeout(timer);
}
}, [displayCount, items.length]);
return (
<div>
{items.slice(0, displayCount).map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 3. 使用 key 保证稳定复用
// ✅ 确保 key 稳定且唯一
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
1
2
3
4
2
3
4
# 🔧 渲染原理
# Q10: React 18 并发渲染解决了什么问题?
回答要点:
# 问题背景
- React 17 及之前:渲染是同步、不可中断的
- 大任务可能导致页面卡顿,用户交互延迟
# 解决方案
Fiber 架构 + 并发模式
- 可以打断低优先级渲染
- 优先处理用户交互
- 保证页面流畅
新 API
import { useTransition, startTransition } from 'react';
// useTransition - 标记低优先级更新
const [isPending, startTransition] = useTransition();
const handleSearch = (text) => {
// 高优先级:立即更新输入框
setInputValue(text);
// 低优先级:可延迟的搜索结果
startTransition(() => {
setSearchResults(search(text));
});
};
// startTransition - 包装低优先级状态更新
startTransition(() => {
setTab('posts');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Q11: 组件为什么会出现"白屏一闪"?怎么优化?
回答要点:
# 原因分析
- 首屏渲染慢:打包体积过大
- 异步数据未返回:组件等待数据加载
- 路由切换:组件卸载和挂载过程
# 优化方案
1. 代码分割(按需加载)
// 路由级别代码分割
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
2. 骨架屏 + Suspense
const SkeletonScreen = () => (
<div className="skeleton">
<div className="skeleton-header" />
<div className="skeleton-content" />
</div>
);
function App() {
return (
<Suspense fallback={<SkeletonScreen />}>
<DataFetchingComponent />
</Suspense>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
3. SSR(服务端渲染)或 SSG(静态生成)
// Next.js SSR
export async function getServerSideProps() {
const data = await fetchData();
return { props: { data } };
}
// Next.js SSG
export async function getStaticProps() {
const data = await fetchData();
return { props: { data } };
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
4. 预加载关键资源
// 预加载组件
const preloadComponent = () => {
const component = lazy(() => import('./HeavyComponent'));
// 触发预加载
component.preload?.();
};
// 使用 <link rel="preload">
<link rel="preload" href="/api/data" as="fetch" />
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 📚 学习建议
# 如何准备 React 面试?
- 理解核心原理:Virtual DOM、Diff 算法、Fiber 架构
- 掌握常用 Hooks:useState、useEffect、useMemo、useCallback
- 熟悉性能优化:React.memo、代码分割、虚拟列表
- 了解最新特性:React 18 并发渲染、Suspense、startTransition
- 实践项目经验:能够结合实际项目讲解优化案例
# 面试技巧
- 结构化回答:问题背景 → 解决方案 → 代码示例
- 举一反三:从一个问题延伸到相关知识点
- 实战经验:结合项目实际遇到的问题和解决方案
- 保持更新:关注 React 官方博客和最新发展
# 🎯 总结
这份 FAQ 覆盖了 React 面试的核心考点:
- ✅ 基础概念:Virtual DOM、Diff 算法、渲染流程
- ✅ Hooks 使用:ref、memo、useMemo、useCallback
- ✅ 性能优化:减少渲染、代码分割、列表优化
- ✅ 渲染原理:并发渲染、白屏优化
- ✅ 实战经验:完整的代码示例和解决方案
通过这些高频问题的准备,你将能够自信地应对 React 相关的技术面试!🚀
上次更新: 2025/10/16, 22:10:39