【React源码09】深入学习React 源码实现——Fiber架构副作用收集Effect List

深入学习React源码实现之Fiber副作用收集

一、Fiber副作用收集机制历史介绍

在 React 的 Stack Reconciler 时代(React 15 及之前),更新是同步的,组件树递归渲染过程中直接进行 DOM 更新操作。这种方式虽然简单,但缺乏中断和恢复能力,无法应对复杂应用中对性能和响应性的需求。

随着 React 16 引入 Fiber 架构,副作用收集成为协调器的重要组成部分。Fiber 将整个更新过程拆分为多个阶段:render phasecommit phase,其中:

  • render phase 负责构建新的 Fiber 树并标记需要执行的副作用。
  • commit phase 负责将这些副作用批量执行到真实 DOM 上。

这种设计使得 React 可以更灵活地调度任务,并支持并发模式(Concurrent Mode)下的中断与优先级调度。

相关源码文件


二、算法设计思路和详细步骤

1. 副作用收集目标

副作用(Effect)是指那些在渲染后需要执行的操作,主要包括:

  • Placement(插入)
  • Update(更新)
  • Deletion(删除)

React 在 completeUnitOfWork() 阶段通过 completeWork() 方法标记这些副作用,并将其添加到父节点的 effectList 中,最终在 commitRoot() 阶段统一执行。

2. 算法流程图解

beginWork()
   ↓
completeWork() ——> 收集当前节点副作用
   ↓
appendAllChildren() / reconcileChildren()
   ↓
递归完成所有子节点的 completeWork()
   ↓
将当前节点 effect 添加到父节点 effectList 中
   ↓
commitRoot()
   ↓
遍历 effectList 执行副作用

3. 关键步骤详解

步骤 1:completeWork() 收集副作用

这段代码是 React Fiber 架构中 completeWork 函数的简化实现,负责完成 Fiber 节点的处理工作。下面是对代码的详细注释:

/**
 * 完成 Fiber 节点的处理工作
 * @param {Fiber | null} current 当前 Fiber 节点(可能为 null)
 * @param {Fiber} workInProgress 工作进中的 Fiber 节点
 * @returns {Fiber | null} 返回下一个要处理的兄弟节点或 null
 */
function completeWork(current: Fiber | null, workInProgress: Fiber): Fiber | null {
  const newProps = workInProgress.pendingProps;
  
  // 根据 Fiber 节点的类型执行不同的处理逻辑
  switch (workInProgress.tag) {
    case HostComponent: {  // 处理 DOM 组件节点
      const type = workInProgress.type;  // DOM 标签名(如 'div', 'span' 等)
      
      if (current !== null && current.stateNode != null) {
        // 如果是更新阶段,更新 DOM 属性
        updateHostComponent(
          current, 
          workInProgress, 
          type, 
          newProps
        );
      } else {
        // 如果是初次创建,创建 DOM 实例
        const instance = createInstance(
          type, 
          newProps, 
          rootContainerInstance,  // 根容器实例
          hostContext,            // 宿主环境上下文
          internalInstanceHandle  // 内部实例句柄
        );
        
        // 附加所有子节点到父实例
        appendAllChildren(
          instance, 
          workInProgress, 
          false,  // 是否为 DOM 属性
          false   // 是否为隐藏的(如 CSS hidden)
        );
        
        // 设置 Fiber 节点的 stateNode 为创建的 DOM 实例
        workInProgress.stateNode = instance;
        
        // 初始化子节点的属性(如自动聚焦等)
        finalizeInitialChildren(
          instance, 
          type, 
          newProps, 
          rootContainerInstance, 
          hostContext
        );
      }
      break;
    }
    case HostText: {  // 处理文本节点
      const newText = newProps;  // 文本内容就是 props
      
      if (current && current.stateNode !== null) {
        // 如果是更新阶段,检查文本内容是否变化
        const oldText = current.memoizedProps;
        if (oldText !== newText) {
          markWorkInProgressReceivedUpdate();  // 标记有更新需要处理
        }
      } else {
        // 如果是初次创建,创建文本实例
        const instance = createTextInstance(
          newText, 
          rootContainerInstance, 
          hostContext, 
          internalInstanceHandle
        );
        // 设置 Fiber 节点的 stateNode 为创建的文本实例
        workInProgress.stateNode = instance;
      }
      break;
    }
    default:
      // 其他类型的节点不做特殊处理
      break;
  }

  // 处理完当前节点后,决定下一个要处理的节点
  if (workInProgress.child !== null) {
    // 如果有子节点,继续向下处理子节点
    return workInProgress.child;
  } else {
    // 如果没有子节点,向上查找兄弟节点
    let node: Fiber | null = workInProgress;
    while (node !== null) {
      if (node.sibling !== null) {
        // 找到兄弟节点,返回它
        return node.sibling;
      }
      if (node.return === null) {
        // 如果没有父节点,返回 null(到达根节点)
        return null;
      }
      // 继续向上查找
      node = node.return;
    }
    return null;
  }
}

关键点说明:

  1. 节点类型处理

    • HostComponent:处理 DOM 元素节点
    • HostText:处理文本节点
    • 其他类型节点不做特殊处理
  2. DOM 组件处理流程

    • 更新阶段:调用 updateHostComponent 更新 DOM 属性
    • 创建阶段:
      • 调用 createInstance 创建 DOM 实例
      • 调用 appendAllChildren 附加所有子节点
      • 调用 finalizeInitialChildren 初始化子节点属性
  3. 文本节点处理流程

    • 更新阶段:检查文本内容是否变化
    • 创建阶段:调用 createTextInstance 创建文本节点
  4. 遍历逻辑

    • 深度优先遍历 Fiber 树
    • 优先处理子节点
    • 没有子节点时查找兄弟节点
    • 没有兄弟节点时向上回溯
  5. 返回值

    • 返回下一个要处理的节点(可能是子节点或兄弟节点)
    • 到达根节点时返回 null

这个函数是 Fiber 架构中协调(reconciliation)过程的重要组成部分,负责将 Fiber 树转换为实际的 DOM 结构。


步骤 2:提交副作用(commitRoot)

这段代码是 React Fiber 架构中 commitRoot 函数的简化实现,负责将工作完成(finished)的 Fiber 树提交到 DOM。下面是对代码的详细注释:

/**
 * 提交根节点及其副作用到 DOM
 * @param {FiberRoot} root Fiber 根节点
 */
function commitRoot(root) {
  // 获取已完成的工作(即 workInProgress 树的根节点)
  const finishedWork = root.finishedWork;
  // 获取已完成的工作对应的更新优先级(lane)
  const lanes = root.finishedLanes;

  // 重置 finishedWork 和 finishedLanes,准备下一次更新
  root.finishedWork = null;
  root.finishedLanes = NoLanes;

  // 提交所有副作用(如 DOM 插入、更新、删除等)
  commitMutationEffects(finishedWork, root);

  // 将当前指针(current)切换到已完成的工作(即 workInProgress 树)
  root.current = finishedWork;
}

关键点说明:

  1. 参数

    • root:Fiber 根节点对象,包含整个 Fiber 树的信息
  2. 获取已完成的工作

    • finishedWork:表示已经完成协调(reconciliation)过程的 Fiber 树根节点
    • lanes:表示已完成工作的更新优先级(在 React 的并发模式下使用)
  3. 重置状态

    • finishedWorkfinishedLanes 重置为初始状态,为下一次更新做准备
  4. 提交副作用

    • commitMutationEffects:执行所有挂起的副作用(如 DOM 操作)
      • 这包括实际的 DOM 插入、更新和删除操作
      • 这个函数会遍历整个 Fiber 树,处理所有标记了副作用的节点
  5. 切换当前指针

    • root.current 指向 finishedWork,表示现在 finishedWork 树成为新的当前树
    • 这意味着下一次渲染将从这棵树开始
  6. 副作用类型

    • commitMutationEffects 中会处理多种副作用,如:
      • Placement:插入新节点
      • Update:更新现有节点
      • Deletion:删除节点

这个函数是 React 渲染过程的最后阶段,负责将协调阶段产生的变更实际应用到 DOM 上。在 React 的并发模式下,这个阶段是不可中断的,以确保 UI 的一致性。


步骤 3:执行副作用(commitMutationEffects)

这段代码是 React Fiber 架构中 commitMutationEffects 函数的简化实现,负责处理所有挂起的副作用(如 DOM 插入、更新和删除)。下面是对代码的详细注释:

/**
 * 提交所有挂起的副作用(如 DOM 插入、更新和删除)
 * @param {Fiber} finishedWork 已完成协调的 Fiber 节点
 * @param {FiberRoot} root Fiber 根节点
 */
function commitMutationEffects(finishedWork: Fiber, root: FiberRoot) {
  // 从 finishedWork 的第一个副作用节点开始遍历
  let nextEffect: Fiber | null = finishedWork.firstEffect;
  
  // 遍历所有带有副作用的 Fiber 节点
  while (nextEffect !== null) {
    // 获取当前节点的副作用标记
    const effectTag = nextEffect.effectTag;
    
    // 处理 Placement 副作用(插入新节点)
    if ((effectTag & Placement) !== NoEffect) {
      commitPlacement(nextEffect);  // 执行实际的 DOM 插入操作
      nextEffect.effectTag &= ~Placement;  // 清除 Placement 标记
    }
    
    // 处理 Update 副作用(更新现有节点)
    if ((effectTag & Update) !== NoEffect) {
      commitWork(nextEffect, nextEffect.alternate);  // 执行实际的 DOM 更新操作
      nextEffect.effectTag &= ~Update;  // 清除 Update 标记
    }
    
    // 处理 Deletion 副作用(删除节点)
    if ((effectTag & Deletion) !== NoEffect) {
      commitDeletion(nextEffect);  // 执行实际的 DOM 删除操作
      nextEffect.effectTag &= ~Deletion;  // 清除 Deletion 标记
    }
    
    // 移动到下一个带有副作用的节点
    nextEffect = nextEffect.nextEffect;
  }
}

关键点说明:

  1. 副作用遍历

    • finishedWork.firstEffect 开始遍历所有带有副作用的 Fiber 节点
    • 使用 nextEffect 指针在副作用链表中移动
  2. 副作用类型处理

    • Placement:处理 DOM 插入操作
      • 调用 commitPlacement 执行实际的插入
      • 清除 Placement 标记
    • Update:处理 DOM 更新操作
      • 调用 commitWork 执行实际的更新
      • 清除 Update 标记
    • Deletion:处理 DOM 删除操作
      • 调用 commitDeletion 执行实际的删除
      • 清除 Deletion 标记
  3. 副作用标记清除

    • 每种副作用处理完成后,使用位操作清除对应的标记
    • 这是为了确保副作用只被处理一次
  4. 副作用链表

    • Fiber 节点通过 nextEffect 指针形成一个链表
    • 这个链表是在协调(reconciliation)阶段构建的
  5. 不可中断性

    • 这个阶段在 React 的并发模式下是不可中断的
    • 确保所有副作用要么全部提交,要么全部不提交

这个函数是 React 渲染过程的最后阶段之一,负责将协调阶段产生的变更实际应用到 DOM 上。在 React 的并发模式下,这个阶段是不可中断的,以确保 UI 的一致性。


三、完整代码实现和注释

以下是一个简化版的副作用收集模拟实现:

// 模拟 Fiber 对象结构
class Fiber {
  constructor(tag, key, elementType, pendingProps, mode) {
    this.tag = tag;           // 类型:FunctionComponent, ClassComponent 等
    this.key = key;           // 用于列表比对
    this.elementType = elementType;// 元素类型(如函数组件、类组件等)
    this.pendingProps = pendingProps;// 待处理的 props
    this.mode = mode;         // 渲染模式(如 ConcurrentMode)

    this.alternate = null;    // 指向另一个树的节点
    this.child = null;        // 第一个子节点
    this.sibling = null;      // 下一个兄弟节点
    this.return = null;       // 父节点
    this.stateNode = null;    // DOM 或组件实例
    this.memoizedState = null; // 状态快照
    this.updateQueue = null;   // 更新队列
    this.effectTag = 'NoEffect';// 副作用标记(如 Placement, Update, Deletion)
    this.nextEffect = null;    // 副作用链表中的下一个节点
    this.firstEffect = null;   // 副作用链表中的第一个节点 
    this.lastEffect = null;    // 副作用链表中的最后一个节点
  }

  addEffect(effectTag) {
    const effect = { effectTag, fiber: this };
    if (!this.parent) return;
    if (!this.parent.firstEffect) {
      this.parent.firstEffect = effect;
      this.parent.lastEffect = effect;
    } else {
      this.parent.lastEffect.nextEffect = effect;
      this.parent.lastEffect = effect;
    }
  }
}

// 模拟 beginWork
function beginWork(current, workInProgress) {
  console.log('Begin work on:', workInProgress.key || 'root');
  return workInProgress.child;
}

// 模拟 completeWork 并收集副作用
function completeWork(workInProgress) {
  console.log('Complete work on:', workInProgress.key || 'root');

  // 模拟不同类型的节点处理
  switch (workInProgress.tag) {
    case 'HostComponent':
      if (!workInProgress.stateNode) {
        // 新增节点
        workInProgress.effectTag = 'Placement';
        workInProgress.addEffect(workInProgress.effectTag);
      } else {
        // 更新节点
        workInProgress.effectTag = 'Update';
        workInProgress.addEffect(workInProgress.effectTag);
      }
      break;
    case 'HostText':
      if (!workInProgress.stateNode) {
        workInProgress.effectTag = 'Placement';
        workInProgress.addEffect(workInProgress.effectTag);
      }
      break;
    default:
      break;
  }
}

// 模拟 commit 阶段
function commitMutationEffects(finishedWork) {
  console.log('Committing effects...');
  let effect = finishedWork.firstEffect;
  while (effect) {
    switch (effect.effectTag) {
      case 'Placement':
        console.log(`Place node: ${effect.fiber.key}`);
        break;
      case 'Update':
        console.log(`Update node: ${effect.fiber.key}`);
        break;
      case 'Deletion':
        console.log(`Delete node: ${effect.fiber.key}`);
        break;
      default:
        break;
    }
    effect = effect.nextEffect;
  }
}

// 主工作循环
function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  const next = beginWork(current, unitOfWork);
  if (next === null) {
    completeWork(unitOfWork);
  }
  return next;
}

function workLoop(isYieldy) {
  let unitOfWork = workInProgressFiber;
  while (unitOfWork !== null) {
    unitOfWork = performUnitOfWork(unitOfWork);
  }
  if (!isYieldy && unitOfWork === null) {
    commitMutationEffects(workInProgressFiber);
  }
}

let currentFiber = new Fiber('HostRoot', null, null, null, 'ConcurrentMode');
let workInProgressFiber = null;

function createWorkInProgressFromCurrent(current) {
  if (!current.alternate) {
    workInProgressFiber = new Fiber(
      current.tag,
      current.key,
      current.elementType,
      current.pendingProps,
      current.mode
    );
    workInProgressFiber.alternate = current;
    current.alternate = workInProgressFiber;
  } else {
    workInProgressFiber = current.alternate;
    workInProgressFiber.pendingProps = current.pendingProps;
    workInProgressFiber.effectTag = 'NoEffect';
  }
  return workInProgressFiber;
}

function scheduleUpdate() {
  workInProgressFiber = createWorkInProgressFromCurrent(currentFiber);
  workLoop(false);
}

scheduleUpdate();

四、设计模式分析

设计模式应用场景
观察者模式effectTag 表示要执行的副作用,由 completeWork 观察并收集
链表模式firstEffectlastEffect 形成副作用链表,便于 commit 阶段遍历
策略模式不同类型 Fiber(如 HostComponent、FunctionComponent)使用不同副作用处理策略
模板方法模式performUnitOfWork() 提供通用流程,beginWorkcompleteWork 是具体实现

五、10大Fiber副作用收集高频面试题

  1. 什么是 React 的副作用?常见的副作用有哪些?

    • 副作用是指在 render 阶段完成后需要执行的 DOM 操作,如 Placement, Update, Deletion
  2. 副作用是如何被收集的?在哪里被记录?

    • completeWork() 阶段通过 effectTag 标记,并加入到父节点的 firstEffect 链表中。
  3. 为什么要在 render 阶段收集副作用,而不是直接操作 DOM?

    • 为了支持异步渲染和中断恢复,避免中间状态暴露给用户。
  4. effectTag 的含义是什么?它是如何表示多个副作用的?

    • 使用位掩码(bitmask)方式存储多个副作用,如 Placement | Update
  5. 副作用是如何执行的?执行顺序是怎样的?

    • commitRoot() 中遍历 effectList,按照深度优先顺序执行副作用。
  6. effectList 是如何构建的?父子节点之间如何传递副作用?

    • 子节点将副作用添加到父节点的 effectList 中,形成一个单向链表。
  7. React 如何处理嵌套组件的副作用?

    • 递归调用 completeWork() 处理每个节点,确保所有子节点的副作用都被正确收集。
  8. 为什么不能在 render 阶段直接修改 DOM?这样做会带来什么问题?

    • 同步修改 DOM 会导致 UI 闪烁;异步渲染时可能因中断导致不一致状态。
  9. React 如何优化副作用的执行效率?

    • 批量执行副作用、使用位运算快速判断副作用类型、减少不必要的 DOM 操作。
  10. 在 Concurrent Mode 下,副作用是否有可能被多次执行?为什么?

    • 是的,因为高优先级更新可能中断低优先级更新,导致部分副作用未提交而重新执行。

总结

Fiber 副作用收集机制是 React 实现高效、可中断、可恢复更新的关键部分。它通过 effectTageffectList 的设计,在 render phase 安全地收集副作用,并在 commit phase 统一执行,保证了 DOM 更新的一致性和性能。理解这一机制有助于深入掌握 React 的内部运行原理,为性能优化和调试提供理论支撑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值