React渲染时机完全指南:从一个电商组件的优化说起

React渲染时机完全指南:从一个电商组件的优化说起

Simon Lv1

前言

作为前端开发者,我们每天都在和 React 打交道,但你真的了解 React 的渲染时机吗?

  • 为什么有时候获取 DOM 元素的高度是 0?
  • 为什么设置的动画效果没有生效?
  • 为什么页面会出现闪烁?
  • useEffect 和 useLayoutEffect 到底该用哪个?
  • requestAnimationFrame 在 React 中有什么用?

在这篇文章中,我将通过一个真实的电商项目案例去搞懂 React 的渲染时机。

一、从一个真实的电商场景说起

1.1 业务背景

在电商项目中,商品分类筛选是一个非常常见的功能。想象一下淘宝或京东的商品列表页,顶部通常会有这样的筛选器:

手机通讯 > 手机 > 华为 | 小米 | OPPO | vivo | 苹果 | 三星 | 荣耀 | realme | 一加 | 魅族...

当分类项特别多时,我们需要:

  • 默认只显示 2 行,多余的折叠起来
  • 提供展开/收起按钮
  • 支持平滑的展开/收起动画
  • 切换一级分类时,二级分类需要重置

1.2 组件效果演示

类目筛选框

类目筛选区-展开

(gif效果太差了。。。。)

1.3 核心技术挑战

看似简单的需求,实现起来却遇到了不少挑战:

  1. 高度计算问题:如何准确获取内容的完整高度?
  2. 动画流畅性:如何实现平滑的高度过渡动画?
  3. 状态切换问题:切换分类时如何避免不必要的动画?
  4. 响应式适配:如何在不同屏幕尺寸下保持良好体验?

这些问题的核心都指向一个关键点:我们需要在正确的时机执行正确的操作

二、React 渲染机制深度解析

2.1 React 的工作流程

在深入代码之前,我们先来理解 React 的完整工作流程:

graph TB
    A[用户交互/Props变化] --> B[触发状态更新]
    B --> C[React 调度更新]
    C --> D[Render Phase
渲染阶段] D --> E[Reconciliation
协调过程] E --> F[生成 Fiber 树] F --> G[Commit Phase
提交阶段] G --> H[更新 DOM] H --> I[执行 useLayoutEffect] I --> J[浏览器绘制] J --> K[执行 useEffect] K --> L[用户看到更新] style D fill:#f9f,stroke:#333,stroke-width:2px style G fill:#9ff,stroke:#333,stroke-width:2px style J fill:#ff9,stroke:#333,stroke-width:2px

2.2 三个关键阶段

阶段一:Render Phase(渲染阶段)

  • 特点:可中断、可恢复、可并发
  • 任务:调用组件函数,生成新的虚拟 DOM 树
  • 限制:不能执行副作用(side effects)
// 这个阶段执行的代码
function MyComponent({ data }) {
// ✅ 纯计算
const processedData = useMemo(() => processData(data), [data]);

// ❌ 不要在这里执行副作用
// document.title = 'New Title'; // 错误!

return <div>{processedData}</div>;
}

阶段二:Commit Phase(提交阶段)

  • 特点:同步执行,不可中断
  • 任务:将变更应用到真实 DOM
  • 时机:useLayoutEffect 在此阶段执行
function MyComponent() {
useLayoutEffect(() => {
// 这里 DOM 已更新,但浏览器还未绘制
// 适合进行 DOM 测量或紧急的样式调整
const height = ref.current.scrollHeight;
console.log('真实高度:', height);
});
}

阶段三:Browser Paint(浏览器绘制)

  • 特点:浏览器的工作,React 不参与
  • 任务:计算布局、绘制像素
  • 时机:useEffect 在此之后执行

2.3 时序对比图

让我们通过一个详细的时序图来对比不同 Hook 的执行时机:

sequenceDiagram
    participant User as 用户
    participant React as React
    participant DOM as DOM
    participant Browser as 浏览器
    participant Effect as useEffect
    participant LayoutEffect as useLayoutEffect
    
    User->>React: 点击按钮
    React->>React: setState 更新状态
    
    rect rgb(255, 230, 230)
        Note over React: Render Phase 开始
        React->>React: 调用组件函数
        React->>React: 生成虚拟 DOM
        React->>React: Diff 算法对比
        Note over React: Render Phase 结束
    end
    
    rect rgb(230, 255, 230)
        Note over React,DOM: Commit Phase 开始
        React->>DOM: 更新真实 DOM
        DOM-->>React: DOM 更新完成
        React->>LayoutEffect: 同步执行 useLayoutEffect
        LayoutEffect-->>React: 执行完成
        Note over React,DOM: Commit Phase 结束
    end
    
    rect rgb(230, 230, 255)
        Note over Browser: Paint Phase 开始
        DOM->>Browser: 触发重排/重绘
        Browser->>Browser: 计算布局
        Browser->>Browser: 绘制像素
        Browser->>User: 显示更新后的界面
        Note over Browser: Paint Phase 结束
    end
    
    Browser->>Effect: 异步执行 useEffect
    Effect-->>React: 执行完成

三、代码实战:剖析折叠组件的实现

现在让我们来看看实际的代码实现,我会逐步解析每个关键部分。

3.1 组件整体结构

首先,让我们了解组件的整体结构:

// 主组件:SecondCategoryBox
const SecondCategoryBox = ({
categoryList, // 分类数据
defaultCategoryIds, // 默认选中项
maxVisibleRows, // 最大可见行数
onCategoryChange // 选中项变化回调
}) => {
// 状态管理
const [activeIDList, setActiveIDList] = useState([]);

// 使用自定义 Hook 管理折叠逻辑
const { containerRef, isExpanded, showToggleButton, setIsExpanded } = useCollapse({
maxVisibleRows,
dependencies: [categoryList]
});

// 渲染逻辑...
};

3.2 核心难点一:精确的高度计算

这是整个组件最核心的部分。我们需要:

  1. 获取内容的完整高度(展开时的高度)
  2. 计算折叠时应该显示的高度
  3. 决定是否需要显示展开/收起按钮
const calculateHeightsWithScale = useCallback(() => {
if (!containerRef.current) return;

const container = containerRef.current;

// 步骤1:临时解除高度限制
const originalHeight = container.style.height;
const originalOverflow = container.style.overflow;

container.style.height = 'auto';
container.style.overflow = 'visible';

// 步骤2:测量真实高度
// 注意:这里必须等待浏览器完成布局计算
const fullHeight = container.scrollHeight;

// 步骤3:计算折叠高度
const visibleHeight = rowHeight * maxVisibleRows + gap * (maxVisibleRows - 1);

// 步骤4:恢复原始样式
container.style.height = originalHeight;
container.style.overflow = originalOverflow;

// 步骤5:更新状态
setHeights({ full: fullHeight, visible: visibleHeight });
setShowToggleButton(fullHeight > visibleHeight);
}, [maxVisibleRows, rowHeight, gap]);

关键问题:什么时候调用这个函数?

3.3 核心难点二:选择正确的执行时机

这就涉及到我们要深入讨论的 React 渲染时机问题。让我们看看不同方案的对比:

方案一:使用 useEffect(❌ 会闪烁)

useEffect(() => {
calculateHeightsWithScale();
}, [categoryList]);

问题分析

graph LR
    A[分类数据变化] --> B[组件重新渲染]
    B --> C[DOM 更新]
    C --> D[浏览器绘制]
    D --> E[用户看到错误高度]
    E --> F[useEffect 执行]
    F --> G[计算并设置正确高度]
    G --> H[再次渲染]
    H --> I[用户看到正确高度]
    
    style E fill:#ffcccc
    style I fill:#ccffcc

用户会先看到错误的高度,然后突然跳到正确高度——这就是”闪烁”!

方案二:使用 useLayoutEffect(⚠️ 可能阻塞渲染)

useLayoutEffect(() => {
calculateHeightsWithScale();
}, [categoryList]);

优点:在浏览器绘制前执行,避免闪烁 缺点:同步执行,可能阻塞渲染,影响性能

方案三:使用 requestAnimationFrame(✅ 最佳方案)

useEffect(() => {
requestAnimationFrame(() => {
calculateHeightsWithScale();
});
}, [categoryList]);

为什么这是最佳方案?

graph TB
    A[useEffect 执行] --> B[注册 RAF 回调]
    B --> C[浏览器完成当前帧绘制]
    C --> D[布局信息已确定]
    D --> E[RAF 回调执行]
    E --> F[准确获取高度]
    F --> G[更新组件状态]
    
    style D fill:#ccffcc
    style F fill:#ccffcc

requestAnimationFrame 确保:

  1. 不阻塞当前的渲染
  2. 在下一帧开始前执行
  3. 此时布局计算已完成,可以准确获取尺寸

3.4 核心难点三:优雅地处理动画

当用户切换一级分类时,我们需要重置二级分类,但不希望用户看到收起动画:

useEffect(() => {
if (firstUpdate) {
// 首次加载,使用默认选中项
setActiveIDList(defaultCategoryIds);
setFirstUpdate(false);
} else {
// 切换分类时的处理

// 步骤1:立即禁用 CSS 过渡
setEnableTransition(false);

// 步骤2:重置所有状态
setIsExpanded(false);
setActiveIDList([]);

// 步骤3:在下一帧恢复过渡效果
requestAnimationFrame(() => {
setEnableTransition(true);
});
}
}, [categoryList]);

时序分析

sequenceDiagram
    participant User as 用户
    participant Component as 组件
    participant CSS as CSS动画
    participant Browser as 浏览器
    
    User->>Component: 切换一级分类
    Component->>Component: categoryList 变化
    Component->>CSS: 禁用 transition
    Component->>Component: 重置状态(高度变为折叠状态)
    Note over CSS: 无动画,瞬间变化
    Component->>Browser: 请求下一帧
    Browser-->>Component: 下一帧开始
    Component->>CSS: 启用 transition
    Note over CSS: 后续交互有动画

3.5 性能优化:响应式设计

组件还实现了一个巧妙的响应式系统:

const calculateScale = useCallback(() => {
const currentWidth = window.innerWidth;
const scale = currentWidth / baseWidth;

// 限制缩放范围,避免极端情况
const clampedScale = Math.max(0.8, Math.min(1.5, scale));

return {
scale: clampedScale,
rowHeight: Math.round(baseRowHeight * clampedScale),
gap: Math.round(baseGap * clampedScale)
};
}, [baseWidth, baseRowHeight, baseGap]);

这确保了组件在不同设备上都有合适的显示效果。

四、深入理解 useEffect 和 useLayoutEffect

4.1 本质区别

// useEffect:在浏览器完成绘制后异步执行
useEffect(() => {
console.log('1. DOM 已更新');
console.log('2. 浏览器已绘制');
console.log('3. 用户已看到变化');
console.log('4. 现在执行不会阻塞渲染');
});

// useLayoutEffect:在浏览器绘制前同步执行
useLayoutEffect(() => {
console.log('1. DOM 已更新');
console.log('2. 浏览器还未绘制');
console.log('3. 用户还看不到变化');
console.log('4. 可以在这里调整样式避免闪烁');
});

4.2 使用场景对比

graph TB
    A[需要执行副作用] --> B{是否影响视觉呈现?}
    
    B -->|是| C{是否需要 DOM 测量?}
    B -->|否| D[使用 useEffect]
    
    C -->|是| E{测量是否紧急?}
    C -->|否| F[使用 useLayoutEffect]
    
    E -->|是| G[useLayoutEffect]
    E -->|否| H[useEffect + RAF]
    
    D --> I[数据获取
事件订阅
日志上报] F --> J[阻止闪烁
同步滚动
焦点管理] G --> K[关键布局计算
动画初始状态] H --> L[非关键测量
性能优化] style B fill:#ffffcc style C fill:#ffffcc style E fill:#ffffcc

4.3 实际案例对比

让我们通过几个实际例子来加深理解:

案例1:工具提示定位

function Tooltip({ children, content }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const triggerRef = useRef();
const tooltipRef = useRef();

// ✅ 使用 useLayoutEffect 避免工具提示闪烁
useLayoutEffect(() => {
if (triggerRef.current && tooltipRef.current) {
const triggerRect = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();

setPosition({
top: triggerRect.top - tooltipRect.height - 8,
left: triggerRect.left + (triggerRect.width - tooltipRect.width) / 2
});
}
}, []);

return (
<>
<span ref={triggerRef}>{children}</span>
<div
ref={tooltipRef}
className="tooltip"
style={{ position: 'fixed', ...position }}
>
{content}
</div>
</>
);
}

案例2:滚动位置恢复

function ScrollRestore({ location }) {
// ✅ 使用 useLayoutEffect 立即恢复滚动位置
useLayoutEffect(() => {
const savedPosition = sessionStorage.getItem(`scroll-${location}`);
if (savedPosition) {
window.scrollTo(0, parseInt(savedPosition));
}
}, [location]);

// ✅ 使用 useEffect 保存滚动位置(非紧急)
useEffect(() => {
const handleScroll = () => {
sessionStorage.setItem(`scroll-${location}`, window.scrollY);
};

window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [location]);
}

案例3:动画序列

function AnimatedList({ items }) {
const [visibleItems, setVisibleItems] = useState([]);

// ❌ 错误:在 useLayoutEffect 中做复杂计算
// useLayoutEffect(() => {
// items.forEach((item, index) => {
// setTimeout(() => {
// setVisibleItems(prev => [...prev, item]);
// }, index * 100);
// });
// }, [items]);

// ✅ 正确:使用 useEffect + RAF
useEffect(() => {
let frameId;
let index = 0;

const animate = () => {
if (index < items.length) {
setVisibleItems(prev => [...prev, items[index]]);
index++;
frameId = requestAnimationFrame(animate);
}
};

frameId = requestAnimationFrame(animate);

return () => {
if (frameId) {
cancelAnimationFrame(frameId);
}
};
}, [items]);
}

五、requestAnimationFrame 的高级应用

5.1 什么是 requestAnimationFrame?

requestAnimationFrame(简称 RAF)是浏览器提供的一个 API,用于在下一次重绘之前执行动画。它的执行时机非常特殊:

graph LR
    A[帧开始] --> B[处理用户输入]
    B --> C[JS 执行]
    C --> D[RAF 回调]
    D --> E[样式计算]
    E --> F[布局]
    F --> G[绘制]
    G --> H[合成]
    H --> I[帧结束]
    
    style D fill:#ffcccc

5.2 在 React 中的应用场景

场景1:确保布局完成后测量

function useMeasure() {
const ref = useRef();
const [bounds, setBounds] = useState({});

useEffect(() => {
if (!ref.current) return;

// 确保在布局稳定后测量
const measure = () => {
requestAnimationFrame(() => {
if (ref.current) {
setBounds(ref.current.getBoundingClientRect());
}
});
};

measure();
window.addEventListener('resize', measure);

return () => window.removeEventListener('resize', measure);
}, []);

return [ref, bounds];
}

场景2:批量 DOM 操作

function batchDOMUpdates(updates) {
requestAnimationFrame(() => {
// 在一个帧内完成所有 DOM 操作
updates.forEach(update => update());

// 强制浏览器立即计算样式(如果需要读取)
// 注意:这会触发强制同步布局,谨慎使用
if (needsRead) {
document.body.offsetHeight; // 强制重排
}
});
}

场景3:平滑动画

function useAnimation(duration = 300) {
const [progress, setProgress] = useState(0);
const frameRef = useRef();
const startTimeRef = useRef();

const start = useCallback(() => {
startTimeRef.current = performance.now();

const animate = (currentTime) => {
const elapsed = currentTime - startTimeRef.current;
const progress = Math.min(elapsed / duration, 1);

setProgress(progress);

if (progress < 1) {
frameRef.current = requestAnimationFrame(animate);
}
};

frameRef.current = requestAnimationFrame(animate);
}, [duration]);

useEffect(() => {
return () => {
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
};
}, []);

return [progress, start];
}

5.3 RAF vs setTimeout/setInterval

// ❌ 不推荐:可能导致掉帧或不流畅
useEffect(() => {
const timer = setInterval(() => {
setPosition(prev => prev + 1);
}, 16); // 约 60fps

return () => clearInterval(timer);
}, []);

// ✅ 推荐:与浏览器刷新率同步
useEffect(() => {
let frameId;

const animate = () => {
setPosition(prev => prev + 1);
frameId = requestAnimationFrame(animate);
};

frameId = requestAnimationFrame(animate);

return () => cancelAnimationFrame(frameId);
}, []);

六、常见问题与最佳实践

6.1 常见错误及解决方案

错误1:在渲染阶段读取 DOM

// ❌ 错误
function BadComponent() {
const ref = useRef();
// 这里 ref.current 可能是 null
const height = ref.current?.offsetHeight || 0;

return <div ref={ref}>Content</div>;
}

// ✅ 正确
function GoodComponent() {
const ref = useRef();
const [height, setHeight] = useState(0);

useEffect(() => {
if (ref.current) {
setHeight(ref.current.offsetHeight);
}
}, []);

return <div ref={ref}>Content</div>;
}

错误2:过度使用 useLayoutEffect

// ❌ 错误:非视觉相关操作
useLayoutEffect(() => {
// 数据获取不应该阻塞渲染
fetch('/api/data').then(setData);
}, []);

// ✅ 正确
useEffect(() => {
fetch('/api/data').then(setData);
}, []);

错误3:忽视清理函数

// ❌ 错误:内存泄漏
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
// 忘记清理!
}, []);

// ✅ 正确
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);

return () => clearInterval(timer);
}, []);

6.2 性能优化建议

1. 避免不必要的布局计算

// ❌ 性能差:每次渲染都计算
function BadComponent({ items }) {
const heights = items.map(item => {
const element = document.getElementById(item.id);
return element?.offsetHeight || 0;
});
}

// ✅ 性能好:只在必要时计算
function GoodComponent({ items }) {
const [heights, setHeights] = useState([]);

useEffect(() => {
requestAnimationFrame(() => {
const newHeights = items.map(item => {
const element = document.getElementById(item.id);
return element?.offsetHeight || 0;
});
setHeights(newHeights);
});
}, [items]);
}

2. 批量更新 DOM

// ✅ 批量读取和写入
function BatchUpdate({ items }) {
useLayoutEffect(() => {
// 第一阶段:批量读取
const measurements = items.map(item => ({
id: item.id,
height: document.getElementById(item.id)?.offsetHeight || 0
}));

// 第二阶段:批量写入
measurements.forEach(({ id, height }) => {
const element = document.getElementById(id);
if (element) {
element.style.transform = `translateY(${height}px)`;
}
});
}, [items]);
}

3. 使用 CSS 代替 JS 动画

// ❌ JS 动画(性能较差)
const [height, setHeight] = useState(0);
useEffect(() => {
let current = 0;
const timer = setInterval(() => {
current += 5;
setHeight(current);
if (current >= 100) clearInterval(timer);
}, 16);
}, []);

// ✅ CSS 动画(性能更好)
const [expanded, setExpanded] = useState(false);
return (
<div
className={`container ${expanded ? 'expanded' : ''}`}
style={{
transition: 'height 0.3s ease-out',
height: expanded ? '100px' : '0px'
}}
/>
);

6.3 调试技巧

1. 可视化渲染时机

function useRenderLog(name) {
console.log(`${name} rendering`);

useLayoutEffect(() => {
console.log(`${name} layout effect`);
});

useEffect(() => {
console.log(`${name} effect`);

requestAnimationFrame(() => {
console.log(`${name} next frame`);
});
});
}

2. 性能监控

function usePerformanceMonitor(name) {
const renderStart = performance.now();

useLayoutEffect(() => {
const layoutEffectTime = performance.now();
console.log(`${name} to layout effect: ${layoutEffectTime - renderStart}ms`);
});

useEffect(() => {
const effectTime = performance.now();
console.log(`${name} to effect: ${effectTime - renderStart}ms`);

requestAnimationFrame(() => {
const frameTime = performance.now();
console.log(`${name} to next frame: ${frameTime - renderStart}ms`);
});
});
}

七、React 18 并发特性与渲染时机

7.1 并发渲染的影响

React 18 引入的并发特性改变了一些渲染行为:

import { startTransition, useDeferredValue, useId } from 'react';

function ConcurrentComponent({ searchTerm, items }) {
// 延迟非紧急更新
const deferredSearchTerm = useDeferredValue(searchTerm);

// 标记低优先级更新
const handleExpensiveUpdate = () => {
startTransition(() => {
// 这个更新可以被中断
setExpensiveState(calculateExpensiveValue());
});
};

// 紧急更新仍然同步处理
const handleUrgentUpdate = () => {
setUrgentState(value); // 立即响应
};
}

7.2 并发渲染下的 useEffect

在并发模式下,组件可能会多次渲染但只提交一次:

graph TB
    A[开始渲染] --> B{高优先级更新?}
    B -->|是| C[中断当前渲染]
    B -->|否| D[继续渲染]
    C --> E[处理高优先级]
    E --> F[重新开始低优先级]
    D --> G[提交到 DOM]
    F --> D
    G --> H[执行 Effects]
    
    style C fill:#ffcccc
    style E fill:#ffcccc

7.3 实践建议

function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);

// 使用 useDeferredValue 优化搜索体验
const deferredQuery = useDeferredValue(query);

// 紧急:显示加载状态
useEffect(() => {
setIsSearching(query !== deferredQuery);
}, [query, deferredQuery]);

// 非紧急:执行搜索
useEffect(() => {
let cancelled = false;

async function doSearch() {
const data = await searchAPI(deferredQuery);
if (!cancelled) {
startTransition(() => {
setResults(data);
});
}
}

doSearch();

return () => {
cancelled = true;
};
}, [deferredQuery]);

return (
<div>
{isSearching && <Spinner />}
<ResultsList results={results} />
</div>
);
}

八、实战总结:回到我们的折叠组件

现在,让我们用学到的知识重新审视最初的折叠组件,看看它是如何解决各种渲染时机问题的:

8.1 问题与解决方案对照

问题 解决方案 原理
获取准确的内容高度 useEffect + RAF 确保布局计算完成
切换分类时的动画闪烁 禁用/启用 transition 精确控制 CSS 动画时机
响应式适配 动态计算缩放比例 避免频繁的 DOM 操作
首次加载的默认状态 firstUpdate 标记 区分初始化和更新

8.2 完整的渲染流程

sequenceDiagram
    participant U as 用户
    participant C as 组件
    participant D as DOM
    participant B as 浏览器
    
    Note over U,B: 场景1:组件首次加载
    U->>C: 页面加载
    C->>C: 初始化状态
    C->>D: 渲染 DOM
    C->>C: useEffect 执行
    C->>B: RAF 注册回调
    B->>C: 下一帧执行测量
    C->>C: 设置正确高度
    C->>D: 更新 DOM
    B->>U: 显示完整内容
    
    Note over U,B: 场景2:用户点击展开
    U->>C: 点击展开按钮
    C->>C: setIsExpanded(true)
    C->>D: 更新高度样式
    Note over D,B: CSS transition 生效
    B->>U: 平滑展开动画
    
    Note over U,B: 场景3:切换分类
    U->>C: 选择新分类
    C->>C: 禁用 transition
    C->>C: 重置状态
    C->>D: 立即更新(无动画)
    C->>B: RAF 注册回调
    B->>C: 下一帧恢复 transition
    Note over C,B: 后续交互恢复动画

8.3 关键代码片段回顾

// 1. 自定义 Hook 封装复杂逻辑
export const useCollapse = (options) => {
const { maxVisibleRows = 2, dependencies = [] } = options;
const containerRef = useRef();
const [isExpanded, setIsExpanded] = useState(false);
const [showToggleButton, setShowToggleButton] = useState(false);

// 2. 使用 RAF 确保准确测量
useEffect(() => {
requestAnimationFrame(() => calculateHeightsWithScale());
}, dependencies);

// 3. 返回必要的状态和引用
return {
containerRef,
isExpanded,
showToggleButton,
setIsExpanded,
containerStyle: {
height: !showToggleButton ? 'auto' :
isExpanded ? heights.full : heights.visible,
overflow: 'hidden',
transition: 'height 0.3s ease-in-out'
}
};
};

九、写在最后

通过这个真实的电商项目案例,我们深入探讨了 React 的渲染时机问题。让我们再次总结一下核心要点:

9.1 核心原则

  1. 理解时机:知道代码在 React 生命周期的哪个阶段执行
  2. 选对工具:useEffect、useLayoutEffect、RAF 各有适用场景
  3. 避免闪烁:需要立即生效的视觉变化用 useLayoutEffect
  4. 性能优先:非紧急操作放在 useEffect 中异步执行
  5. 精确控制:使用 RAF 在正确的时机进行 DOM 测量

9.2 决策流程图

graph TD
    A[需要副作用?] -->|是| B[影响视觉?]
    A -->|否| Z[纯组件逻辑]
    
    B -->|是| C[需要 DOM 测量?]
    B -->|否| D[useEffect]
    
    C -->|是| E[测量紧急?]
    C -->|否| F[useLayoutEffect]
    
    E -->|是| G[useLayoutEffect]
    E -->|否| H[useEffect + RAF]
    
    D --> I[异步操作
数据获取
事件订阅] F --> J[防止闪烁
滚动恢复] G --> K[关键测量
初始定位] H --> L[性能优化
非关键测量] style A fill:#f9f,stroke:#333,stroke-width:4px style B fill:#bbf,stroke:#333,stroke-width:2px style C fill:#bbf,stroke:#333,stroke-width:2px style E fill:#bbf,stroke:#333,stroke-width:2px

9.3 从理论到实践

理解 React 的渲染时机不仅仅是理论知识,更是解决实际问题的关键。当你遇到以下问题时,请想起这篇文章:

  • 页面闪烁 → 检查是否应该使用 useLayoutEffect
  • 获取的尺寸为 0 → 使用 RAF 确保布局完成
  • 动画卡顿 → 考虑使用 CSS 动画或 RAF
  • 性能问题 → 将非紧急操作移到 useEffect

十、参考资料

  1. React 官方文档 - Hooks Reference
  2. React 源码解析 - Fiber 架构
  3. Web 性能优化 - requestAnimationFrame
  4. React 18 Working Group
  5. 浏览器渲染原理

如果这篇文章对你有帮助,欢迎点赞、收藏和分享。有任何问题或不同见解,也欢迎在评论区讨论!

  • 标题: React渲染时机完全指南:从一个电商组件的优化说起
  • 作者: Simon
  • 创建于 : 2025-07-24 14:53:25
  • 更新于 : 2025-07-24 15:45:25
  • 链接: https://www.simonicle.cn/2025/07/24/【从项目到技术】React渲染时机指南/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论