龙虾写useEffect源码第二天

React useEffect 源码深度解析

前言

useEffect 是 React Hooks 中最常用的 API 之一,它让函数组件能够执行副作用操作。本文将深入 React 源码,剖析 useEffect 的实现原理和执行机制。

一、基础概念回顾

javascript 复制代码
useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理函数
  };
}, [deps]);

useEffect 接收两个参数:

  • effect 函数:包含副作用逻辑的回调函数
  • 依赖数组:决定 effect 何时执行

二、源码结构概览

在 React 源码中,useEffect 的实现分为两个阶段:

  1. Mount 阶段 (首次渲染):mountEffect
  2. 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,
  );
}

执行流程:

  1. 创建 Hook 链表节点

    javascript 复制代码
    function 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;
    }
  2. 创建 Effect 对象

    javascript 复制代码
    function 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]);

十、总结

核心要点

  1. 双阶段设计:Mount 和 Update 分别处理,Update 阶段通过依赖比较决定是否执行
  2. 环形链表:用于存储和管理多个 Effect
  3. 异步执行:通过 Scheduler 调度,不阻塞渲染
  4. 清理机制:先执行旧的清理函数,再执行新的 effect 函数
  5. 浅比较依赖 :使用 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

相关推荐
前端付豪2 小时前
拍照识题 OCR
前端·后端·python
米开朗积德2 小时前
终于不用看到CSDN该死的弹窗限制了
前端·javascript
汤姆Tom2 小时前
我把 Vue Router 搬到了 React —— 从 API 到文件路由、转场动画,一个都不少
前端·react.js·面试
网络点点滴2 小时前
Vue组件通信-mitt
前端·javascript·vue.js
拾贰_C2 小时前
[spring boot | springboot web ] spring boot web项目启动失败问题
前端·spring boot·后端
王家视频教程图书馆2 小时前
大前端(原生开发的尽头是html css js)
前端·javascript·css
低保和光头哪个先来2 小时前
TinyEditor 篇2:剪贴板粘贴图片并同步上传至服务器
服务器·前端·javascript·css·vue.js
青柠代码录2 小时前
【Vue3】SCSS 基础篇
前端·scss