React 面试必问:7 大核心考点深度拆解与避坑实录

9 阅读7分钟

1. useEffect 的三大场景与执行时机

useEffect 是处理副作用的核心 Hook,它的行为完全由依赖数组决定。

🟢 场景一:空依赖数组 []

  • 代码useEffect(() => { ... }, [])

  • 行为:仅在组件首次挂载(Mount) 后执行一次。

  • 类组件等价物componentDidMount

  • 典型用途

    • 初始化数据请求(API Call)。
    • 设置全局事件监听(如 window.resize)。
    • 初始化第三方库实例。
  • ⚠️ 陷阱:如果在依赖数组中漏写了内部使用的 state/prop,会导致闭包陷阱(读取到旧值)。

🟡 场景二:有依赖数组 [a, b]

  • 代码useEffect(() => { ... }, [a, b])

  • 行为

    1. 首次挂载时执行。
    2. 当且仅当 a 或 b 的值发生变化(浅比较 Object.is)时,再次执行。
  • 类组件等价物componentDidMount + componentDidUpdate (特定条件)

  • 典型用途

    • 根据 URL 参数 (id) 重新获取数据。
    • 当某个状态变化时,更新文档标题或发送埋点。
  • ⚠️ 陷阱

    • 无限循环:如果在 effect 内部修改了依赖项(如 setCount(count + 1) 且 count 在依赖中),会触发死循环。
    • 对象/数组依赖:如果依赖是对象 {} 或数组 [],每次渲染都会生成新引用,导致 effect 频繁执行。解决:使用 useMemo 包裹对象,或只依赖对象的原始属性。

🔴 场景三:返回清理函数 return () => { ... }

  • 代码

    useEffect(() => {
      // 设置副作用
      const timer = setInterval(...);
      
      // 返回清理函数
      return () => {
        clearInterval(timer);
      };
    }, []);
    
  • 行为

    1. 组件卸载前执行(类似 componentWillUnmount)。
    2. 下次 effect 执行前执行(如果依赖变了,先清理旧的,再执行新的)。
  • 典型用途

    • 清除定时器 (clearIntervalclearTimeout)。
    • 取消未完成的网络请求 (AbortController)。
    • 移除事件监听 (removeEventListener)。
    • 断开 WebSocket 连接。
    • 销毁第三方库实例。
  • 核心价值防止内存泄漏竞态条件(Race Conditions)。


2. useEffect 清理函数详解:为什么必须写?

很多初学者认为“组件没了浏览器会自动回收”,但在单页应用(SPA)中,逻辑上的卸载不等于物理资源的释放

核心作用拆解:

  1. 防止内存泄漏 (Memory Leaks)

    • 场景:组件 A 启动了一个定时器,用户快速切换路由导致 A 卸载。如果不清除定时器,它仍在后台运行,尝试更新一个已不存在的组件状态,导致报错 Can't perform a React state update on an unmounted component,且定时器占用内存。
    • 对策:在 return 中 clearInterval
  2. 避免竞态条件 (Race Conditions)

    • 场景:用户快速切换 ID(从 1 到 2)。

      • 请求 ID=1 发出(慢)。
      • 请求 ID=2 发出(快,先回来)。
      • 请求 ID=1 回来了(晚),覆盖了 ID=2 的数据。
    • 对策:在清理函数中取消上一个请求。

    useEffect(() => {
      let ignore = false; // 标记位
      fetchData(id).then(data => {
        if (!ignore) setData(data);
      });
      return () => { ignore = true; }; // 清理时标记为忽略
    }, [id]);
    // 或者使用 AbortController
    
  3. 解除外部订阅

    • 场景:订阅了 Redux Store、WebSocket 或 DOM 事件。如果不取消订阅,即使组件销毁,回调函数仍会被触发,可能导致对已销毁 DOM 的操作报错。

3. HOC (高阶组件) 深度解析

✅ 标准定义

HOC (Higher-Order Component)  是一个函数,它接受一个组件作为参数,并返回一个新的增强组件

  • 公式const EnhancedComponent = withSomething(WrappedComponent)
  • 本质:React 中复用组件逻辑的高级模式(在 Hooks 出现前是主流)。
  • 原则纯函数,不修改原组件,通过组合(Composition)创造新功能。

🛡️ 权限控制场景(标准实现)

这是 HOC 最经典的应用场景:权限守卫

// withAuth.js
import { Navigate } from 'react-router-dom';

export function withAuth(WrappedComponent, requiredRole) {
  return function AuthenticatedComponent(props) {
    const { user } = useAuth(); // 假设有一个获取用户信息的 Hook

    // 1. 未登录
    if (!user) {
      return <Navigate to="/login" replace />;
    }

    // 2. 权限不足
    if (requiredRole && user.role !== requiredRole) {
      return <Navigate to="/403" replace />;
    }

    // 3. 权限通过,渲染原组件
    return <WrappedComponent {...props} user={user} />;
  };
}

// 使用
const AdminPage = withAuth(Dashboard, 'admin');
  • 优势:业务页面组件不需要关心“怎么判断权限”,只需要关注“页面长什么样”。逻辑解耦。

⚔️ HOC vs 普通封装函数

特性HOC (高阶组件)普通工具函数
输入/输出输入组件 → 输出新组件输入数据 → 输出数据/结果
复用层级组件级复用(包含 UI、生命周期、状态)逻辑级复用(纯计算、格式化)
能力范围可以拦截渲染、注入 Props、控制生命周期、处理副作用只能处理传入的数据,无法触达 DOM 或生命周期
典型场景权限控制、日志埋点、数据注入、错误边界日期格式化、数学计算、字符串处理

面试加分点:提到 HOC 的缺点(嵌套地狱 Wrapper Hell、静态方法丢失、Ref 转发问题),并说明现在更推荐使用 Custom Hooks 来替代 HOC 进行逻辑复用,但 HOC 在控制渲染结构(如权限守卫、布局包裹)上仍有不可替代的价值。


4. React 组件更新机制(触发重渲染的条件)

React 的更新是自顶向下的。以下三种情况会触发组件重新渲染(Re-render):

  1. State 变化:调用 setState 或 Hook 的 setter (如 setCount)。

  2. Props 变化:父组件重新渲染,传递给子组件的 Props 发生了改变(引用不同或值不同)。

  3. 父组件渲染默认情况下,只要父组件渲染,无论子组件的 Props 是否变化,子组件也会跟着重新渲染

    • 这是性能优化的重点:如果子组件渲染开销大,需要使用 React.memo 阻断这种默认行为。

注意:Context 的值变化,也会导致所有消费该 Context 的组件重新渲染。


5. useCallback vs useMemo:缓存的艺术

这两个 Hook 都是为了性能优化,避免不必要的计算或渲染,但侧重点不同。

🧠 useMemo:缓存计算结果

  • 目的:避免昂贵的计算在每次渲染时重复执行。

  • 返回值:缓存后的数据值

  • 场景

    • 过滤/排序大型列表。
    • 复杂的数学运算。
    • 保持对象引用的稳定性(作为 useEffect 的依赖)。
  • 代码

    const sortedList = useMemo(() => {
      return list.sort((a, b) => a.value - b.value);
    }, [list]); // 只有 list 变了才重新排序
    

⚡ useCallback:缓存函数引用

  • 目的:保持函数引用的稳定性,防止子组件因 Props 中的函数变化而无谓渲染。

  • 返回值:缓存后的函数本身

  • 本质useCallback(fn, deps) 等价于 useMemo(() => fn, deps)

  • 场景

    • 传递给被 React.memo 包裹的子组件的事件处理函数。
    • 作为 useEffect 的依赖项(避免 effect 频繁触发)。
  • 代码

    const handleClick = useCallback(() => {
      doSomething(id);
    }, [id]); // 只有 id 变了,handleClick 才会变
    
    // 传给子组件,防止子组件无谓渲染
    <Child onClick={handleClick} />
    

误区警示:不要滥用!创建缓存本身也有开销。只有在计算真的很慢,或者确实导致了子组件频繁渲染时才使用。


6. React.memo:组件级的防抖

  • 定义:一个高阶组件(HOC),用于包裹函数组件。

  • 作用浅比较(Shallow Compare)  组件的 Props。

    • 如果 Props 没变 👉 跳过渲染,直接复用上一次的渲染结果。
    • 如果 Props 变了 👉 正常重新渲染。
  • 等价物:类组件中的 PureComponent

  • 代码

    const MyComponent = React.memo(function MyComponent({ data }) {
      return <div>{data}</div>;
    });
    
  • 进阶用法:可以传入第二个参数,自定义比较逻辑。

    React.memo(Component, (prevProps, nextProps) => {
      // 返回 true 表示跳过渲染,false 表示重新渲染
      return prevProps.id === nextProps.id; 
    });
    
  • 配合:通常与 useCallback 和 useMemo 配合使用。如果父组件传下来的函数每次都是新的,React.memo 就会失效。


7. 为什么要用 Hooks?(核心优势)

Hooks 不仅仅是语法糖,它是 React 编程范式的转变。

  1. 逻辑复用更优雅 (Logic Reuse)

    • 以前:需要用 HOC 或 Render Props,导致组件树嵌套过深(Wrapper Hell),调试困难。
    • 现在:使用 Custom Hooks,将逻辑提取为简单的函数,按需调用,扁平化代码结构。
  2. 逻辑聚合 (Co-location)

    • 以前:相关逻辑分散在 componentDidMountcomponentDidUpdatecomponentWillUnmount 等不同生命周期中,难以维护。
    • 现在:使用 useEffect 等,将相关的逻辑(如订阅/取消订阅)写在同一个 Hook 里,内聚性更强。
  3. 代码简洁,去 Class 化

    • 无需处理 this 指向问题(新手噩梦)。
    • 代码量更少,更易阅读。
    • 更容易进行静态分析和类型推断(TypeScript 友好)。
  4. 拥抱并发未来

    • Hooks 是为 React 的并发模式(Concurrent Mode)设计的,类组件很难适配可中断渲染等高级特性。

💡 面试总结口诀

  • Effect:空挂有变清尾巴,泄漏竞态全靠它。
  • HOC:函参组返做门禁,逻辑复用防嵌套。
  • 更新:自变爹给爹动身,子随父动是默认。
  • 缓存:Memo 算值 Callback 函,组件 Memo 挡渲染。
  • Hooks:去 Class 来聚逻辑,无 This 复用更给力。