专业编程教程与实战项目分享平台

网站首页 > 技术文章 正文

前端跳槽突围课:React18底层源码深入剖析(21章完整版)

ins518 2025-05-23 17:41:10 技术文章 1 ℃ 0 评论

获课:keyouit.xyz/5247/

在 React 18 中,组件卸载时确实会触发 useEffect 的清理函数。这一行为是 React 副作用机制的核心设计之一,其实现涉及 effect 链表管理、依赖追踪以及调度系统的协作。以下从源码级视角深入解析其机制:


一、useEffect清理函数的触发时机

  1. 组件卸载时
    当组件从 DOM 中移除(如父组件更新导致子组件销毁、路由切换等),React 会调用 commitHookEffectListUnmount 函数,遍历当前组件所有 effect 链表,并依次执行其清理函数(destroy 函数)。
  2. 依赖项变更导致的重新执行前
    若组件未卸载,但 useEffect 的依赖项发生变化,React 会先执行旧 effect 的清理函数,再执行新 effect 的挂载逻辑(通过 commitHookEffectListMount)。

二、源码级实现细节

1.effect链表的结构

React 通过 Fiber 节点维护 effect 链表,每个 effect 对象包含以下关键字段:

typescript


interface Effect {


tag: HookType; // 标识是 useEffect、useLayoutEffect 等


create: () => (() => void) | void; // 副作用函数


destroy: (() => void) | void; // 清理函数


deps: DependencyList | null; // 依赖项数组


next: Effect | null; // 指向链表中的下一个 effect


}

2. 清理函数的注册与执行

  • 注册阶段
    在 renderWithHooks 函数中,React 根据 useEffect 的参数生成 effect 对象,并将其挂载到当前组件的 Fiber 节点的 updateQueue 中。
  • 提交阶段(Commit Phase)
    在 commitRootImpl 函数中,React 根据操作类型(挂载/更新/卸载)调用不同的处理函数:
    • 卸载:commitHookEffectListUnmount
      遍历 Fiber 节点的 effect 链表,依次执行每个 effect 的 destroy 函数。
    • 挂载/更新:commitHookEffectListMount
      执行 create 函数生成新的副作用,并保存其返回的清理函数到 destroy 字段。

3. 依赖项追踪与比较

React 通过 areHookInputsEqual 函数比较新旧依赖项数组,决定是否需要重新执行副作用:

typescript


function areHookInputsEqual(nextDeps: DependencyList, prevDeps: DependencyList | null) {


if (prevDeps === null) {


return false;


}


for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {


if (Object.is(nextDeps[i], prevDeps[i])) {


continue;


}


return false;


}


return true;


}


三、关键源码路径分析

  1. 卸载时的清理函数调用
    在 commitHookEffectListUnmount 中:
  2. typescript
  3. function commitHookEffectListUnmount(
  4. tag: number,
  5. finishWork: Fiber,
  6. nearestMountedAncestor: Fiber | null,
  7. ) {
  8. const updateQueue: FunctionComponentUpdateQueue | null = (finishWork.updateQueue: any);
  9. if (updateQueue !== null) {
  10. const lastEffect = updateQueue.lastEffect;
  11. if (lastEffect !== null) {
  12. let firstEffect = lastEffect.next;
  13. let effect = firstEffect;
  14. do {
  15. if ((effect.tag & tag) === tag) {
  16. // 执行清理函数
  17. const destroy = effect.destroy;
  18. effect.destroy = undefined;
  19. if (destroy !== undefined) {
  20. destroy();
  21. }
  22. }
  23. effect = effect.next;
  24. } while (effect !== firstEffect);
  25. }
  26. }
  27. }
  28. 依赖项变更时的清理逻辑
    在 commitHookEffectListMount 中:
  29. typescript
  30. function commitHookEffectListMount(
  31. tag: number,
  32. finishWork: Fiber,
  33. nearestMountedAncestor: Fiber | null,
  34. ) {
  35. // ... 省略其他逻辑
  36. const create = effect.create;
  37. effect.destroy = create(); // 保存清理函数
  38. }

四、为什么需要清理函数?

  1. 资源释放
    例如取消定时器、网络请求、事件监听等,避免内存泄漏。
  2. 状态同步
    如第三方库的初始化/销毁(如 echarts 实例的销毁)。
  3. 副作用隔离
    确保副作用仅在组件挂载期间生效。

五、示例验证

jsx


import { useEffect } from 'react';




function Example() {


useEffect(() => {


const timer = setInterval(() => console.log('Tick'), 1000);


return () => {


console.log('Cleanup');


clearInterval(timer);


};


}, []);




return <div>Example</div>;


}




// 卸载时输出: "Cleanup"


六、总结

  • 组件卸载时,React 通过 commitHookEffectListUnmount 遍历 effect 链表并执行清理函数。
  • 依赖项变更时,React 先执行旧 effect 的清理函数,再执行新 effect 的挂载逻辑。
  • 这一机制由 Fiber 节点、updateQueue 和调度系统协作完成,确保副作用的生命周期与组件严格同步。

通过理解 effect 链表的管理和依赖追踪的实现,可以更高效地编写副作用逻辑,避免潜在的资源泄漏问题。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表