非常好的问题 👍。 在前端面试中,“useEffect 实现原理” 是 React Hooks 高频面试题之一,尤其考察候选人对 Fiber 架构、更新流程、Hook 链表机制 的理解。下面我会分成两部分讲解: 1️⃣ 面试中该怎么说(简洁高分版) 2️⃣ 深入源码级理解(适合加分或面试官追问)
⸻
🎯 一、面试高分回答模板(3分钟答法)
面试官问:“你能讲讲 useEffect 的实现原理吗?”
你可以这样答👇
在 React 中,useEffect 是一个处理副作用的 Hook。 它的实现依赖于 Fiber 架构 和 Hook 链表机制。
每个函数组件对应一个 Fiber 节点,在这个 Fiber 上会维护一个 memoizedState 链表来存放所有 Hook 的状态。 当组件首次渲染时,React 会通过 mountEffect 创建一个 effect 对象,并把它挂到当前 Fiber 的 effect list 上。 在 commit 阶段,React 会统一遍历 effect list,根据 effect.tag 来执行不同类型的副作用(比如 layoutEffect、passiveEffect)。
对于 useEffect 来说,它属于异步执行的 被动副作用(passive effect),会在浏览器完成布局和绘制之后,通过 scheduler 调度异步执行,保证不会阻塞渲染。
而当依赖项变化时,React 会比较新旧依赖数组,如果有变化,就会在下一次 commit 阶段重新执行 effect,并在执行前调用上一次返回的清理函数(cleanup)。
📌 一句总结:
useEffect 在 Fiber commit 阶段异步执行,通过依赖数组控制执行时机,并在更新时执行清理函数以保证副作用正确性。
⸻
🧩 二、源码层面实现原理(React Fiber 视角)
要理解实现原理,我们要从 Fiber 的两个阶段说起:
1️⃣ Render 阶段(创建 Hook 链)
React 每次渲染函数组件时,会调用一系列 Hook,比如:
useEffect(() => {...}, [deps]);
React 内部会调用:
mountEffect(create, deps);
核心逻辑(在 ReactFiberHooks.js):
function mountEffect(create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 创建 effect 对象
const effect = {
tag: PassiveEffect, // 标识这是 useEffect
create, // 副作用函数
destroy: undefined, // 清理函数
deps: nextDeps, // 依赖项
next: null
};
// 挂载到当前 fiber 的 effect list
pushEffect(PassiveEffect, create, undefined, nextDeps);
hook.memoizedState = effect;
}
👉 关键点: • 每个 Hook 都会对应一个 hook 对象,形成一个链表。 • 对于 useEffect,会创建一个 effect 对象挂在 Fiber 上,等待 commit 阶段统一处理。
⸻
2️⃣ Commit 阶段(执行副作用)
Fiber 的 commit 阶段分为三步:
before mutation → mutation → layout → passive
useEffect 的执行在 passive 阶段,由 commitPassiveEffects 完成
function commitPassiveMountEffects(root, fiber) {
const updateQueue = fiber.updateQueue;
const lastEffect = updateQueue?.lastEffect;
if (lastEffect !== null) {
let effect = lastEffect.next;
do {
if (effect.tag & PassiveEffect) {
// 执行上一次的 cleanup
if (typeof effect.destroy === 'function') {
effect.destroy();
}
// 执行新的 effect
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== lastEffect.next);
}
}
💡 总结这个阶段做的事: • 执行上次的清理函数(destroy)。 • 执行新的副作用函数(create)。 • 将返回的清理函数重新保存,用于下次执行前清理。
⸻
3️⃣ 依赖变化检测
React 会保存上一次的依赖 prevDeps,每次更新时通过 areHookInputsEqual(nextDeps, prevDeps) 判断是否执行 effect:
if (areHookInputsEqual(nextDeps, prevDeps)) { // 依赖未变,不重新执行 return; } else { // 依赖变化,创建新的 effect pushEffect(PassiveEffect, create, destroy, nextDeps); }
⸻
🧠 三、面试加分点
你可以在最后补充一句(非常加分):
在 React 18 之后,useEffect 的调度由 scheduler 控制,被放入“被动副作用队列(passiveEffectQueue)”,由微任务统一异步执行,这样可以避免阻塞渲染,提高性能。 而 useLayoutEffect 则在 DOM 变更之后、浏览器绘制之前同步执行。
⸻
✅ 面试总结版回答结构(记住这段)
useEffect 的实现基于 Fiber 架构。 在 render 阶段,React 会创建一个 effect 对象挂在 Fiber 上。 在 commit 阶段,React 会遍历 effect list,异步执行所有带有 PassiveEffect 标识的副作用函数。 如果有依赖数组,会比较新旧依赖,只在变化时执行。 执行前会调用上一次的清理函数,以保持副作用的正确性。 整个过程通过调度器异步执行,保证渲染不会被阻塞。