今天你深入研究过React中useEffect的源码实现机制了吗?
- 内容介绍
- 文章标签
- 相关问答
React useEffect 源码详细说明
前言
是 React Hooks 中最常用的 API 之一。它让函数组件能够执行副作用操作,例如网络请求、订阅或手动操作 DOM。很多开发者在实际使用中会碰到「副作用不触发」或「清理函数忘记」等痛点。这篇文章从源码层面剖析其实现机制,并结合常见问题提供实战解决思路。
一、基础概念回顾 & 常见痛点定位 🎯
// 最简化的 useEffect 用法
useEffect => {
// 副作用逻辑
return => {
// 清理函数
}
},);
- EFFECT 函数: 包含副作用逻辑的回调;如果返回一个函数,则该函数会在下次 EFFECT 执行前或组件卸载时被调用,用于资源清理。
-
DEPENDENCY ARRAY: 决定 EFFECT 什么时间重新运行。其实,空数组只跑一次未传递则每次渲染后都会跑。
- ❗ 痛点 #1 "副作用不触发" → 多半因为依赖写漏或误用了空数组;或者把不可变对象直接放进 deps 导致无限循环。
- ❗ 痛点 #2 "忘记返回清理函数" → 常导致内存泄漏或旧数据残留。
- ❗ 痛点 #3 "异步请求出现 race condition" → 当多个请求并发且先后顺序不确定时需要在 cleanup 中标记已取消。 <\/ul> <\/li> <\/ul>
- MOUNT 阶段 -> mountEffect / mountLayoutEffect )
- UPDATE 阶段 -> updateEffect / updateLayoutEffect ) <\/ol>
- 延迟到 Commit 阶段统一处理——可以确保所有 DOM 更新已经完成,避免 UI 与副作用产生竞争条件。话说回来,
- 批量合并——一次渲染可能产生多个 Effect。把它们集中起来一次性遍历,可显著提高性能。
- 提供安全退出口——若某个 Fiber 在提交之前被标记为中止。pending 列表会自动过滤掉对应 Effect,防止已失效的副作用仍被调用。其实,
- 使用浅比较 来判定每个元素是否相同。这是 React 为了兼容 NaN 与 -0 所做的细节处理。
- 若任意一个元素“不相等”,则认为依赖发生变化。从而触发新的 passive Effect。
- 忘记返回 cleanup → 此处不会报错。但下次 re‑run 时没有任何销毁步骤,会出现「重复订阅」等异常行为。
- 异步 race condition → 当 request A 完成后立即调用其内部 cleanup。而 request B 正在进行中,此时若 B 完成再调用自己的 cleanup,就能保证只有最新的数据写入 UI。
二、源码结构概览
React 对 实现划分为两大阶段,每个阶段都有对应的内部方法:
主要文件 & 方法入口:packages/react-reconciler/src/ReactFiberHooks.js packages/react-reconciler/src/ReactFiberWorkLoop.js <\/tt> <\/div>
三、Mount 阶段—— mountEffect 实现细节 <\/span> <\/h4>
主要入口 ——
mountPassivelyMountedSideEffectsList.add\<\/i>javascript // packages/react-reconciler/src/ReactFiberHooks.js function mountPassivelyMountedSideEffectsList { const hook = mountWorkInProgressHook;// 创建新的 Hook 节点并链接到当前 fiber 上 const depArray = dependencies ===
// 构造 Effect 对象 const effect = { tag这方面。Passive,// 标识为 passive create,destroy: undefined,deps: depArray,next: null,};
// 把它加入全局 pending 列表,稍后由 flushPassiveEffects 完成真正执行 pushPendingPassiveEffects;
hook.memoizedState = effect;}
为什么要把 Effect 放进 pending 列表?
小结 – Mount 难点映射使用者痛点
使用者痛点 源码对应位置 副作用根本未运行 检查是否成功进入 pushPendingPassiveEffects第一次渲染就出现内存泄漏 确认创建阶段有没有正确返回 cleanup 函数 初始渲染多余运行 若想“一次”运行。请确保第二个参数是空数组
主要实现片段
javascript function updatePassivelyMountedSideEffectsList { const hook = updateWorkInProgressHook;const prevEff = hook.memoizedState;const depArray = dependencies ===
// ---------- 判断依赖是否变化 ---------- // if ) { // 👉 没变 → 跳过调度。仅保留上一次注册好的 destroy,以备下次使用 hook.memoizedState = prevEff;return,}
// ---------- 有变 → 重建 Effect 并加入 pending 列表 ---------- // const newEff = { tag : Passive。create,destroy : undefined,deps : depArray,next : null,};pushPendingPassiveEffects;hook.memoizedState = newEff;}
主要比较工具 ——
areHookInputsEqual\<\/i>javascript function areHookInputsEqual { if return false;for { if ) return false;} return true;按理说,}对应使用者痛点
痛点 如何通过源码定位解决 无限循环 查看 areHookInputsEqual\<\/i>返回 false 的原因;说起来,通常是对象字面量或 inline function 每次生成新引用导致一直“变化”。错误跳过更新 确认上一轮保存的 prevEff.deps<\/i>` 与当前传入的一致;若手动修改了引用却忘记更新,则会错误地跳过效果。
五、Commit 阶段 —— 真正执行与 Cleanup
完整流程
javascript // packages/react-reconciler/src/ReactFiberWorkLoop.js function flushPassiveEffects { const effects = pendingPassiveEffects;pendingPassiveEffects.length = 0;
for { const eff = effects;
// Step①:如果上一次已有 cleanup,需要先执行 if { try { eff.destroy;不过,} catch{ captureCommitPhaseErrorOnRoot;} }
// Step②:执行最新创建的副作用,并把返回值保存为下一轮 cleanup try { eff.destroy = eff.create;} catch{ captureCommitPhaseErrorOnRoot;} } }
为什么要先 destroy 再 create?
1️⃣ 防止旧资源使用情况导致冲突,例如旧订阅仍然活跃而新的订阅又开始监听同一事件;2️⃣ 保证每次更新都是“干净”的状态,使得开发者可以放心假设 “上一次 clean-up 已经完成”。
与使用者实际场景对照
六、实战 Debug 小技巧 🎯
1️⃣ 快速定位缺失依赖
js // 在项目根目录创建一个 ESLint rule 或使用官方插件 react-hooks/exhaustive-deps当遗漏关键变量时报错,让你第一时间发现问题所在。2️⃣ 确认 Cleanup 是否被调用
js useEffect => { console.log;按理说,return => console.log;},),打开浏览器控制台,你应该看到「clean up」总是在下一次「effect run」之前打印出来。如果没有,那说明你的 dependencies 没有变化或者你根本没有返回 clean‑up 函数。3️⃣ 捕获 Race Condition
js let cancelled = false;话说回来,useEffect => { cancelled = false;fetch.n).n(data=>{ if setData;}),return => { cancelled=true };},),利用闭包中的 flag 能有效防止「已失效」的数据写入 UI。4️⃣ 源码断点法 - 打开 Chrome DevTools → Sources → 搜索关键词
"pushPendingPassiveEffects"。话说回来,样子,从而直观定位 bug 根源。---
七、面试高频速记卡 🎓
问题 答案要点 为什么 React 把 useLayoutEffect<\/i>与useEffect\<\/i>分离?Layout‑effect 在 DOM mutation 前同步运行,用于读取布局信息;按理说,passive‑effect 在 commit 后异步运行。用于非阻塞任务,如网络请求或订阅。 如何判断一个 Effect 是否需要重新调度? 看它上一轮保存的 .deps与本轮传入的一致性,通过areHookInputsEqual<\/i>` 做浅比较决定。什么情况下 flushPassiveEffects<\/i>` 会抛出错误但不会导致页面崩溃?_ 捕获错误后交给 error boundaries 或者开发环境下打印警告。生产环境默认吞掉,以免影响其它 UI 更新。 │
©2026 开源社区 · 这篇文章仅用于学习交流,如有侵权请联系删除。再看参考来源,React 官方仓库。《深入浅出 React Hooks》.
React useEffect 源码详细说明
前言
是 React Hooks 中最常用的 API 之一。它让函数组件能够执行副作用操作,例如网络请求、订阅或手动操作 DOM。很多开发者在实际使用中会碰到「副作用不触发」或「清理函数忘记」等痛点。这篇文章从源码层面剖析其实现机制,并结合常见问题提供实战解决思路。
一、基础概念回顾 & 常见痛点定位 🎯
// 最简化的 useEffect 用法
useEffect => {
// 副作用逻辑
return => {
// 清理函数
}
},);
- EFFECT 函数: 包含副作用逻辑的回调;如果返回一个函数,则该函数会在下次 EFFECT 执行前或组件卸载时被调用,用于资源清理。
-
DEPENDENCY ARRAY: 决定 EFFECT 什么时间重新运行。其实,空数组只跑一次未传递则每次渲染后都会跑。
- ❗ 痛点 #1 "副作用不触发" → 多半因为依赖写漏或误用了空数组;或者把不可变对象直接放进 deps 导致无限循环。
- ❗ 痛点 #2 "忘记返回清理函数" → 常导致内存泄漏或旧数据残留。
- ❗ 痛点 #3 "异步请求出现 race condition" → 当多个请求并发且先后顺序不确定时需要在 cleanup 中标记已取消。 <\/ul> <\/li> <\/ul>
- MOUNT 阶段 -> mountEffect / mountLayoutEffect )
- UPDATE 阶段 -> updateEffect / updateLayoutEffect ) <\/ol>
- 延迟到 Commit 阶段统一处理——可以确保所有 DOM 更新已经完成,避免 UI 与副作用产生竞争条件。话说回来,
- 批量合并——一次渲染可能产生多个 Effect。把它们集中起来一次性遍历,可显著提高性能。
- 提供安全退出口——若某个 Fiber 在提交之前被标记为中止。pending 列表会自动过滤掉对应 Effect,防止已失效的副作用仍被调用。其实,
- 使用浅比较 来判定每个元素是否相同。这是 React 为了兼容 NaN 与 -0 所做的细节处理。
- 若任意一个元素“不相等”,则认为依赖发生变化。从而触发新的 passive Effect。
- 忘记返回 cleanup → 此处不会报错。但下次 re‑run 时没有任何销毁步骤,会出现「重复订阅」等异常行为。
- 异步 race condition → 当 request A 完成后立即调用其内部 cleanup。而 request B 正在进行中,此时若 B 完成再调用自己的 cleanup,就能保证只有最新的数据写入 UI。
二、源码结构概览
React 对 实现划分为两大阶段,每个阶段都有对应的内部方法:
主要文件 & 方法入口:packages/react-reconciler/src/ReactFiberHooks.js packages/react-reconciler/src/ReactFiberWorkLoop.js <\/tt> <\/div>
三、Mount 阶段—— mountEffect 实现细节 <\/span> <\/h4>
主要入口 ——
mountPassivelyMountedSideEffectsList.add\<\/i>javascript // packages/react-reconciler/src/ReactFiberHooks.js function mountPassivelyMountedSideEffectsList { const hook = mountWorkInProgressHook;// 创建新的 Hook 节点并链接到当前 fiber 上 const depArray = dependencies ===
// 构造 Effect 对象 const effect = { tag这方面。Passive,// 标识为 passive create,destroy: undefined,deps: depArray,next: null,};
// 把它加入全局 pending 列表,稍后由 flushPassiveEffects 完成真正执行 pushPendingPassiveEffects;
hook.memoizedState = effect;}
为什么要把 Effect 放进 pending 列表?
小结 – Mount 难点映射使用者痛点
使用者痛点 源码对应位置 副作用根本未运行 检查是否成功进入 pushPendingPassiveEffects第一次渲染就出现内存泄漏 确认创建阶段有没有正确返回 cleanup 函数 初始渲染多余运行 若想“一次”运行。请确保第二个参数是空数组
主要实现片段
javascript function updatePassivelyMountedSideEffectsList { const hook = updateWorkInProgressHook;const prevEff = hook.memoizedState;const depArray = dependencies ===
// ---------- 判断依赖是否变化 ---------- // if ) { // 👉 没变 → 跳过调度。仅保留上一次注册好的 destroy,以备下次使用 hook.memoizedState = prevEff;return,}
// ---------- 有变 → 重建 Effect 并加入 pending 列表 ---------- // const newEff = { tag : Passive。create,destroy : undefined,deps : depArray,next : null,};pushPendingPassiveEffects;hook.memoizedState = newEff;}
主要比较工具 ——
areHookInputsEqual\<\/i>javascript function areHookInputsEqual { if return false;for { if ) return false;} return true;按理说,}对应使用者痛点
痛点 如何通过源码定位解决 无限循环 查看 areHookInputsEqual\<\/i>返回 false 的原因;说起来,通常是对象字面量或 inline function 每次生成新引用导致一直“变化”。错误跳过更新 确认上一轮保存的 prevEff.deps<\/i>` 与当前传入的一致;若手动修改了引用却忘记更新,则会错误地跳过效果。
五、Commit 阶段 —— 真正执行与 Cleanup
完整流程
javascript // packages/react-reconciler/src/ReactFiberWorkLoop.js function flushPassiveEffects { const effects = pendingPassiveEffects;pendingPassiveEffects.length = 0;
for { const eff = effects;
// Step①:如果上一次已有 cleanup,需要先执行 if { try { eff.destroy;不过,} catch{ captureCommitPhaseErrorOnRoot;} }
// Step②:执行最新创建的副作用,并把返回值保存为下一轮 cleanup try { eff.destroy = eff.create;} catch{ captureCommitPhaseErrorOnRoot;} } }
为什么要先 destroy 再 create?
1️⃣ 防止旧资源使用情况导致冲突,例如旧订阅仍然活跃而新的订阅又开始监听同一事件;2️⃣ 保证每次更新都是“干净”的状态,使得开发者可以放心假设 “上一次 clean-up 已经完成”。
与使用者实际场景对照
六、实战 Debug 小技巧 🎯
1️⃣ 快速定位缺失依赖
js // 在项目根目录创建一个 ESLint rule 或使用官方插件 react-hooks/exhaustive-deps当遗漏关键变量时报错,让你第一时间发现问题所在。2️⃣ 确认 Cleanup 是否被调用
js useEffect => { console.log;按理说,return => console.log;},),打开浏览器控制台,你应该看到「clean up」总是在下一次「effect run」之前打印出来。如果没有,那说明你的 dependencies 没有变化或者你根本没有返回 clean‑up 函数。3️⃣ 捕获 Race Condition
js let cancelled = false;话说回来,useEffect => { cancelled = false;fetch.n).n(data=>{ if setData;}),return => { cancelled=true };},),利用闭包中的 flag 能有效防止「已失效」的数据写入 UI。4️⃣ 源码断点法 - 打开 Chrome DevTools → Sources → 搜索关键词
"pushPendingPassiveEffects"。话说回来,样子,从而直观定位 bug 根源。---
七、面试高频速记卡 🎓
问题 答案要点 为什么 React 把 useLayoutEffect<\/i>与useEffect\<\/i>分离?Layout‑effect 在 DOM mutation 前同步运行,用于读取布局信息;按理说,passive‑effect 在 commit 后异步运行。用于非阻塞任务,如网络请求或订阅。 如何判断一个 Effect 是否需要重新调度? 看它上一轮保存的 .deps与本轮传入的一致性,通过areHookInputsEqual<\/i>` 做浅比较决定。什么情况下 flushPassiveEffects<\/i>` 会抛出错误但不会导致页面崩溃?_ 捕获错误后交给 error boundaries 或者开发环境下打印警告。生产环境默认吞掉,以免影响其它 UI 更新。 │
©2026 开源社区 · 这篇文章仅用于学习交流,如有侵权请联系删除。再看参考来源,React 官方仓库。《深入浅出 React Hooks》.

