useEffect 实现原理深度解析
useEffect是 React 中最强大也最容易混淆的 Hook 之一。它不仅让我们能够处理副作用,还涉及调度系统、优先级、清理机制等复杂概念。本文将深入剖析useEffect的完整生命周期。
一、Effect 对象的数据结构
Effect 对象
Effect Tag 系统
存储位置
Effect 对象存储在两个地方:
二、初始挂载:mountEffect
挂载流程
pushEffect 详解
为什么使用环形链表?
- 方便从任意位置开始遍历
- 插入操作只需 O(1) 时间
lastEffect.next就是firstEffect
三、重渲染:updateEffect
依赖比较
依赖比较算法
关键点:
- 使用
Object.is进行比较(值相等或同一引用) - 不比较数组长度变化(但会有警告)
- 空数组
[]意味着永远不重新执行 undefined或不传意味着每次都执行
四、Effect 执行:flushPassiveEffects
调度时机
执行时机:
- DOM 已更新
- 浏览器已绘制(在下一帧之前)
- 异步执行(不阻塞浏览器绘制)
执行流程图
Loading diagram...
flushPassiveEffects 实现
关键设计:
- 清理函数必须在 effect 函数之前执行
- 子组件的 effects 先于父组件执行
五、清理函数执行:commitPassiveUnmountEffects
遍历 Fiber 树
执行清理函数
六、Effect 函数执行:commitPassiveMountEffects
执行流程
调用 create 函数
关键点:
create()的返回值会被保存为destroy- 如果
create()不返回函数,destroy为undefined - 下次重新执行时,会先调用这个
destroy
七、执行顺序详解
完整执行顺序
组件卸载
八、useEffect vs useLayoutEffect
核心区别
| 特性 | useEffect | useLayoutEffect |
| 执行时机 | DOM 更新后,浏览器绘制后(异步) | DOM 更新后,浏览器绘制前(同步) |
| 是否阻塞渲染 | 否 | 是 |
| Effect Tag | HookPassive | HookLayout |
| 调度优先级 | NormalSchedulerPriority | 同步执行 |
| 使用场景 | 数据获取、订阅、日志 | DOM 测量、避免闪烁 |
实现差异
何时使用 useLayoutEffect
九、常见陷阱与最佳实践
依赖数组问题
清理函数的重要性
Effect 中的状态更新
竞态条件处理
十、性能优化
避免不必要的 Effect
拆分复杂 Effect
自定义 Hook 封装
十一、调试技巧
使用 useEffect 的第三个参数(DEV only)
添加日志
十二、总结
核心要点
- Effect 对象:由
tag、create、destroy、deps组成 - 执行时机:DOM 更新后、浏览器绘制后(异步)
- 执行顺序:先清理、后执行;子组件先、父组件后
- 依赖比较:使用
Object.is逐个比较 - 清理函数:必须正确实现,防止内存泄漏
- 性能优化:拆分独立关注点,避免不必要的 effect
最佳实践清单
- ✅ 始终正确声明依赖数组
- ✅ 为订阅和定时器提供清理函数
- ✅ 使用 ESLint 规则检查依赖
- ✅ 处理竞态条件和组件卸载
- ✅ 拆分复杂 effect 为多个独立 effect
- ✅ 优先考虑是否真的需要 effect
- ✅ 测量前用 useLayoutEffect,其他用 useEffect