Jacky's blog
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)

Jack Yang

编程; 随笔
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)
  • web
  • web concept
  • javascript

  • css

  • vue

  • react

    • React 完整学习指南
    • React Hooks 完整指南
    • REACT NATIVE
    • REACT 实用代码片段
    • REACT 高频问题
      • 📖 目录
      • 🎯 基础概念
        • Q1: React 为什么要用 Virtual DOM?
        • Q2: React 的 Diff 算法是怎么工作的?
        • Q3: React 渲染流程分哪几个阶段?
      • 🎣 Hooks 相关
        • Q4: ref 为空(ref.current 为 null),为什么?
        • 1. 组件尚未挂载
        • 2. 元素未正确关联
        • 3. 条件渲染导致元素未渲染
        • 4. 自定义组件未转发 ref
        • Q5: React.memo 和 useMemo 有什么区别?
        • Q5.1: 什么是 props 浅比较?有什么陷阱?
        • 陷阱1:对象字面量导致不必要的渲染
        • 陷阱2:数组字面量的问题
        • 陷阱3:嵌套对象变化检测不到
      • ⚡ 性能优化
        • Q6: 什么情况下组件会重新渲染?
        • Q7: 如何优化 React 渲染性能?
        • 1. 减少不必要渲染
        • 2. 懒加载和代码分割
        • 3. 列表优化
        • 4. 拆分 state
        • 5. 批处理更新
        • Q8: 为什么列表渲染里不要用 index 作为 key?
        • Q9: 如何优化一个渲染 1 万条数据的列表?
        • 1. 虚拟列表(推荐)
        • 2. 分批渲染
        • 3. 使用 key 保证稳定复用
      • 🔧 渲染原理
        • Q10: React 18 并发渲染解决了什么问题?
        • 问题背景
        • 解决方案
        • Q11: 组件为什么会出现"白屏一闪"?怎么优化?
        • 原因分析
        • 优化方案
      • 📚 学习建议
        • 如何准备 React 面试?
        • 面试技巧
      • 🎯 总结
  • nextjs

  • module

  • web faq
  • web3

  • more

  • 《web》
  • react
Jacky
2024-12-01
目录

REACT 高频问题

# 📌 React 高频面试问答

适合人群:准备React面试的开发者、React学习者
问题类型:基础概念、性能优化、渲染原理、常见问题
标签:FAQ - 快速回顾高频问题

# 📖 目录

  • 基础概念
  • Hooks 相关
  • 渲染原理
  • 性能优化
  • 常见问题

# 🎯 基础概念

# Q1: React 为什么要用 Virtual DOM?

回答要点:

  1. 性能优化:真实 DOM 操作代价高,频繁修改会导致页面卡顿
  2. 抽象层:Virtual DOM 在内存中用 JS 对象表示 UI 结构
  3. 批量更新:更新时先计算 diff,再批量更新真实 DOM,减少重绘/回流
  4. 跨平台:提供统一的编程模型,可以渲染到不同平台

补充说明:

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

# Q2: React 的 Diff 算法是怎么工作的?

回答要点:

  1. 分层比较:只在同一层级比较节点,不跨层比较
  2. 类型判断:
    • 节点类型不同 → 直接替换整棵子树
    • 类型相同 → 对比 props,只更新差异部分
  3. 列表优化:通过 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

# Q3: React 渲染流程分哪几个阶段?

回答要点:

  1. Render 阶段(可中断)

    • 构建 Fiber 树
    • 计算新的 Virtual DOM
    • 执行 Diff 算法
    • 标记需要更新的节点
  2. 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. 元素未正确关联

// ❌ 错误:ref 没有传递
const inputRef = useRef(null);
return <input />;

// ✅ 正确:正确传递 ref
return <input ref={inputRef} />;
1
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

# 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

# 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

# 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

常见陷阱:

# 陷阱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:数组字面量的问题

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

# 陷阱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

自定义比较函数:

// 如果需要深比较或自定义逻辑,传入第二个参数
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

对比表:

特性 浅比较 深比较
比较层级 只比较第一层 递归比较所有层级
基本类型 ✅ 可以检测变化 ✅ 可以检测变化
对象/数组引用 ✅ 可以检测引用变化 ✅ 可以检测引用变化
对象/数组内容 ❌ 检测不到内容变化 ✅ 可以检测内容变化
性能 ⚡ 快速 🐌 较慢
React 默认 ✅ React.memo 默认使用 ❌ 需要自定义

最佳实践:

  • 使用 useMemo 缓存对象和数组 props
  • 使用 useCallback 缓存函数 props
  • 避免在 JSX 中使用对象/数组字面量
  • 更新嵌套对象时,创建新的引用
  • 需要深比较时,使用自定义比较函数

# ⚡ 性能优化

# Q6: 什么情况下组件会重新渲染?

回答要点:

  1. 自身 state 变化
  2. 父组件重新渲染,子组件默认会跟着渲染
  3. context 值改变,所有消费该 context 的组件都会重新渲染
  4. 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

# 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. 懒加载和代码分割

see: code-splitting (opens new window)

// 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

# 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

# 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

# 5. 批处理更新

// React 18 自动批处理
const handleClick = () => {
  setCount(c => c + 1);  // 不会立即渲染
  setFlag(f => !f);      // 不会立即渲染
  // 批量更新,只渲染一次
};
1
2
3
4
5
6

# Q8: 为什么列表渲染里不要用 index 作为 key?

回答要点:

当列表顺序变化时,使用 index 作为 key 会导致:

  1. 输入框内容错位
  2. 动画或状态异常
  3. 性能问题:无法正确复用节点

问题示例:

// ❌ 错误:使用 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

# 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. 分批渲染

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

# 3. 使用 key 保证稳定复用

// ✅ 确保 key 稳定且唯一
{items.map(item => (
  <div key={item.id}>{item.name}</div>
))}
1
2
3
4

# 🔧 渲染原理

# Q10: React 18 并发渲染解决了什么问题?

回答要点:

# 问题背景

  • React 17 及之前:渲染是同步、不可中断的
  • 大任务可能导致页面卡顿,用户交互延迟

# 解决方案

  1. Fiber 架构 + 并发模式

    • 可以打断低优先级渲染
    • 优先处理用户交互
    • 保证页面流畅
  2. 新 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

# Q11: 组件为什么会出现"白屏一闪"?怎么优化?

回答要点:

# 原因分析

  1. 首屏渲染慢:打包体积过大
  2. 异步数据未返回:组件等待数据加载
  3. 路由切换:组件卸载和挂载过程

# 优化方案

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. 骨架屏 + 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

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

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

# 📚 学习建议

# 如何准备 React 面试?

  1. 理解核心原理:Virtual DOM、Diff 算法、Fiber 架构
  2. 掌握常用 Hooks:useState、useEffect、useMemo、useCallback
  3. 熟悉性能优化:React.memo、代码分割、虚拟列表
  4. 了解最新特性:React 18 并发渲染、Suspense、startTransition
  5. 实践项目经验:能够结合实际项目讲解优化案例

# 面试技巧

  • 结构化回答:问题背景 → 解决方案 → 代码示例
  • 举一反三:从一个问题延伸到相关知识点
  • 实战经验:结合项目实际遇到的问题和解决方案
  • 保持更新:关注 React 官方博客和最新发展

# 🎯 总结

这份 FAQ 覆盖了 React 面试的核心考点:

  • ✅ 基础概念:Virtual DOM、Diff 算法、渲染流程
  • ✅ Hooks 使用:ref、memo、useMemo、useCallback
  • ✅ 性能优化:减少渲染、代码分割、列表优化
  • ✅ 渲染原理:并发渲染、白屏优化
  • ✅ 实战经验:完整的代码示例和解决方案

通过这些高频问题的准备,你将能够自信地应对 React 相关的技术面试!🚀

#FAQ#React#interview
上次更新: 2025/10/16, 22:10:39
REACT 实用代码片段
nextjs

← REACT 实用代码片段 nextjs→

最近更新
01
npx 使用指南
10-12
02
cursor
09-28
03
inspect
07-20
更多文章>
Theme by Vdoing | Copyright © 2019-2025 Jacky | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式