【React源码31】深入学习React 源码实现—— useLayoutEffect底层实现(最完整版)

深入详解 useLayoutEffect 的实现原理与源码分析(最完整版)


一、概述

useLayoutEffect 是 React 提供的一个 Hook,用于在DOM 更新之后但浏览器绘制之前同步执行副作用。它与 useEffect 的最大区别在于:

特性useEffectuseLayoutEffect
执行时机在 DOM 更新后异步执行在 DOM 更新后、页面渲染前同步执行
是否阻塞页面渲染不会
是否适合测量 DOM 布局❌ 不推荐✅ 推荐

🧠 适用场景:

  • 需要测量或操作 DOM 布局;
  • 动画初始计算;
  • 避免视觉闪烁(如位置调整);

二、历史发展与设计背景

2.1 类组件时代:生命周期函数的局限

在类组件中,开发者通常使用以下生命周期方法进行布局相关操作:

  • componentDidMount
  • componentDidUpdate

这些方法虽然可以访问真实 DOM,但无法控制其执行顺序和性能。

2.2 Hooks 引入:useEffect 与 useLayoutEffect 分离职责

React v16.8 引入了 Hooks,并将副作用分为两类:

Hook执行时机是否同步用途
useEffectLayout 之后,异步执行数据获取、订阅等不影响布局的操作
useLayoutEffectLayout 之后,绘制之前,同步执行✅ 是测量、动画、布局调整等影响视觉的操作

2.3 版本发布时间线

版本时间内容
React 16.82019 年 2 月正式发布 useLayoutEffect Hook

三、源码文件定位(React v18.2)

以下是 useLayoutEffect 在 React 源码中的核心位置:

文件名路径功能
ReactFiberHooks.jspackages/react-reconciler/src/ReactFiberHooks.js定义 useLayoutEffect 的 Hook 实现逻辑
ReactFiberBeginWork.jspackages/react-reconciler/src/ReactFiberBeginWork.js处理 Hook 初始化和更新
ReactFiberCommitWork.jspackages/react-reconciler/src/ReactFiberCommitWork.js在 commit 阶段执行 layout effect
ReactFiberFlags.jspackages/react-reconciler/src/ReactFiberFlags.js标记 Hook 副作用类型
ReactFiberWorkLoop.jspackages/react-reconciler/src/ReactFiberWorkLoop.js控制副作用调度机制

四、useLayoutEffect 的语法结构

useLayoutEffect(() => {
  // 同步执行的副作用逻辑
  return () => {
    // 清理逻辑(可选)
  };
}, [deps]);
参数描述
第一个参数一个回调函数,包含副作用逻辑
第二个参数依赖项数组,决定是否重新执行副作用
返回值可选的清理函数,在组件卸载或依赖变化时调用

五、算法设计思路与步骤详解

✅ 总体思想:

useLayoutEffect 是 React 中用于处理同步副作用的核心 Hook,其执行发生在:

  1. DOM 已更新
  2. 页面尚未重绘
  3. 同步执行副作用逻辑
  4. 可能修改 DOM 布局以避免视觉问题

📌 关键流程图解:

组件 render
执行 render 函数
创建 Fiber 节点
执行 useLayoutEffect 回调
更新 DOM
执行 useLayoutEffect 的副作用函数
页面重绘
执行 useEffect 的副作用

六、底层实现原理详解

6.1 Hook 初始化阶段(简化源码模拟)

// 挂载阶段的 useLayoutEffect 实现(简化版)
function mountLayoutEffect(create, deps) {
  // 1. 创建/获取当前正在处理的 WorkInProgress Hook
  const hook = mountWorkInProgressHook();

  // 2. 处理依赖项:将 undefined 转为 null(React 内部统一格式)
  const nextDeps = deps === undefined ? null : deps;
  
  // 3. 初始化副作用状态(存储创建函数和依赖项)
  hook.memoizedState = {
    create,    // 用户传入的副作用创建函数
    deps: nextDeps, // 依赖数组(可能为 null)
  };

  // 4. 标记当前 Fiber 需要执行 Layout Effect
  // LayoutMask 表示该组件有 Layout 阶段的副作用需要同步执行
  currentlyRenderingFiber.flags |= LayoutMask;
  
  /* 完整执行流程:
  1. 组件渲染时创建 Hook 并记录副作用
  2. 通过 flags 标记需要执行 Layout Effect
  3. 在提交阶段(Commit Phase)的 Layout 阶段:
     - 执行所有带 LayoutMask 标记的副作用
     - 同步执行 create 函数
     - 捕获执行过程中的错误(通过 Error Boundary)
  */
}
注释说明:
  • hook.memoizedState:保存副作用函数和依赖项;
  • currentlyRenderingFiber.flags |= LayoutMask:标记该 Hook 为 layout effect,需在 commit 阶段执行;

6.2 更新阶段(判断依赖变化)

// 更新阶段的 useLayoutEffect 实现(简化版)
function updateLayoutEffect(create, nextDeps) {
  // 1. 获取当前正在处理的 WorkInProgress Hook(更新阶段的 Hook)
  const hook = updateWorkInProgressHook();
  
  // 2. 读取之前挂载阶段保存的副作用对象(包含 create 函数和依赖项)
  const prevEffect = hook.memoizedState;

  // 3. 依赖项对比:浅比较新旧依赖数组
  if (areHookInputsEqual(prevEffect.deps, nextDeps)) {
    // 依赖未变化时直接返回,不执行任何操作(性能优化)
    return;
  }

  // 4. 依赖变化时创建新的副作用对象
  const effect = {
    create,    // 用户传入的副作用创建函数(可能已更新)
    deps: nextDeps, // 新的依赖数组
  };

  // 5. 更新 Hook 的 memoizedState 为新副作用对象
  hook.memoizedState = effect;

  // 6. 标记当前 Fiber 需要执行 Layout Effect(同步执行)
  // LayoutMask 表示该组件有 Layout 阶段的副作用需要执行
  currentlyRenderingFiber.flags |= LayoutMask;

  /* 完整执行流程:
  1. 组件更新时检测到 useLayoutEffect 调用
  2. 通过 updateWorkInProgressHook 获取当前 Hook
  3. 对比新旧依赖项(浅比较)
  4. 依赖变化时:
     - 创建新副作用对象
     - 更新 Hook 状态
     - 标记 Fiber 需执行 Layout Effect
  5. 在提交阶段(Commit Phase)的 Layout 阶段:
     - 执行所有带 LayoutMask 标记的副作用
     - 同步执行 create 函数
     - 自动捕获执行过程中的错误(通过 Error Boundary)
  */
}
注释说明:
  • areHookInputsEqual():比较依赖项是否变化;
  • 若依赖变化,更新 memoizedState;
  • 继续标记为 layout effect,等待 commit 阶段执行;

6.3 commit 阶段执行副作用

// 提交阶段处理 Layout 效应的入口函数(简化版)
function commitLayoutEffects(finishedWork) {
  // 获取当前 fiber 的对应 current 树节点(可能为 null,表示首次挂载)
  const current = finishedWork.alternate;

  // 检查该 fiber 是否有 Layout 阶段的副作用需要执行
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    try {
      // 执行具体的 Layout 效应逻辑
      commitLayoutEffectOnFiber(current, finishedWork);
    } catch (error) {
      // 捕获提交阶段的错误(通过 Error Boundary 处理)
      captureCommitPhaseError(finishedWork, error);
    }
  }
}

// 执行具体 Layout 效应的函数(简化版)
function commitLayoutEffectOnFiber(current, fiber) {
  // 获取组件实例(类组件)或 null(函数组件)
  const instance = fiber.stateNode;
  
  // 获取当前 Hook 的 memoizedState(即保存的副作用对象)
  const hook = fiber.memoizedState;

  if (hook !== null) {
    // 获取副作用对象(实际应为环形链表结构,此处简化)
    const effect = hook;
    
    // 执行副作用创建函数(用户传入的 create 函数)
    const destroy = effect.create();
    
    // 保存清理函数(实际 React 会保留之前的 destroy 用于卸载)
    if (typeof destroy === 'function') {
      effect.destroy = destroy; // ⚠️ 简化实现:实际需处理更新时的清理逻辑
    }
  }
}
注释说明:
  • commitLayoutEffects():在 commit 阶段执行所有 layout effect;
  • commitLayoutEffectOnFiber():具体执行某个 fiber 上的 layout effect;
  • effect.create():执行用户传入的副作用函数;
  • effect.destroy:保存清理函数供后续卸载时调用;

七、完整代码实现与注释(模拟版)

7.1 自定义 useLayoutEffect Hook 模拟实现

// 定义全局变量追踪当前 Hook 和挂载状态(⚠️ 实际 React 使用 Fiber 链表管理)
let currentHook = null;       // 当前处理的 Hook 节点
let isMounting = true;        // 挂载阶段标记

function useMyLayoutEffect(create, deps) {
  // 创建当前 Hook 的副作用对象(⚠️ 实际 React 使用环形链表存储)
  const hook = {
    create,    // 用户传入的副作用创建函数(如:() => { ... })
    deps,      // 当前依赖数组(用于对比是否需要更新)
    destroy: undefined, // 清理函数(由 create 返回,用于卸载时清理资源)
  };

  if (isMounting) {
    // 组件初次挂载时执行
    hook.destroy = create(); // 立即执行副作用函数并保存清理函数
  } else {
    // 组件更新阶段:对比新旧依赖
    const prevDeps = currentHook.deps; // 获取前一次依赖
    // 浅比较依赖数组(⚠️ 实际 React 使用 areHookInputsEqual)
    const hasChanged = !prevDeps || !areDepsEqual(deps, prevDeps);

    if (hasChanged) {
      // 依赖变化:执行新副作用并保存清理函数
      hook.destroy = create();
    } else {
      // 依赖未变:复用前一次的清理函数(⚠️ 实际需先执行旧清理再创建新副作用)
      hook.destroy = currentHook.destroy;
    }
  }

  currentHook = hook;  // 更新当前 Hook 节点(⚠️ 实际 React 使用链表遍历)
  isMounting = false;  // 标记为更新阶段(⚠️ 组件卸载后重新挂载时需重置)
}

// 模拟依赖数组的浅比较函数
function areDepsEqual(nextDeps, prevDeps) {
  // 遍历依赖数组进行严格相等比较(⚠️ 实际 React 使用 Object.is)
  for (let i = 0; i < nextDeps.length; i++) {
    if (!Object.is(nextDeps[i], prevDeps[i])) {
      return false; // 发现不匹配项立即返回 false
    }
  }
  return true; // 所有依赖项均匹配
}

7.2 使用示例:同步测量 DOM 尺寸

import { useRef, useLayoutEffect } from 'react';

function MeasureBox() {
  const boxRef = useRef(null);

  useLayoutEffect(() => {
    const node = boxRef.current;
    const rect = node.getBoundingClientRect();
    console.log('盒子尺寸:', rect.width, rect.height);
  }, []);

  return (
    <div ref={boxRef} style={{ width: '200px', height: '100px', background: 'lightblue' }}>
      我是一个盒子
    </div>
  );
}

八、最佳实践与注意事项

✅ 推荐做法:

  • 用于需要同步执行的 DOM 操作;
  • 配合 ref 获取真实 DOM;
  • 避免在其中执行耗时任务,以免阻塞渲染;
  • 配合 useEffect 构建完整的副作用管理方案;

❌ 不推荐做法:

  • 不要在其中执行网络请求;
  • 不要频繁修改 state 导致多次触发;
  • 不要滥用,优先考虑 useEffect
  • 不要忘记清理副作用;

九、源码分析:ReactFiberHooks.js 中的实现

以下为 React 源码中 useLayoutEffect 的核心部分(简化):

// 挂载阶段的 useLayoutEffect 实现(React 内部核心逻辑)
function mountLayoutEffect(create, deps) {
  // 1. 创建新 Hook 节点(WorkInProgress 阶段专用)
  const hook = mountWorkInProgressHook();

  // 2. 统一依赖项格式:将 undefined 转为 null(React 内部规范)
  const nextDeps = deps === undefined ? null : deps;

  // 3. 创建副作用描述符并挂载到 Hook 状态
  hook.memoizedState = pushEffect(
    // 副作用标志组合:Layout 阶段 + HasEffect(表示需要执行)
    Layout | HasEffect,
    // 用户传入的副作用创建函数(如:() => { ...副作用逻辑... })
    create,
    // 清理函数(首次挂载时无旧副作用,故为 undefined)
    undefined,
    // 处理后的依赖数组(可能为 null)
    nextDeps
  );
}

// 更新阶段的 useLayoutEffect 实现(React 内部核心逻辑)
function updateLayoutEffect(create, deps) {
  // 1. 获取当前正在处理的 WorkInProgress Hook 节点
  const hook = updateWorkInProgressHook();
  
  // 2. 统一依赖项格式(保持与挂载阶段一致的处理逻辑)
  const nextDeps = deps === undefined ? null : deps;
  
  // 3. 读取之前挂载阶段保存的副作用描述符
  const effect = hook.memoizedState;

  // 4. 浅比较新旧依赖数组(React 内部优化关键步骤)
  if (areHookInputsEqual(effect.deps, nextDeps)) {
    // 5. 依赖未变化:创建无 HasEffect 的副作用描述符
    hook.memoizedState = pushEffect(
      Layout,          // 仅保留 Layout 阶段标志(跳过执行)
      create,          // 用户传入的副作用创建函数
      effect.destroy,  // 复用之前挂载阶段的清理函数
      nextDeps         // 新依赖数组(可能未变)
    );
  } else {
    // 6. 依赖变化:创建带 HasEffect 的副作用描述符
    hook.memoizedState = pushEffect(
      Layout | HasEffect, // 标记需要执行 Layout 副作用
      create,             // 用户传入的副作用创建函数
      effect.destroy,     // 复用之前挂载阶段的清理函数
      nextDeps            // 新依赖数组(已变化)
    );
  }
}

/* 关键概念说明:
1. Layout 阶段:
   - 发生在 DOM 更新后、浏览器重绘前
   - 适合需要立即操作 DOM 的副作用(如:读取 DOM 尺寸)

2. HasEffect 标志:
   - 控制副作用是否需要执行
   - 挂载阶段必然设置
   - 更新阶段依赖变化时设置

3. pushEffect 函数:
   - 将副作用描述符加入副作用链表
   - 返回新的副作用描述符(供 Hook 状态存储)

4. areHookInputsEqual:
   - 浅比较依赖数组(Object.is)
   - 用于优化性能(跳过未变化的副作用)
*/

十、常见面试题(高频)

题目答案概要
1. useLayoutEffectuseEffect 有什么区别?useLayoutEffect 同步执行,影响布局;useEffect 异步执行,不影响渲染。
2. useLayoutEffect 会在什么时候执行?在 DOM 更新后、页面绘制前同步执行。
3. useLayoutEffect 是否会影响性能?会,因为它同步执行,可能阻塞页面渲染。
4. useLayoutEffect 是否可以返回清除函数?可以,用于清理副作用。
5. useLayoutEffect 是否可以在服务端渲染(SSR)中使用?可以,但要注意 window 对象不存在。
6. useLayoutEffect 是否可以在类组件中使用?不支持,只能用于函数组件。
7. useLayoutEffect 是否可以用于动画?可以,常用于动画初始化计算。
8. useLayoutEffect 是否可以替代 window.addEventListener('resize')可以结合使用,用于监听 DOM 布局变化。
9. useLayoutEffect 是否可以用于表单验证?不推荐,应使用 useEffect 或其他状态管理方式。
10. useLayoutEffect 是否必须提供依赖项?不是必须,但建议提供以避免不必要的重复执行。

十一、结语

通过对 useLayoutEffect 的深入剖析,我们了解到它是 React 中处理同步副作用的关键 Hook,适用于 DOM 布局测量、动画初始化等场景。

掌握它的原理、使用方式以及最佳实践,有助于你:

  • 更好地理解 React 的副作用系统;
  • 构建更稳定、流畅的 UI;
  • 应对高级前端面试和技术挑战;

如果你希望进一步研究,老曹建议阅读官方源码中的如下部分:

  • ReactFiberHooks.js:查看 useLayoutEffect 的完整实现;
  • ReactFiberBeginWork.js:观察 Hook 初始化流程;
  • ReactFiberCommitWork.js:查看 commit 阶段如何执行 layout effect;
  • ReactFiberFlags.js:了解副作用标记机制;
  • ReactFiberWorkLoop.js:学习 React 的副作用调度机制。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈前端老曹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值