react高频面试题

433 阅读10分钟

React 面试高频题可以从 核心概念、Hooks、性能优化、状态管理、路由、工程化 等方面展开,以下是精选的高频题目及详细解析,覆盖面试中最常考察的知识点:

一、核心概念

1. React 的核心特点是什么?

  • 组件化:将 UI 拆分为独立、可复用的组件,降低复杂度。
  • 声明式编程:只描述 UI 应该是什么样子,而非如何更新 DOM,减少命令式操作。
  • 虚拟 DOM:通过内存中的虚拟 DOM 树缓存 UI 状态,对比新旧虚拟 DOM 差异,批量更新真实 DOM,提升性能。
  • 单向数据流:数据从父组件向下传递,子组件通过回调函数通知父组件修改数据,避免数据混乱。
  • JSX 语法:将 HTML 与 JavaScript 结合,直观描述 UI 结构,编译后转为 React.createElement 调用。

2. 虚拟 DOM 的工作原理?

  1. 生成虚拟 DOM:组件渲染时,React 将 JSX 转换为虚拟 DOM(一个 JavaScript 对象树,描述节点类型、属性、子节点)。
  2. 对比新旧虚拟 DOM:当状态变化时,生成新的虚拟 DOM,通过 Diff 算法 对比新旧树的差异(只对比同层节点,降低复杂度)。
  3. 批量更新真实 DOM:收集所有差异(“补丁”),一次性更新真实 DOM,减少 DOM 操作次数(DOM 操作是性能瓶颈)。

3. React 的生命周期(旧版 class 组件)?

分为三个阶段:

  • 挂载阶段:组件创建并插入 DOM。

    • constructor:初始化状态、绑定事件。
    • componentWillMount(已废弃):挂载前执行,不推荐操作 DOM。
    • render:返回 JSX,纯函数(无副作用)。
    • componentDidMount:挂载后执行,可操作 DOM、发起网络请求。
  • 更新阶段:组件状态 / 属性变化时触发。

    • componentWillReceiveProps(已废弃):接收新 props 时执行。
    • shouldComponentUpdate:判断是否需要重新渲染(返回 false 则跳过更新,优化性能)。
    • componentWillUpdate(已废弃):更新前执行。
    • render:重新渲染。
    • componentDidUpdate:更新后执行,可操作更新后的 DOM。
  • 卸载阶段:组件从 DOM 中移除。

    • componentWillUnmount:卸载前执行,清理定时器、事件监听、网络请求等副作用。

4. React 事件系统的特点?

  • 合成事件:React 封装了原生 DOM 事件,提供统一的事件对象(SyntheticEvent),跨浏览器兼容。

  • 事件委托:所有事件都委托到 document 节点(或根组件),减少 DOM 事件绑定次数,提升性能。

  • 异步批量处理:React 会批量处理事件回调中的状态更新(例如,多次 setState 合并为一次渲染)。

  • 与原生事件的区别

    • 合成事件的 this 需通过 bind 或箭头函数绑定(class 组件)。
    • 原生事件需手动移除监听,合成事件在组件卸载时自动清理。

二、Hooks 相关

1. 为什么需要 Hooks?

  • 解决 class 组件的问题

    • 避免复杂的生命周期(如 componentDidMount 中混杂网络请求和 DOM 操作)。
    • 解决 HOC 和 Render Props 导致的 “嵌套地狱”。
    • 无需绑定 this,简化代码。
  • 让函数组件拥有状态和副作用:函数组件原本是纯函数,Hooks 让其可以管理状态(useState)、执行副作用(useEffect)等。

2. 常用 Hooks 及其作用?

  • useState:管理组件状态,返回 [状态值, 更新函数]

    • 示例:const [count, setCount] = useState(0);
  • useEffect:处理副作用(网络请求、DOM 操作、定时器等),相当于 componentDidMountcomponentDidUpdatecomponentWillUnmount 的集合。

    • 依赖数组为空:仅在挂载时执行。
    • 依赖数组包含变量:变量变化时执行。
    • 无依赖数组:每次渲染后执行。
    • 返回清理函数:组件卸载或依赖变化前执行(清理副作用)。
  • useRef:创建一个可变的 ref 对象,用于存储 DOM 元素、定时器 ID 等,更新 ref 不会触发组件重新渲染。

    • 示例:const inputRef = useRef(null);(用于获取输入框 DOM 节点)。
  • useContext:获取 Context 中的数据,避免多层组件传递 props(“prop drilling”)。

  • useReducer:用于复杂状态管理(如状态依赖 previous state、多个子值),类似 Redux 的 reducer。

  • useMemo:缓存计算结果,避免每次渲染重复计算(依赖数组变化时才重新计算)。

  • useCallback:缓存函数引用,避免每次渲染创建新函数(用于传递给子组件,配合 React.memo 优化性能)。

3. Hooks 的使用规则?

  • 只能在函数组件或自定义 Hooks 的顶层调用(不能在条件语句、循环、嵌套函数中调用)。
  • 只能在 React 函数组件或自定义 Hooks 中使用(不能在普通 JavaScript 函数中使用)。
  • 遵循命名规范:自定义 Hooks 必须以 use 开头(如 useLocalStorage)。

4. useState 和 useReducer 的区别?

  • useState:适用于简单状态(如单个数字、字符串),更新函数是直接替换状态。

  • useReducer:适用于复杂状态(如对象、数组,或状态更新依赖 previous state),通过 dispatch 动作(action)触发状态更新,逻辑更清晰、可预测。

    • 示例:
      const reducer = (state, action) => {
        switch (action.type) {
          case 'INCREMENT': return { count: state.count + 1 };
          case 'DECREMENT': return { count: state.count - 1 };
          default: return state;
        }
      };
      const [state, dispatch] = useReducer(reducer, { count: 0 });
      dispatch({ type: 'INCREMENT' });
      

5. useEffect 的依赖数组?

  • 依赖数组是 useEffect 的第二个参数,用于控制副作用的执行时机。
  • 若依赖数组为空([]):副作用仅在组件挂载时执行一次,卸载时执行清理函数。
  • 若依赖数组包含变量(如 [count]):副作用在组件挂载时执行,且每次 count 变化时重新执行。
  • 若不传递依赖数组:副作用在每次组件渲染后(包括初始渲染和更新渲染)都执行。
  • 注意:依赖数组必须包含所有在 useEffect 中使用的外部变量(否则会导致闭包问题,使用旧的变量值)。

三、性能优化

1. React 性能优化的常用手段?

  • 减少不必要的渲染

    • 用 React.memo 包装纯函数组件(浅比较 props,避免父组件更新导致子组件不必要渲染)。
    • 用 useMemo 缓存计算结果,useCallback 缓存函数引用(配合 React.memo 使用)。
    • class 组件中重写 shouldComponentUpdate(返回 false 跳过更新)。
  • 虚拟列表:长列表场景下(如 thousands 条数据),使用 react-window 或 react-virtualized 只渲染可视区域的列表项,减少 DOM 节点数量。

  • 懒加载

    • 路由懒加载:React.lazy(() => import('./Page')) 配合 Suspense 实现按需加载。
    • 图片懒加载:使用 react-lazyload 或原生 IntersectionObserver API。
  • 避免直接修改状态:React 状态更新是不可变的(如修改数组 / 对象需创建新实例),否则 React 无法检测状态变化。

    • 错误示例:state.array.push(1);(直接修改原数组)。
    • 正确示例:setState(prev => [...prev.array, 1]);(创建新数组)。
  • 批量更新状态:React 会自动批量处理同一事件回调中的多个 setState,若需手动批量更新(如异步操作中),可使用 ReactDOM.unstable_batchedUpdates

  • 使用生产环境构建:通过 npm run build 生成优化后的生产包(移除开发环境的警告、调试代码)。

2. React.memo 的作用?

  • React.memo 是一个高阶组件(HOC),用于包装纯函数组件。

  • 作用:浅比较组件的 props,若 props 未变化,则跳过组件的重新渲染,提升性能。

  • 注意:

    • 仅比较 props,不比较 state(组件自身 state 变化仍会触发渲染)。

    • 浅比较:对于对象 / 数组,只比较引用是否相同,不深比较内部值(若需深比较,可传递第二个参数 areEqual 函数)。

    • 示例:

      const MyComponent = React.memo((props) => {
        // 组件逻辑
      }, (prevProps, nextProps) => {
        // 自定义比较逻辑,返回 true 表示 props 相同,不重新渲染
        return prevProps.count === nextProps.count;
      });
      

四、状态管理

1. React 状态管理方案对比(Context + useReducer vs Redux vs MobX)?

方案适用场景优点缺点
Context + useReducer中小型应用,状态共享范围小(如主题、用户信息)原生支持,无需第三方库,轻量不支持中间件(如日志、异步),状态复杂时冗余
Redux大型应用,状态复杂、共享范围广生态完善(中间件、DevTools),可预测性强配置繁琐,样板代码多
MobX中大型应用,追求开发效率响应式编程,自动追踪状态依赖,代码简洁灵活性高导致规范难统一,调试相对复杂

2. Redux 的核心概念和工作流程?

  • 核心概念

    • Store:存储应用状态的容器(唯一)。
    • State:Store 中存储的状态数据(只读)。
    • Action:描述状态变化的普通对象(必须有 type 字段,如 { type: 'INCREMENT' })。
    • Reducer:纯函数,接收 state 和 action,返回新的 state(state, action) => newState)。
    • Dispatch:触发状态更新的函数(store.dispatch(action))。
  • 工作流程

    1. 组件通过 store.dispatch(action) 发送一个动作。
    2. Redux 调用 Reducer,传入当前 state 和 action,计算新的 state
    3. Store 更新 state,并通知所有订阅了 Store 的组件。
    4. 组件重新渲染。

3. Redux 中间件的作用?

  • 中间件是 Redux 处理副作用(如异步请求、日志、崩溃监控)的扩展点,位于 dispatch(action) 和 reducer 之间。

  • 常用中间件:

    • redux-thunk:支持异步 Action(Action 可以是函数,接收 dispatch 和 getState)。
    • redux-saga:处理复杂异步逻辑(如并发请求、取消请求),基于 Generator 函数。
    • redux-logger:打印 Action 和状态变化日志,方便调试。

五、路由

1. React Router 的核心组件?

  • BrowserRouter:使用 HTML5 History API 实现路由(URL 无 #),需要后端配置支持(所有路由指向 index.html)。
  • HashRouter:使用 URL 的 # 后面的部分作为路由(锚点路由),无需后端配置。
  • Route:定义路由规则(path 路径,component 组件,exact 精确匹配)。
  • Switch:只渲染第一个匹配的 Route(避免多个路由同时匹配)。
  • Link:路由跳转组件(类似 <a> 标签,无页面刷新)。
  • Redirect:重定向路由(如未登录时重定向到登录页)。
  • useHistory/useNavigate:编程式导航(如登录后跳转首页)。

2. React Router 中 history 对象的常用方法?

  • push(path):跳转到指定路径(添加新历史记录)。
  • replace(path):替换当前路径(不添加新历史记录)。
  • go(n):前进 / 后退 n 步(如 go(-1) 后退一页)。
  • goBack():后退一页(等价于 go(-1))。
  • goForward():前进一页(等价于 go(1))。

六、工程化与实践

1. React 项目的目录结构?

常见目录结构(基于 create-react-app):

src/
├── assets/          # 静态资源(图片、样式文件)
├── components/      # 公共组件(可复用的UI组件)
│   ├── Button/
│   │   ├── index.js
│   │   └── Button.css
├── pages/           # 页面组件(路由对应的页面)
│   ├── Home/
│   ├── About/
├── hooks/           # 自定义Hooks
├── context/         # Context相关文件
├── redux/           # Redux相关文件(store、reducers、actions)
├── router/          # 路由配置
├── utils/           # 工具函数
├── services/        # API请求封装
├── App.js           # 根组件
└── index.js         # 入口文件

2. 如何处理 React 中的跨域问题?

  • 开发环境:使用 create-react-app 内置的代理(修改 package.json):

    "proxy": "http://localhost:3001" // 后端接口地址
    

    或使用 http-proxy-middleware 自定义代理规则。

  • 生产环境

    • 后端配置 CORS(允许跨域域名)。
    • 使用 Nginx 反向代理(将前端请求转发到后端接口)。

3. React 中如何实现组件通信?

  • 父传子:通过 props 传递数据 / 函数。
  • 子传父:父组件传递回调函数,子组件调用该函数传递数据。
  • 兄弟组件:通过父组件中转(子 1 传父,父传子 2),或使用 Context/Redux。
  • 跨层级组件:使用 Context + useContext,或 Redux/MobX 等状态管理库。
  • 非关系组件:使用事件总线(如 events 库),或状态管理库。

4. React 错误边界(Error Boundary)的作用?

  • 捕获子组件树中的 JavaScript 错误(渲染、生命周期、事件回调中的错误),并显示备用 UI(避免整个应用崩溃)。

  • 只能捕获子组件的错误,不能捕获自身、异步代码(如 setTimeout)、事件回调中的错误。

  • 实现方式(class 组件):

    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      static getDerivedStateFromError(error) {
        return { hasError: true }; // 更新状态,显示备用UI
      }
    
      componentDidCatch(error, errorInfo) {
        console.log(error, errorInfo); // 日志上报
      }
    
      render() {
        if (this.state.hasError) {
          return <h1>Something went wrong.</h1>; // 备用UI
        }
        return this.props.children;
      }
    }
    
  • 使用: <ErrorBoundary><ChildComponent /></ErrorBoundary>

总结

React 面试高频题主要围绕 核心原理(虚拟 DOM、生命周期)、Hooks 用法与规则、性能优化手段、状态管理方案、路由配置、组件通信 等方面。准备时需结合实际项目经验,理解每个知识点的底层逻辑和实际应用场景,避免死记硬背。