useEffect 的底层是如何实现的?(美团面试原题)

源码

useEffect 是 React 中的一个核心 Hook,用于在函数组件中处理副作用(side effects)。副作用包括数据获取、订阅、手动 DOM 操作以及记录日志等操作。以下是对 useEffect 的源码和底层原理的解析。

源码位置:github.com/facebook/re...

useEffect 的实现分为两个部分: mountEffectupdateEffect ,分别对应组件的挂载阶段更新阶段

挂载阶段:mountEffect

mountEffect 是在组件首次渲染时调用的,用于注册副作用。

ts 复制代码
/**
 * 处理 useEffect 的挂载逻辑,负责创建、更新和销毁副作用的调度
 * 
 * @param create - 副作用创建函数(对应 useEffect 的第一个参数)
 * @param createDeps - 创建阶段的依赖数组(对应 useEffect 的第二个参数)
 * @param update - (可选)副作用更新回调,用于 CRUD 重载模式
 * @param updateDeps - (可选)更新阶段的依赖数组
 * @param destroy - (可选)副作用销毁回调,用于 CRUD 重载模式
 */
function mountEffect(
  create: (() => (() => void) | void) | (() => {...} | void | null),
  createDeps: Array<mixed> | void | null,
  update?: ((resource: {...} | void | null) => void) | void,
  updateDeps?: Array<mixed> | void | null,
  destroy?: ((resource: {...} | void | null) => void) | void,
): void {
  // 开发环境下且处于严格模式时,启用增强型副作用检查
  // `__DEV__` 标志区分开发/生产环境行为
  // `currentlyRenderingFiber.mode` 判断并发模式特性
  if (
    __DEV__ &&
    (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode &&
    (currentlyRenderingFiber.mode & NoStrictPassiveEffectsMode) === NoMode
  ) {
    // 判断是否启用 CRUD 重载模式(增删改查扩展)
    // `enableUseEffectCRUDOverload` 实验性功能开关
    // 当检测到 `update` 或 `destroy` 参数存在时,启用资源生命周期管理
    if (
      enableUseEffectCRUDOverload &&
      (typeof update === 'function' || typeof destroy === 'function')
    ) {
      // 挂载支持资源管理的增强型副作用(带更新/销毁回调)
      mountResourceEffectImpl(
        MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect, // 组合标志位:
        // - MountPassiveDevEffect: 开发模式下的被动效果挂载(专用的挂载追踪)
        // - PassiveEffect: 标准被动效果(useEffect 类型)
        // - PassiveStaticEffect: 静态树优化相关(防止不必要的副作用触发)
        HookPassive, // 标识这是 useEffect 类型(区别于 useLayoutEffect)
        create,
        createDeps,
        update,
        updateDeps,
        destroy,
      );
    } else {
      // 挂载标准副作用(无 CRUD 扩展)
      mountEffectImpl(
        MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect,
        HookPassive,
        create,
        createDeps,
      );
    }
  } else {
    // 生产环境或非严格模式逻辑
    // 重点看这里
    if (
      enableUseEffectCRUDOverload &&
      (typeof update === 'function' || typeof destroy === 'function')
    ) {
      // 生产环境下的资源管理副作用(移除开发模式专用标志位)
      mountResourceEffectImpl(
        PassiveEffect | PassiveStaticEffect, // 生产环境不包含 MountPassiveDevEffect
        HookPassive,
        create,
        createDeps,
        update,
        updateDeps,
        destroy,
      );
    } else {
      // 标准生产环境副作用处理
      // 我们大部分使用的时候,执行的这里
      // 核心逻辑被委托给 `mountEffectImpl`,它会将副作用注册到当前 Fiber 节点的更新队列中。
      mountEffectImpl(
        PassiveEffect | PassiveStaticEffect,
        HookPassive,
        create,
        createDeps,
      );
    }
  }
}

核心分流逻辑

graph TD A[开始] --> B{开发模式且严格模式?} B -->|是| C{启用CRUD扩展?} B -->|否| D{启用CRUD扩展?} C -->|是| E[调用mountResourceEffectImpl] C -->|否| F[调用mountEffectImpl] D -->|是| G[调用mountResourceEffectImpl] D -->|否| H[调用mountEffectImpl]

更新阶段:updateEffect

updateEffect 是在组件更新时调用的,用于检查依赖是否发生变化,并决定是否重新执行副作用。

ts 复制代码
/**
 * 处理 useEffect 在组件更新阶段的副作用逻辑,根据是否启用 CRUD 扩展模式选择不同的更新策略
 * 
 * @param create - 副作用创建函数(对应 useEffect 的第一个参数)
 * @param createDeps - 创建阶段的依赖数组(对应 useEffect 的第二个参数)
 * @param update - (可选)副作用更新回调,用于 CRUD 重载模式
 * @param updateDeps - (可选)更新阶段的依赖数组
 * @param destroy - (可选)副作用销毁回调,用于 CRUD 重载模式
 */
function updateEffect(
  create: (() => (() => void) | void) | (() => {...} | void | null),
  createDeps: Array<mixed> | void | null,
  update?: ((resource: {...} | void | null) => void) | void,
  updateDeps?: Array<mixed> | void | null,
  destroy?: ((resource: {...} | void | null) => void) | void,
): void {
  // 判断是否启用 CRUD 扩展模式且存在更新/销毁回调
  if (
    enableUseEffectCRUDOverload && // 实验性功能开关
    (typeof update === 'function' || typeof destroy === 'function')
  ) {
    // 执行带资源管理的增强型副作用更新
    updateResourceEffectImpl(
      PassiveEffect,        // 标志位:标准被动效果(对应 useEffect)
      HookPassive,          // Hook 类型标识(区别于 useLayoutEffect)
      create,               // 原始创建函数
      createDeps,           // 创建阶段依赖
      update,               // 更新回调(当依赖变化时触发)
      updateDeps,           // 更新阶段依赖
      destroy               // 资源销毁回调
    );
  } else {
    // 执行标准副作用更新流程
    updateEffectImpl(
      PassiveEffect,        // 副作用类型标识
      HookPassive,          // Hook 类型标识
      create,               // 副作用创建函数
      createDeps            // 依赖数组
    );
  }
}

核心逻辑解析

  1. CRUD 扩展模式 (enableUseEffectCRUDOverload)
  • 性质:实验性功能

  • 功能特性

    • 允许更精细控制副作用的生命周期
    • 通过 update/destroy 回调实现资源管理
  • 典型应用场景

    js 复制代码
    // WebSocket 连接管理示例
    useEffect({
      create: () => new WebSocket(url),
      update: (socket) => socket.reconnect(),
      destroy: (socket) => socket.close(),
    }, [url])
  1. 更新策略分流机制
  • 增强模式路径updateResourceEffectImpl
graph TD A[开始更新] --> B{依赖变化检查} B -->|createDeps 变化| C[触发旧资源 destroy] C --> D[执行新 create 函数] D --> E[注册新资源 update/destroy] B -->|仅 updateDeps 变化| F[触发资源 update]
  • 标准模式路径 → `updateEffectImpl
graph TD A[开始更新] --> B{createDeps 变化?} B -->|是| C[清理旧 effect] C --> D[创建新 effect] B -->|否| E[跳过执行]

核心实现:mountEffectImplupdateEffectImpl

这两个函数是 useEffect 的核心实现,负责将副作用注册到 Fiber 节点的更新队列中。

ts 复制代码
/**
 * 处理 useEffect 在组件挂载阶段的初始化逻辑(React 内部实现)
 * 
 * @param fiberFlags - Fiber 节点标记(如 PassiveEffect 表示异步副作用)
 * @param hookFlags - Hook 类型标记(如 HookPassive 对应 useEffect)
 * @param create - 用户传入的副作用函数
 * @param createDeps - 依赖项数组
 */
function mountEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  createDeps: Array<mixed> | void | null,
): void {
  // 1. 创建新的 Hook 节点并挂载到 Fiber 链表
  const hook = mountWorkInProgressHook();
  
  // 2. 规范化依赖项(将 undefined 转换为 null)
  const nextDeps = createDeps === undefined ? null : createDeps;
  
  // 3. 标记 Fiber 节点需要处理副作用(如 PassiveEffect 会触发异步调度)
  currentlyRenderingFiber.flags |= fiberFlags;
  
  // 4. 创建副作用对象并存入 Hook 状态
  hook.memoizedState = pushSimpleEffect(
    HookHasEffect | hookFlags, // 组合标志位:标记需要执行的副作用
    createEffectInstance(),    // 创建空的副作用实例容器
    create,                    // 用户传入的副作用函数
    nextDeps,                  // 规范化后的依赖数组
  );
}

/**
 * 处理 useEffect 在组件更新阶段的更新逻辑(React 内部实现)
 * 
 * @param fiberFlags - Fiber 节点标记
 * @param hookFlags - Hook 类型标记
 * @param create - 新的副作用函数
 * @param deps - 新的依赖项数组
 */
function updateEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  // 1. 获取当前正在处理的 Hook
  const hook = updateWorkInProgressHook();
  
  // 2. 规范化新依赖项
  const nextDeps = deps === undefined ? null : deps;
  
  // 3. 获取上一次渲染的副作用状态
  const effect: Effect = hook.memoizedState;
  const inst = effect.inst; // 关联的副作用实例(存储清理函数等)

  // 4. 依赖对比逻辑(仅在非首次渲染时执行)
  if (currentHook !== null) {
    if (nextDeps !== null) {
      // 4.1 获取上一次的依赖项
      const prevEffect: Effect = currentHook.memoizedState;
      const prevDeps = prevEffect.deps;
      
      // 4.2 依赖项未变化时优化处理
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 复用现有副作用(不添加 HookHasEffect 标记)
        hook.memoizedState = pushSimpleEffect(
          hookFlags,   // 仅保留基础标记
          inst,        // 复用实例(避免重复创建)
          create,      // 理论上可复用但 React 会传入新函数
          nextDeps,
        );
        return; // 提前退出,跳过副作用更新
      }
    }
  }

  // 5. 依赖变化时的处理
  // 5.1 标记 Fiber 需要处理副作用
  currentlyRenderingFiber.flags |= fiberFlags;

  // 5.2 创建新的副作用记录(添加 HookHasEffect 标记)
  hook.memoizedState = pushSimpleEffect(
    HookHasEffect | hookFlags, // 标记需要执行副作用
    inst,                      // 可能复用或替换实例
    create,                    // 新传入的副作用函数
    nextDeps,
  );
}

/**
 * 核心逻辑说明:
 * 
 * 1. 标志位系统:
 *    - HookHasEffect:核心标记,决定是否执行副作用
 *    - fiberFlags:控制副作用调度时机(如 PassiveEffect 表示异步执行)
 *    - hookFlags:标识 Hook 类型(Layout/Passive)
 * 
 * 2. 副作用生命周期:
 *    挂载阶段:创建 -> 渲染后执行
 *    更新阶段:依赖变化 -> 清理旧 -> 执行新
 *    卸载阶段:执行清理
 * 
 * 3. 性能优化:
 *    - 依赖未变化时跳过 HookHasEffect 标记
 *    - 复用 effect 实例避免重复创建
 *    - 通过位运算快速组合/检查标记
 * 
 * 4. 关键函数说明:
 *    - areHookInputsEqual:深度对比依赖项数组
 *    - createEffectInstance:创建存储清理函数等元数据的容器
 *    - pushSimpleEffect:创建并返回 effect 对象
 */

底层原理

  1. Fiber 数据结构

    • 每个组件对应一个 Fiber 节点,useEffect 的副作用会存储在 Fiber 节点的 updateQueue 中。
    • updateQueue 是一个链表,存储了所有的副作用。
  2. 依赖追踪

    • React 会在每次渲染时比较依赖数组,只有当依赖发生变化时才会重新执行副作用。
    • 这种机制可以避免不必要的副作用执行,提高性能。
  3. 清理机制

    • 如果副作用返回一个清理函数,React 会在下一次执行副作用之前调用它。
    • 清理函数的调用时机包括组件卸载和依赖变化。
  4. 调度与优先级

    • useEffect 的副作用是异步执行的,React 会在浏览器空闲时执行它们。
    • 通过设置 fiberFlags,React 可以在提交阶段标记需要执行的副作用。

总结

useEffect 是 React 用于管理副作用的 Hook,它在 commit 阶段 统一执行,确保副作用不会影响渲染。

在 React 源码中,useEffect 通过 Fiber 机制 在 commit 阶段 进行处理:

(1) useEffect 存储在 Fiber 节点上

React 组件是通过 Fiber 数据结构 组织的,每个 useEffect 都会存储在 fiber.updateQueue 中。

(2) useEffect 何时执行

React 组件更新后,React 在 commit 阶段 统一遍历 effect 队列,并执行 useEffect 副作用。

React 使用 useEffectEvent() 注册 effect,在 commitLayoutEffect 之后,异步执行 useEffect,避免阻塞 UI 渲染。

(3) useEffect 依赖变化的处理

依赖数组的比较使用 Object.is(),只有依赖变化时才重新执行 useEffect。

在更新阶段,React 遍历旧 effect,并先执行清理函数,然后再执行新的 effect。

简化的 useEffect 实现如下:

js 复制代码
function useEffect(callback, dependencies) {
  const currentEffect = getCurrentEffect() // 获取当前 Fiber 节点的 Effect

  if (dependenciesChanged(currentEffect.dependencies, dependencies)) {
    cleanupPreviousEffect(currentEffect) // 先执行上次 effect 的清理函数
    const cleanup = callback() // 执行 useEffect 传入的回调
    currentEffect.dependencies = dependencies
    currentEffect.cleanup = cleanup // 存储清理函数
  }
}

相比 useLayoutEffect,useEffect 是 异步执行,不会阻塞 UI 渲染。

相关链接

相关推荐
uhakadotcom4 分钟前
OpenHands:AI 驱动的软件开发框架
后端·面试·github
午后书香6 分钟前
一天三场面试,口干舌燥要晕倒(二)
前端·javascript·面试
uhakadotcom18 分钟前
FinGPT:金融领域的开源语言模型框架
后端·面试·github
Book_熬夜!21 分钟前
CSS—补充:CSS计数器、单位、@media媒体查询
前端·css·html·媒体
海姐软件测试1 小时前
面试时,如何回答好“你是怎么测试接口的?”
测试工具·面试·职场和发展·postman
几度泥的菜花1 小时前
如何禁用移动端页面的多点触控和手势缩放
前端·javascript
狼性书生1 小时前
electron + vue3 + vite 渲染进程到主进程的双向通信
前端·javascript·electron
肥肠可耐的西西公主1 小时前
前端(AJAX)学习笔记(CLASS 4):进阶
前端·笔记·学习
拉不动的猪2 小时前
Node.js(Express)
前端·javascript·面试
Re.不晚2 小时前
Web前端开发——HTML基础下
前端·javascript·html