React useEffect 源码深度解析
前言
useEffect 是 React Hooks 中最常用的 API 之一,它让函数组件能够执行副作用操作。本文将深入 React 源码,剖析 useEffect 的实现原理和执行机制。
一、基础概念回顾
javascript
useEffect(() => {
// 副作用逻辑
return () => {
// 清理函数
};
}, [deps]);
useEffect 接收两个参数:
- effect 函数:包含副作用逻辑的回调函数
- 依赖数组:决定 effect 何时执行
二、源码结构概览
在 React 源码中,useEffect 的实现分为两个阶段:
- Mount 阶段 (首次渲染):
mountEffect - Update 阶段 (更新渲染):
updateEffect
核心文件路径
bash
packages/react-reconciler/src/ReactFiberHooks.js
三、Mount 阶段:mountEffect
3.1 入口函数
javascript
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect,
HookPassive,
create,
deps,
);
}
关键点:
PassiveEffect:标记这是一个被动副作用(异步执行)HookPassive:Hook 的类型标识
3.2 mountEffectImpl 实现
javascript
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 1. 创建 Hook 对象
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 2. 标记 Fiber 需要执行副作用
currentlyRenderingFiber.flags |= fiberFlags;
// 3. 创建 Effect 对象并存储
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
执行流程:
-
创建 Hook 链表节点
javascriptfunction mountWorkInProgressHook(): Hook { const hook: Hook = { memoizedState: null, // 存储 effect 对象 baseState: null, baseQueue: null, queue: null, next: null, // 指向下一个 Hook }; // 插入 Hook 链表 if (workInProgressHook === null) { currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; } -
创建 Effect 对象
javascriptfunction pushEffect(tag, create, destroy, deps) { const effect: Effect = { tag, // 标记位(HookHasEffect | HookPassive) create, // effect 函数 destroy, // 清理函数(初始为 undefined) deps, // 依赖数组 next: null, // 指向下一个 Effect }; // 获取 Fiber 上的 updateQueue(环形链表) let componentUpdateQueue = currentlyRenderingFiber.updateQueue; if (componentUpdateQueue === null) { // 首次创建,构建环形链表 componentUpdateQueue = createFunctionComponentUpdateQueue(); currentlyRenderingFiber.updateQueue = componentUpdateQueue; componentUpdateQueue.lastEffect = effect.next = effect; } else { // 插入环形链表 const lastEffect = componentUpdateQueue.lastEffect; const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } return effect; }
Effect 环形链表结构:
markdown
Fiber.updateQueue.lastEffect → Effect3 → Effect1 → Effect2 → Effect3 (环形)
↑ |
└──────────────────────────────┘
四、Update 阶段:updateEffect
4.1 入口函数
javascript
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(
PassiveEffect,
HookPassive,
create,
deps,
);
}
4.2 updateEffectImpl 实现
javascript
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 1. 获取当前 Hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy; // 获取上次的清理函数
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 2. 比较依赖是否变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 依赖未变化,不执行 effect
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 3. 依赖变化,标记需要执行 effect
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // 添加 HookHasEffect 标记
create,
destroy,
nextDeps,
);
}
关键:依赖比较
javascript
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) return false;
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// 使用 Object.is 进行浅比较
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
Object.is vs ===:
javascript
Object.is(NaN, NaN) // true (=== 为 false)
Object.is(+0, -0) // false (=== 为 true)
五、Effect 的执行时机
5.1 Commit 阶段概览
React 的 Commit 阶段分为三个子阶段:
scss
Before Mutation → Mutation → Layout
↓ ↓ ↓
(DOM 变更前) (DOM 变更) (DOM 变更后)
↓
调度 useEffect
5.2 调度 Effect 执行
在 Layout 阶段结束后:
javascript
function commitRoot(root) {
// ... 前置工作
// Before Mutation 阶段
commitBeforeMutationEffects(root, finishedWork);
// Mutation 阶段
commitMutationEffects(root, finishedWork);
// 切换 Fiber 树
root.current = finishedWork;
// Layout 阶段
commitLayoutEffects(finishedWork, root);
// 调度 useEffect(异步)
if (rootDoesHavePassiveEffects) {
scheduleCallback(NormalPriority, () => {
flushPassiveEffects();
return null;
});
}
}
5.3 执行 Effect
javascript
function flushPassiveEffects(): boolean {
// 1. 先执行所有上一次的清理函数
commitPassiveUnmountEffects(root.current);
// 2. 再执行本次的 effect 函数
commitPassiveMountEffects(root, root.current);
}
执行清理函数:
javascript
function commitPassiveUnmountEffects(finishedWork: Fiber): void {
const updateQueue = finishedWork.updateQueue;
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const { destroy, tag } = effect;
// 只执行标记了 HookHasEffect 的 effect
if ((tag & HookHasEffect) !== NoHookEffect) {
if (destroy !== undefined) {
safelyCallDestroy(finishedWork, destroy);
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
执行 effect 函数:
javascript
function commitPassiveMountEffects(finishedWork: Fiber): void {
const updateQueue = finishedWork.updateQueue;
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const { create, tag } = effect;
if ((tag & HookHasEffect) !== NoHookEffect) {
const destroy = create(); // 执行 effect 函数
if (typeof destroy === 'function') {
effect.destroy = destroy; // 保存清理函数
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
六、完整执行流程示例
javascript
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Effect:', count);
return () => {
console.log('Cleanup:', count);
};
}, [count]);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
执行时间线
首次渲染(Mount):
markdown
1. Render 阶段:
- mountEffect 创建 Effect 对象
- Fiber.flags |= PassiveEffect
2. Commit 阶段:
- Before Mutation
- Mutation(DOM 更新)
- Layout
- 调度 flushPassiveEffects(宏任务)
3. 异步执行 Effect:
- 执行 effect 函数
- 控制台输出:"Effect: 0"
- 保存 destroy 函数
更新渲染(count: 0 → 1):
markdown
1. Render 阶段:
- updateEffect 比较依赖 [0] vs [1]
- 依赖变化,添加 HookHasEffect 标记
2. Commit 阶段:
- DOM 更新
- 调度 flushPassiveEffects
3. 异步执行 Effect:
- 先执行上次的清理函数
- 控制台输出:"Cleanup: 0"
- 执行新的 effect 函数
- 控制台输出:"Effect: 1"
组件卸载(Unmount):
markdown
1. Commit 阶段:
- 执行最后的清理函数
- 控制台输出:"Cleanup: 1"
七、关键设计要点
7.1 为什么是异步执行?
javascript
// useEffect:异步执行(不阻塞渲染)
useEffect(() => {
// 在浏览器绘制后执行
heavyComputation();
});
// useLayoutEffect:同步执行(阻塞渲染)
useLayoutEffect(() => {
// 在 DOM 更新后、浏览器绘制前执行
measureDOM();
});
对比:
| Hook | 执行时机 | 是否阻塞渲染 | 使用场景 |
|---|---|---|---|
| useEffect | Layout 后异步 | 否 | 数据获取、订阅、日志 |
| useLayoutEffect | Layout 阶段同步 | 是 | DOM 测量、动画 |
7.2 环形链表的优势
javascript
// 环形链表特点:
// 1. 无需遍历即可找到首尾节点
// 2. 插入删除操作 O(1)
// 3. 遍历时自然形成循环
lastEffect.next = firstEffect; // 首尾相连
7.3 依赖数组的三种情况
javascript
// 1. 无依赖数组:每次渲染都执行
useEffect(() => {
console.log('每次都执行');
});
// 2. 空依赖数组:仅首次执行
useEffect(() => {
console.log('仅首次执行');
}, []);
// 3. 有依赖:依赖变化时执行
useEffect(() => {
console.log('count 变化时执行');
}, [count]);
源码判断逻辑:
javascript
if (nextDeps === undefined) {
// 无依赖数组,总是执行
return false;
}
if (prevDeps === null) {
// 首次渲染,总是执行
return false;
}
// 比较依赖数组
return areHookInputsEqual(nextDeps, prevDeps);
八、常见陷阱与解决方案
8.1 闭包陷阱
问题代码:
javascript
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 永远是 0
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖导致闭包陈旧
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
解决方案 1:添加依赖
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 正确获取最新值
}, 1000);
return () => clearInterval(timer);
}, [count]); // 添加依赖
解决方案 2:使用函数式更新
javascript
useEffect(() => {
const timer = setInterval(() => {
setCount(c => {
console.log(c); // 获取最新值
return c;
});
}, 1000);
return () => clearInterval(timer);
}, []); // 无需依赖
8.2 无限循环
问题代码:
javascript
function App() {
const [data, setData] = useState([]);
useEffect(() => {
setData([...data, 1]); // 每次执行都改变 data
}, [data]); // data 变化触发 effect → 无限循环
}
解决方案:
javascript
useEffect(() => {
// 使用函数式更新,无需依赖 data
setData(prev => [...prev, 1]);
}, []); // 空依赖,仅执行一次
8.3 竞态条件
问题代码:
javascript
function Profile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data); // 可能设置过期数据
});
}, [userId]);
}
场景:
makefile
userId: 1 → 2 → 3
请求顺序:1 → 2 → 3
响应顺序:1 → 3 → 2(网络波动)
最终 user:2(错误!应该是 3)
解决方案 1:清理标志
javascript
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(data => {
if (!cancelled) {
setUser(data);
}
});
return () => {
cancelled = true; // 清理时标记取消
};
}, [userId]);
解决方案 2:AbortController
javascript
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(setUser)
.catch(err => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort();
}, [userId]);
九、性能优化建议
9.1 拆分 Effect
不推荐:
javascript
useEffect(() => {
subscribeToA(a);
subscribeToB(b);
subscribeToC(c);
return () => {
unsubscribeA();
unsubscribeB();
unsubscribeC();
};
}, [a, b, c]); // 任一变化都重新订阅全部
推荐:
javascript
useEffect(() => {
subscribeToA(a);
return () => unsubscribeA();
}, [a]);
useEffect(() => {
subscribeToB(b);
return () => unsubscribeB();
}, [b]);
useEffect(() => {
subscribeToC(c);
return () => unsubscribeC();
}, [c]);
9.2 避免不必要的依赖
问题代码:
javascript
function App() {
const options = { interval: 1000 }; // 每次渲染都创建新对象
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, options.interval);
return () => clearInterval(timer);
}, [options]); // options 每次都是新对象 → 每次都执行
}
解决方案 1:直接使用原始值
javascript
const interval = 1000;
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, interval);
return () => clearInterval(timer);
}, [interval]);
解决方案 2:useMemo
javascript
const options = useMemo(() => ({ interval: 1000 }), []);
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, options.interval);
return () => clearInterval(timer);
}, [options]);
十、总结
核心要点
- 双阶段设计:Mount 和 Update 分别处理,Update 阶段通过依赖比较决定是否执行
- 环形链表:用于存储和管理多个 Effect
- 异步执行:通过 Scheduler 调度,不阻塞渲染
- 清理机制:先执行旧的清理函数,再执行新的 effect 函数
- 浅比较依赖 :使用
Object.is比较依赖数组中的每一项
数据结构总览
javascript
// Fiber 节点
Fiber {
memoizedState: Hook, // Hook 链表头
updateQueue: { // Effect 环形链表
lastEffect: Effect
},
flags: PassiveEffect // 标记位
}
// Hook 对象
Hook {
memoizedState: Effect, // 存储 Effect 对象
next: Hook // 下一个 Hook
}
// Effect 对象
Effect {
tag: HookHasEffect | HookPassive,
create: () => void | (() => void),
destroy: (() => void) | undefined,
deps: Array<any> | null,
next: Effect // 环形链表
}
执行顺序
markdown
Render 阶段
└─ mountEffect / updateEffect
└─ 创建 Effect 对象
└─ 标记 Fiber.flags
Commit 阶段
├─ Before Mutation
├─ Mutation(DOM 更新)
├─ Layout
└─ 调度 useEffect(异步)
异步执行
├─ commitPassiveUnmountEffects(清理)
└─ commitPassiveMountEffects(执行)
最佳实践
✅ 推荐:
- 始终提供正确的依赖数组
- 拆分独立的 Effect
- 清理副作用(定时器、订阅、请求)
- 使用 ESLint 规则
react-hooks/exhaustive-deps
❌ 避免:
- 在 Effect 中直接修改依赖的状态
- 依赖引用类型而不缓存
- 忽略 cleanup 函数
- 在 Effect 中执行同步的 DOM 操作(用 useLayoutEffect)
参考资源
文章版本: v1.0
最后更新: 2026-03-12
适用 React 版本: 18.x