# 高频前端面试题汇总之React篇
一、组件基础
1. React 事件机制
React并不是将click事件绑定到了div的真实DOM上,而是在document处监听了所有的事件,当事件发生并且冒泡到document处的时候,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂载销毁时统一订阅和移除事件。
除此之外,冒泡到document上的事件也不是原生的浏览器事件,而是由react自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用event.preventDefault()方法,而不是调用event.stopProppagation()方法。
<div onClick={this.handleClick.bind(this)}>点我</div>
2. React的事件和普通的HTML事件有什么不同?
核心答案
React事件是合成事件,它并不是原生的事件,而是React封装的一套跨浏览器兼容的事件系统。它将所有不同浏览器的事件行为封装成统一的API,让开发者无需关心浏览器差异。
1. 事件绑定方式不同
-
普通HTML事件:使用小写字母,以字符串形式内联绑定。
<button onclick="handleClick()">点击我</button> -
React事件:使用驼峰命名法,以JSX表达式形式传递函数本身(而非字符串)。
<button onClick={handleClick}>点击我</button> // 注意是 onClick={handleClick},而不是 onClick={handleClick()}
2. 事件对象的不同
- 普通HTML事件:接收的是浏览器原生的 **
Event** 对象。在不同浏览器中可能存在差异。 - React事件:接收的是React包装过的
SyntheticEvent(合成事件)对象。这个对象与原生事件具有相同的接口(如event.preventDefault(),event.stopPropagation()),但它是跨浏览器一致的,屏蔽了底层浏览器的差异。
3. 默认行为阻止方式不同
-
普通HTML事件:可以通过返回
false来阻止默认行为。<a href="https://example.com" onclick="console.log('点击了'); return false">点击</a> -
React事件:必须显式地调用
event.preventDefault()。返回false无效。function handleClick(event) { event.preventDefault(); // 必须显式调用 console.log('链接被点击了,但不会跳转'); } return <a href="https://example.com" onClick={handleClick}>点击我</a>;
4. 事件处理函数中的 this指向
- 普通HTML事件:函数内的
this默认指向触发事件的DOM元素。 - React事件:在类组件中,如果不进行绑定,事件处理函数中的
this默认是undefined(在严格模式下)或全局对象(非严格模式),不会自动指向组件实例。因此需要手动绑定(如使用箭头函数或在构造函数中绑定)。
5. 事件委托(事件池)机制
-
普通HTML事件:事件直接绑定在对应的DOM元素上。
-
React事件:React并不会将事件处理函数直接绑定到每一个具体的DOM节点上。而是采用事件委托机制,在组件挂载时,React会在根节点(v17之前是document,v17及之后是React渲染的根容器) 上为每种事件类型只添加一个监听器。当事件在子元素上触发时,它会通过事件冒泡到根节点,然后React根据内部映射关系找到对应的组件和事件处理函数进行处理。
- 优点:大幅节省内存,特别是动态创建大量元素时。
- 注意(事件池) :在React 16及之前的版本中,
SyntheticEvent对象会被放入一个“事件池”中重用,这意味着在异步代码中无法再访问事件对象。如果需要异步访问,必须调用event.persist()。在React 17+中,这个特性已被移除,无需再担心此问题。
总结对比表格
| 特性 | 普通HTML事件 | React事件 |
|---|---|---|
| 命名 | 小写(onclick) | 驼峰(onClick) |
| 绑定值 | 字符串代码 | 函数引用 |
| 事件对象 | 原生 Event对象 | 合成 SyntheticEvent对象 |
| 阻止默认行为 | return false或 event.preventDefault() | 必须显式调用 event.preventDefault() |
this指向 | 指向当前DOM元素 | 默认不为组件实例,需要手动绑定 |
| 底层机制 | 直接绑定 | 事件委托到根容器 |
| 浏览器兼容性 | 需开发者自己处理 | React已处理,兼容性好 |
为什么React要这样做?(设计思想)
- 跨浏览器一致性:提供统一的API,让开发者无需编写兼容不同浏览器的代码。
- 性能优化:通过事件委托,减少了内存开销,避免了频繁绑定/解绑事件。
- 框架生态整合:合成事件是React框架的一部分,可以与React的组件生命周期、虚拟DOM和状态更新机制无缝协作。
3.React 中的事件代理:原理与实践
1. 什么是事件代理?
事件代理(Event Delegation)是一种将事件处理程序绑定到父元素而不是每个子元素的技术。当子元素触发事件时,事件会冒泡到父元素,由父元素统一处理。
2. React 中的事件代理机制
React 的自动事件代理
React 默认就使用了事件代理机制,而且是框架级别的自动代理:
// 开发者编写的代码
function List() {
const handleClick = (item) => {
console.log('点击了:', item);
};
return (
<ul>
<li onClick={() => handleClick(1)}>项目 1</li>
<li onClick={() => handleClick(2)}>项目 2</li>
<li onClick={() => handleClick(3)}>项目 3</li>
</ul>
);
}
// React 底层实现的事件代理:
// 1. 不会在每个 <li> 上绑定原生 click 事件
// 2. 只在根容器上绑定一个 click 事件监听器
// 3. 通过事件冒泡捕获所有子元素的点击
React 17+ 的事件代理架构
// React 应用结构
const root = ReactDOM.createRoot(document.getElementById('root'));
// React 内部的事件代理实现:
// 在根容器上绑定少量事件监听器
root.addEventListener('click', reactClickHandler);
root.addEventListener('change', reactChangeHandler);
// ... 其他事件类型
function reactClickHandler(nativeEvent) {
// 1. 找到实际触发事件的DOM元素
const targetElement = nativeEvent.target;
// 2. 通过Fiber树找到对应的React组件
const targetFiber = findClosestFiber(targetElement);
// 3. 创建合成事件
const syntheticEvent = createSyntheticEvent(nativeEvent);
// 4. 调用对应的事件处理函数
dispatchEvent(targetFiber, 'onClick', syntheticEvent);
}
总结
React 事件代理的核心价值:
- 自动代理:React 框架级别自动实现高效的事件代理
- 性能优化:减少内存占用,避免大量事件绑定
- 动态适应:自动处理动态添加/删除的元素
- 简化代码:统一的事件处理逻辑
4.React 高阶组件、Render Props、Hooks 区别与演进
一、核心区别对比
| 特性 | 高阶组件 (HOC) | Render Props | Hooks |
|---|---|---|---|
| 实现方式 | 函数包装组件,返回新组件 | 组件通过函数prop渲染内容 | 函数组件内直接使用状态和逻辑 |
| 代码结构 | 容易产生"包装地狱" | 容易产生"回调地狱" | 扁平化,逻辑聚合 |
| 复用粒度 | 组件级别复用 | 渲染逻辑复用 | 状态逻辑复用 |
| TypeScript | 类型定义复杂 | 类型定义中等 | 类型推断优秀 |
| 学习成本 | 中等 | 中等 | 相对较低 |
二、代码示例对比
1. 高阶组件 (HOC) - 逻辑复用通过组件包装
// 容易产生包装地狱
const EnhancedComponent = withRouter(
withTheme(
withAuth(Component)
)
);
2. Render Props - 逻辑复用通过函数传递
// 容易产生回调地狱
<DataProvider>
{data => (
<ThemeProvider>
{theme => (
<UserProvider>
{user => <Component data={data} theme={theme} user={user} />}
</UserProvider>
)}
</ThemeProvider>
)}
</DataProvider>
3. Hooks - 逻辑复用直接调用
// 逻辑扁平化聚合
function Component() {
const data = useData();
const theme = useTheme();
const user = useUser();
// 相关逻辑集中管理
}
三、为什么要不断迭代?
1. 解决"嵌套地狱"问题
- HOC/Render Props:深度嵌套导致调试困难
- Hooks:扁平化结构,组件树更清晰
2. 更好的逻辑组织
// ❌ 类组件:逻辑分散在不同生命周期
class Component extends React.Component {
componentDidMount() {
// 数据获取 + 事件监听 + 定时器混杂
}
componentWillUnmount() {
// 清理逻辑混杂
}
}
// ✅ Hooks:按功能聚合逻辑
function Component() {
useDataFetching(); // 数据获取逻辑
useEventListener(); // 事件监听逻辑
useInterval(); // 定时器逻辑
}
3. 更好的 TypeScript 支持
- HOC:泛型类型定义复杂
- Hooks:自动类型推断,开发体验更好
4. 函数式编程导向
- 更符合 React 函数式理念
- 减少类组件的 this 指向问题
- 代码更简洁、可预测
四、面试一句话总结
"React 从 HOC 到 Render Props 再到 Hooks 的演进,核心是为了解决逻辑复用时的嵌套复杂性,让代码更扁平化、可维护,同时更好地支持 TypeScript 和函数式编程范式。" 演进脉络:组件包装 → 渲染委托 → 逻辑直接复用
5.React Fiber 架构:核心理解与解决的问题
一、Fiber 是什么?
React Fiber 是 React 16+ 的新的协调算法(reconciliation algorithm) ,它重新实现了 React 的虚拟 DOM 差异比较机制。
核心定位
Fiber 是一个用于增量渲染的重新实现的核心算法,支持将渲染工作分割成多个块,并分布到多个帧中。
二、Fiber 要解决的核心问题
1. 解决同步渲染导致的"卡顿"问题
// React 15 的同步渲染(Stack Reconciler)
function syncRender() {
// 如果组件树很大,JS会长时间占用主线程
// ⚠️ 用户交互、动画会卡住,直到整个树渲染完成
renderEntireTree(); // 不可中断
}
2. 支持优先级调度
// Fiber 支持不同优先级的更新
const priorities = {
Immediate: 1, // 用户输入、动画
UserBlocking: 2, // 用户交互反馈
Normal: 3, // 普通数据更新
Low: 4, // 复杂计算
Idle: 5 // 非关键任务
};
三、Fiber 的核心机制
1. 可中断的渲染过程
// Fiber 将渲染分解为多个工作单元
function workLoop() {
while (nextUnitOfWork && !shouldYield()) {
// 可以随时中断,让出主线程
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork && pendingCommit) {
commitAllWork(pendingCommit); // 提交阶段是同步的
}
requestIdleCallback(workLoop); // 利用浏览器的空闲时间
}
2. 双缓冲技术(Double Buffering)
// Fiber 维护两棵树:current 和 workInProgress
let currentTree = createFiberTree(); // 当前显示的树
let workInProgressTree = null; // 正在构建的新树
function updateComponent() {
// 在内存中构建新树,不阻塞当前UI
workInProgressTree = beginWork(currentTree);
// 新树构建完成后,一次性切换
if (workInProgressTree.completed) {
commitWork(workInProgressTree); // 提交到DOM
currentTree = workInProgressTree; // 切换指针
}
}
四、Fiber 的数据结构
Fiber 节点的核心属性
class FiberNode {
constructor(tag) {
this.tag = tag; // 组件类型(函数/类组件等)
this.key = null; // 唯一标识
this.type = null; // 组件构造函数
// 树结构关系
this.return = null; // 父Fiber
this.child = null; // 第一个子Fiber
this.sibling = null; // 下一个兄弟Fiber
// 状态相关
this.pendingProps = null; // 新props
this.memoizedProps = null;// 旧props
this.memoizedState = null;// 当前状态
this.updateQueue = null; // 状态更新队列
// 副作用标记
this.effectTag = null; // 需要执行的操作
this.nextEffect = null; // 下一个有副作用的Fiber
// 工作进度控制
this.alternate = null; // 指向另一棵树的对应Fiber
}
}
五、Fiber 的工作流程(两阶段提交)
阶段1:渲染/协调阶段(可中断)
// 此阶段在内存中进行,可随时中断
function renderPhase() {
// 遍历Fiber树,找出需要更新的组件
while (hasWork && !shouldYieldToBrowser()) {
const fiber = getNextFiber();
// 对比新旧虚拟DOM,标记变化
const changes = reconcileChildren(fiber);
// 如果时间片用尽,保存进度,下次继续
if (timeRemaining() <= 0) {
break; // 让出主线程给更高优先级任务
}
}
}
阶段2:提交阶段(不可中断)
// 此阶段同步执行,一次性更新DOM
function commitPhase() {
// 应用所有变更到真实DOM
commitAllWork(finishedWork);
// 此阶段必须同步完成,避免UI不一致
// 不可中断,确保DOM状态一致性
}
六、Fiber 带来的核心能力
1. 并发特性(Concurrent Features)
// Suspense:支持延迟加载
function App() {
return (
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
);
}
// useTransition:区分紧急和非紧急更新
function SearchBox() {
const [isPending, startTransition] = useTransition();
const handleChange = (value) => {
startTransition(() => {
// 非紧急更新:可以中断
setQuery(value);
});
};
return (
<div className={isPending ? 'pending' : ''}>
<input onChange={e => handleChange(e.target.value)} />
</div>
);
}
2. 优先级调度示例
function InteractiveComponent() {
const [urgentData, setUrgentData] = useState(null);
const [normalData, setNormalData] = useState(null);
// 用户输入:最高优先级
const handleUserInput = (value) => {
// 同步执行,立即响应
setUrgentData(value);
};
// 数据加载:普通优先级
const loadData = async () => {
// 可中断的更新
const data = await fetchData();
setNormalData(data);
};
}
七、实际解决的问题对比
React 15(Stack Reconciler)的问题
// 问题场景:大型列表更新
function updateLargeList() {
// ❌ 同步阻塞:更新1000个项会卡住界面
for (let i = 0; i < 1000; i++) {
updateListItem(i); // 长时间占用主线程
}
// 期间用户点击、动画都会卡顿
}
React 16+(Fiber Reconciler)的解决方案
// Fiber 的增量渲染
function updateLargeListWithFiber() {
// ✅ 可中断:每处理几个项目就检查时间
for (let i = 0; i < 1000; i++) {
updateListItem(i);
// 检查时间片是否用尽
if (shouldYield()) {
// 让出主线程,处理用户交互
requestIdleCallback(resumeUpdate);
break;
}
}
}
八、面试要点总结
Fiber 解决的三大核心问题:
- 主线程阻塞:将渲染任务拆分成可中断的小任务
- 优先级调度:不同更新有不同的优先级(用户输入 > 动画 > 数据更新)
- 并发渲染:支持暂停、继续、中止渲染任务
Fiber 的两大阶段:
- 协调阶段:可中断,在内存中计算变更
- 提交阶段:不可中断,一次性更新DOM
带来的新特性:
- Suspense(代码分割、数据获取)
- useTransition(并发更新)
- 更好的错误边界处理
一句话总结:"Fiber 重构了 React 的协调算法,通过可中断的增量渲染和优先级调度,解决了大型应用渲染时的卡顿问题,为并发特性奠定了基础。"
6.React.Component 和 React.PureComponent 的区别
核心区别
| 特性 | React.Component | React.PureComponent |
|---|---|---|
| 更新机制 | 默认每次都会重新渲染 | 自动进行浅比较优化 |
| shouldComponentUpdate | 需要手动实现 | 自动实现浅比较 |
| 性能 | 可能不必要的重新渲染 | 自动避免不必要的渲染 |
| 使用场景 | 需要精细控制更新逻辑 | Props/State 结构相对简单 |
关键差异点
1. 更新逻辑不同
// Component - 总是重新渲染
class MyComponent extends React.Component {
// 没有默认的 shouldComponentUpdate
// 每次 setState() 或父组件更新都会重新渲染
}
// PureComponent - 智能比较
class MyPureComponent extends React.PureComponent {
// 自动实现 shouldComponentUpdate
// 对 props 和 state 进行浅比较
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState);
}
}
2. 浅比较的局限性
class Example extends React.PureComponent {
state = { items: [] };
handleClick = () => {
// ❌ 错误:直接修改原数组,浅比较无法检测变化
this.state.items.push('new item');
this.setState({ items: this.state.items }); // 不会重新渲染
// ✅ 正确:创建新引用
this.setState({
items: [...this.state.items, 'new item']
}); // 会重新渲染
};
}
使用建议
使用 PureComponent 当:
- Props/State 结构相对简单
- 数据不可变,避免直接修改
- 需要性能优化但不想手动实现 shouldComponentUpdate
使用 Component 当:
- 需要精细控制更新逻辑
- 数据结构复杂,浅比较不够用
- 有特殊的渲染条件需求
一句话总结
"PureComponent 通过自动实现 shouldComponentUpdate 进行浅比较来优化性能,而 Component 需要手动控制更新逻辑,适用于更复杂的场景。" 记住关键点:PureComponent = Component + 自动浅比较优化
7.React 中 Component、Element、Instance 的区别与联系
核心区别
| 概念 | 定义 | 特点 | 创建方式 |
|---|---|---|---|
| Component | UI 的蓝图/模板 | 可复用的代码单元 | class MyComponent或 function MyComponent |
| Element | UI 的描述对象 | 轻量、不可变、纯对象 | <Component prop="value">或 React.createElement() |
| Instance | 组件的运行实例 | 有状态、生命周期、DOM 引用 | React 内部自动创建和管理 |
8.React.createClass 与 extends Component 的区别
核心区别对比
| 特性 | React.createClass | extends Component |
|---|---|---|
| 语法 | 传统方式,React 15 及之前 | ES6 class,React 16+ 推荐 |
| this 绑定 | 自动绑定方法中的 this | 需要手动绑定或使用箭头函数 |
| Mixins 支持 | 支持 | 不支持,被 HOC/Hooks 替代 |
| PropTypes | 内置在 createClass 中 | 需要额外导入 prop-types 包 |
| 性能 | 相对较慢 | 更优的性能和内存使用 |
9.React 高阶组件(HOC):核心概念与适用场景
一、高阶组件是什么?
高阶组件是一个函数,接收一个组件,返回一个新的增强组件。
// 基本结构
const EnhancedComponent = higherOrderComponent(WrappedComponent);
// 实际示例
const withAuth = (WrappedComponent) => {
return (props) => {
const isAuthenticated = checkAuth();
return isAuthenticated ? <WrappedComponent {...props} /> : <LoginPage />;
};
};
const PrivateComponent = withAuth(MyComponent);
二、与普通组件的核心区别
| 特性 | 普通组件 | 高阶组件 |
|---|---|---|
| 本质 | 渲染UI的React组件 | 增强组件逻辑的函数 |
| 输入 | Props | 一个React组件 |
| 输出 | React元素(JSX) | 新的增强组件 |
| 用途 | 界面展示 | 逻辑复用和功能增强 |
三、适用场景(什么时候用?)
1. 权限控制
// 需要登录才能访问的页面
const withAuth = (WrappedComponent) => {
return (props) => {
return isLoggedIn ? <WrappedComponent {...props} /> : <Redirect to="/login" />;
};
};
const AdminPage = withAuth(AdminComponent);
2. 数据获取
// 统一的数据加载逻辑
const withData = (url) => (WrappedComponent) => {
return class extends Component {
state = { data: null, loading: true };
async componentDidMount() {
const data = await fetch(url);
this.setState({ data, loading: false });
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
};
const UserList = withData('/api/users')(UserComponent);
3. 逻辑复用(多个组件共享相同逻辑)
// 多个组件都需要日志记录功能
const withLogger = (WrappedComponent) => {
return class extends Component {
componentDidMount() {
console.log(`Component ${WrappedComponent.name} mounted`);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
};
const ComponentA = withLogger(ComponentA);
const ComponentB = withLogger(ComponentB);
四、不适用场景(什么时候不用?)
1. 简单UI组件
// ❌ 过度设计 - 简单按钮不需要HOC
const withStyle = (WrappedComponent) => { /* ... */ };
const FancyButton = withStyle(Button);
// ✅ 直接使用props更简单
<Button style={fancyStyle} />
2. 现代React项目(优先考虑Hooks)
// ❌ 传统HOC方式
const withData = (url) => (WrappedComponent) => { /* ... */ };
// ✅ 现代Hooks方式
function useData(url) {
const [data, setData] = useState(null);
useEffect(() => { fetchData(); }, [url]);
return data;
}
function Component() {
const data = useData('/api/data');
return /* ... */;
}
"高阶组件是通过函数包装来增强组件逻辑的模式,适用于权限控制、数据获取等横切关注点,但现代React中多数场景已被Hooks替代。" 关键记忆点:
- HOC是函数,输入组件输出增强组件
- 解决逻辑复用问题
- 可能导致嵌套过深,现代项目优先考虑Hooks