React 面试

107 阅读14分钟

# 高频前端面试题汇总之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 falseevent.preventDefault()必须显式调用 event.preventDefault()
this指向指向当前DOM元素默认不为组件实例,需要手动绑定
底层机制直接绑定事件委托到根容器
浏览器兼容性需开发者自己处理React已处理,兼容性好
为什么React要这样做?(设计思想)
  1. 跨浏览器一致性:提供统一的API,让开发者无需编写兼容不同浏览器的代码。
  2. 性能优化:通过事件委托,减少了内存开销,避免了频繁绑定/解绑事件。
  3. 框架生态整合:合成事件是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 事件代理的核心价值:

  1. 自动代理:React 框架级别自动实现高效的事件代理
  2. 性能优化:减少内存占用,避免大量事件绑定
  3. 动态适应:自动处理动态添加/删除的元素
  4. 简化代码:统一的事件处理逻辑

4.React 高阶组件、Render Props、Hooks 区别与演进

一、核心区别对比
特性高阶组件 (HOC)Render PropsHooks
实现方式函数包装组件,返回新组件组件通过函数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 解决的三大核心问题:
  1. 主线程阻塞:将渲染任务拆分成可中断的小任务
  2. 优先级调度:不同更新有不同的优先级(用户输入 > 动画 > 数据更新)
  3. 并发渲染:支持暂停、继续、中止渲染任务
Fiber 的两大阶段:
  • 协调阶段:可中断,在内存中计算变更
  • 提交阶段:不可中断,一次性更新DOM
带来的新特性:
  • Suspense(代码分割、数据获取)
  • useTransition(并发更新)
  • 更好的错误边界处理

一句话总结:"Fiber 重构了 React 的协调算法,通过可中断的增量渲染和优先级调度,解决了大型应用渲染时的卡顿问题,为并发特性奠定了基础。"

6.React.Component 和 React.PureComponent 的区别

核心区别
特性React.ComponentReact.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 的区别与联系

核心区别
概念定义特点创建方式
ComponentUI 的蓝图/模板可复用的代码单元class MyComponentfunction MyComponent
ElementUI 的描述对象轻量、不可变、纯对象<Component prop="value">React.createElement()
Instance组件的运行实例有状态、生命周期、DOM 引用React 内部自动创建和管理

8.React.createClass 与 extends Component 的区别

核心区别对比
特性React.createClassextends 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