当前位置: 首页 > article >正文

React 源码揭秘 | 工作流程

上一篇React源码揭秘 | 启动入口-CSDN博客介绍了React启动的入口,以及一些前置知识,这篇来说一下整个React的工作循环。

上篇说到,调用createRoot(Container).render(<App/>) 函数,启动整个渲染流程,其中render函数如下:

/**
 * 更新container 需要传入
 * @param element 新的element节点
 * @param root APP根节点
 */
const updateContainer = (element: ReactElement, root: FiberRootNode) => {
  // 默认情况下 同步渲染
  scheduler.runWithPriority(PriorityLevel.IMMEDIATE_PRIORITY, () => {
    // 请求获得当前更新lane
    const lane = requestUpdateLane();
    // 获hostRootFiber
    const hostRootFiber = root.current;
    // 更新的Element元素入队
    hostRootFiber.updateQueue?.enqueue(
      new Update<ReactElement>(element, lane),
      hostRootFiber,
      lane
    );
    // scheduleUpdateOnFiber 调度更新
    scheduleUpdateOnFiber(root.current, lane);
  });
};

/** 创建根节点的入口 */
export function createRoot(container: Container) {
  // 创建FiberRootNode
  const root = createContainer(container);
  return {
    render(element: ReactElement) {
      // TODO
      // 初始化合成事件
      initEvent(container);
      // 更新contianer
      return updateContainer(element, root);
    },
  };
}

render函数做了两件事,

1. 是初始化合成事件,React通过对Container进行事件代理的方式管理事件,对事件进行一层封装,后面会详细讲。

2. 调用updateContianer函数,此函数调用scheduler.runWithPriority传入一个立刻执行优先级的同步执行回调(runWithPriority具体细节见React源码揭秘 | scheduler 并发更新原理-CSDN博客)

在这个回调中,目前主要关注的是,向hostRootFiber的updateQueue中加入了当前渲染的根ReactElement元素,你目前也不需要关注updateQueue的实现,只需要理解成一个队列即可。

最后调用scheduleUpdateOnFiber 开启调度,同时传入HostRootFiber

scheduleUpdateOnFiber - 寻找节点,标记信息

此函数用来调度某个节点的更新,你可以在react-reconciler/workLoop.ts 中找到其实现:

/** 在Fiber中调度更新 */
export function scheduleUpdateOnFiber(fiberNode: FiberNode, lane: Lane) {
  /** 先从更新的fiber节点递归到hostRootFiber
   *  这个过程中,一个目的是寻找fiberRootNode节点
   *  一个是更新沿途的 childLines
   */
  const fiberRootNode = markUpdateLaneFromFiberToRoot(fiberNode, lane);
  // 更新root的pendingLane, 更新root节点的pendingLanes 表示当前正在处理的lanes
  markRootUpdated(fiberRootNode, lane);
  // 保证根节点被正确调度
  ensureRootIsScheduled(fiberRootNode);
}

其中,入参fiberNode 可能是任意的Fiber节点,不一定是updateContainer中传入的HostRootFiber根节点,因为updateContainer需要从根节点开始更新,而更新可能发生在任意节点中,比如函数节点中调用useXXX hooks等。

scheduleUpdateOnFiber会调用markUpdateFromFiberToRoot 这个函数看名称就知道其作用是从当前的Fiber节点向上顺着return指针找到根节点,并且在沿途做一些标记,至于是什么标记可以先不用管,这个标记过程类似于一个冒泡的过程,其实现如下:

/**
 * 从当前fiberNode找到root节点 并且更新沿途fiber的childLanes
 * @param fiberNode
 */
export function markUpdateLaneFromFiberToRoot(
  fiberNode: FiberNode,
  lane: Lane
) {
  let parent = fiberNode.return; // parent表示父节点
  let node = fiberNode; // node标记当前节点
  while (parent !== null) {
    parent.childLanes = mergeLane(parent.childLanes, lane);
    const alternate = parent.alternate;
    if (alternate !== null) {
      alternate.childLanes = mergeLane(alternate.childLanes, lane);
    }
    // 处理parent节点的childLanes
    node = parent;
    parent = parent.return;
  }

  /** 检查当前是否找到了hostRootFiber */
  if (node.tag === HostRoot) {
    return node.stateNode;
  }

  return null;
}

markUpdateFromFiberToRoot 函数返回FiberRootNode节点,并且完成查找沿途的标记。

scheduleUpdateOnFiber拿到root并且完成标记之后,会调用markRootUpdate在根节点上标记信息。这个目前也不用管

最后调用ensureRootIsUpdated 来正式开启调度流程。

ensureRootIsScheduled - 开启调度

ensureRootIsScheduled函数内部包含一些优先级的调度,目前你只需要知道,这个函数调用了performWorkOnRoot 来开启渲染流程

performWorkOnRoot - 开启渲染流程

React的渲染流程包含 render和commit这两个阶段

render阶段

render和commit你可以理解为 生产 和 消费的关系。其中render就是深度优先的创建本次更新的Fiber树,并且给需要添加/更新/删除的节点打上标签

render阶段可以打断,当有更新的优先级任务进入时,会中断当前更新,当高优先级任务执行完成后重新开始render流程。 所以render流程 (对应的组件函数的执行)可以被执行多次,所以这也是为什么React要求函数组件必须是纯函数,不能有副作用,如果有副作用需要放到useEffect hooks中,比如发送请求等。 如果在useEffect之外进行副作用操作,由于render可能会被反复执行多次,会导致意料之外的后果(比如多次发送请求)。

这也是新版本react取消了componentWillUpdate 的原因,因为其在render阶段被执行,有可能会被执行多次,所以干脆取消掉这个生命周期钩子,给fiber让路。

commit阶段

commit阶段可以理解为消费render阶段打的标签的过程,其中render阶段仅仅负责打标签,不会真的操作DOM,而操作DOM是由commit阶段完成。

commit阶段和render阶段不同,commit阶段是同步且不可以被打断的,其内部还可以细分为三个阶段:

1. Muattaion阶段,负责处理节点的创建Placement 删除Delection 更新Update等

2. Layout阶段,负责处理节点的Ref, useLayoutEffect等

3. Passive阶段,负责处理useEfffect等副作用,此阶段会在渲染之后异步进行,不会影响组件渲染,这也是React官方推荐使用的副作用

performWorkOnRoot就是负责开启这些流程 其实现如下

/** 从root开始 处理同步任务 */
export function performSyncWorkOnRoot(root: FiberRootNode) {
  // 获取当前的优先级
  const lane = getNextLane(root);

  if (lane !== SyncLane) {
    /**
     * 这里 lane如果不是同步任务了,说明同步任务的lane已经被remove 应该执行低优先级的任务了
     *  此时应该停止执行当前任务 重新调度
     * 【实现同步任务的批处理,当第一次执行完之后 commit阶段remove SyncLane 这里就继续不下去了,
     * 后面微任务中的 performSyncWorkOnRoot都不执行了】
     */
    return ensureRootIsScheduled(root);
  }

  // 开始生成fiber 关闭并发模式
  const exitStatus = renderRoot(root, lane, false);
  switch (exitStatus) {
    // 注意 同步任务一次性执行完 不存在RootInComplete中断的情况
    case RootCompleted:
      // 执行成功 设置finishedWork 和 finishedLane 并且commit
      // 设置root.finishedWork
      root.finishedWork = root.current.alternate;
      root.finishedLane = lane;
      // 设置wipRootRenderLane = NoLane;
      wipRootRenderLane = NoLane;
      commitRoot(root);
    default:
    // TODO Suspense的情况
  }
}

目前阶段 你只需要关注,先调用renderRoot 开启render阶段,再调用commitRoot开启commit阶段即可。

renderRoot - 渲染根节点

renderRoot函数主要做两件事情

1. 准备一个工作栈 调用prepareFreshStack 获得一个HostRootFiber根

2. 调用workLoop函数开始深度优先创建Fiber树

/**
 * 渲染root 生成fiber对象
 * @param root  当前根节点
 * @param lane  当前车道
 * @param shouldTimeSlice 是否开启并发
 */
export function renderRoot(
  root: FiberRootNode,
  lane: Lane,
  shouldTimeSlice: boolean
) {
  let workLoopRetryTimes = 0;

  if (wipRootRenderLane !== lane) {
    // 避免重新进行初始化
    /** 先进行准备初始化 */
    prepareRefreshStack(root, lane);
  }

  while (true) {
    try {
      // 开启时间片 scheduler调度
      shouldTimeSlice ? workConcurrentLoop() : workLoop();
      break;
    } catch (e) {
      /** 使用try catch保证workLoop顺利执行 多次尝试 */
      workLoopRetryTimes++;
      if (workLoopRetryTimes > 20) {
        console.warn("workLoop执行错误!", e);
        break;
      }
    }
  }

  /** 判断任务是否执行完成 如果执行完成RootCompleted 否则 返回RootInCompleted*/
  if (shouldTimeSlice && workInProgress !== null) {
    return RootInComplete;
  }

  // 任务完成
  return RootCompleted;
}

alternate-双缓冲树

React的每次渲染都会创建一棵新的Fiber树,但是由于Diff算法需要比较旧的Fiber树和当前的ReactElement信息,判断是否需要服用旧的Fiber节点。React中引入双缓冲树的概念,在React的根节点FiberRootNode下维护着两颗Fiber树,其current指针指向当前已经完成渲染的HostRootFiber树根,当渲染新的更新时,会创建一个新的HostRootFiber,并且把其中的alternate互相指向对方。

此过程发生在prepareRefreshStack中,如图所示:

每次完成一次更新后,current指针会指向新生成的Fiber树,表示当前完成渲染的Fiber树,下次更新时,旧的HostRootFiber将会变成新的待生成的Fiber树根,反复切换,在此过程中,旧的节点不一定会被删除,可能会被复用,节省开销。

prepareRefreshStack & createWorkInProgress - 复用节点

prepareRefreshStack的作用是,调用createWorkInProgress创建/复用获得新的HostRootFiber节点,并且将其赋给workInPregress

/**
 * prepareFreshStack 这个函数的命名可能会让人觉得它与“刷新(refresh)”相关,
 * 但它的作用实际上是为了 准备一个新的工作栈,而不是刷新。
 * @param root
 * @param lane 当前车道
 */
function prepareRefreshStack(root: FiberRootNode, lane: Lane) {
  // 重新赋finishedWork
  root.finishedWork = null;
  root.finishedLane = NoLane;
  // 设置当前的运行任务lane
  wipRootRenderLane = lane;
  /** 给workInProgress赋值 */
  /** 这里在首次进入的时候 会创建一个新的hostRootFiber
   * 在react中存在两棵fiber树,两个hostRootFiber根节点 用alternate链接,成为双缓存
   */

  workInProgress = createWorkInProgress(root.current, {});
}

 createWorkInProgress函数的作用是,创建/复用就节点的方式获取当前待处理的workinprogress节点,其实现如下:

/** 根据现有的Fiber节点,创建更新的Fiber节点
 * 如果当前Fiber节点存在alternate 复用
 * 弱不存在,创建新的FiberNode
 * 将current的内容拷贝过来 包含lane memorizedState/props child 等
 *
 * 在Fiber节点内容可以复用的情况调用,新的fiber节点的 tag type stateNode 等会复用 | props,lane flags delecation这些副作用 会重置
 */
export function createWorkInProgress(
  currentFiber: FiberNode,
  pendingProps: ReactElementProps
) {
  /** 创建wip 当前的workInProgress 先看看能不能复用.alternate */
  let wip = currentFiber.alternate;

  if (wip === null) {
    /** mount阶段,说明对面不存在alternate节点 */
    wip = new FiberNode(currentFiber.tag, pendingProps, currentFiber.key);
    /** stateNode为fiber对应的真实dom节点 */
    wip.stateNode = currentFiber.stateNode;
    /** 建立双向的alternate链接 */
    wip.alternate = currentFiber;
    currentFiber.alternate = wip;
  } else {
    /** update节点,复用 重置副作用 */
    wip.flags = NoFlags;
    wip.subTreeFlags = NoFlags;
    wip.pendingProps = pendingProps;
    wip.delections = null;
  }

  // 剩下的可以复用
  wip.key = currentFiber.key;
  wip.tag = currentFiber.tag;
  wip.type = currentFiber.type;
  // ref需要传递
  wip.ref = currentFiber.ref;
  wip.memorizedState = currentFiber.memorizedState;
  wip.memorizedProps = currentFiber.memorizedProps;
  wip.updateQueue = currentFiber.updateQueue;
  //  这里需要注意,只需要复用child 可以理解为 新的节点的child指向currentFiber.child 因为后面diff的时候 只需要用的child,仅做对比,
  // 后面会创建新的fiber 此处不需要sibling和return 进行了连接 可以理解成 只复用alternate的内容 不复用其节点之间的关系
  // stateNode也不需要复用 因为alternate和currentFiber之间 如果有关联,那么type一定是相等的
  wip.child = currentFiber.child;

  /** 注意复用的时候 一定要把lane拷贝过去 */
  wip.lanes = currentFiber.lanes;
  wip.childLanes = currentFiber.childLanes;
  return wip;
}

其中,wip 表示本次新的workInProgress节点,可以通过currentFIber.alternate指针获得

注意,每个Fiber节点都有其对应的alternate 指向其对应的current旧节点!

如果不存在wip节点,说明是第一次挂载节点,那么就new FiberNode(), 其中

类型就是currentFiber的类型,因为只有类型一样才会调用createWorkInProgress函数复用,如图:

 

 如果存在wip,说明是更新阶段,直接复用当前wip作为新的Fiber节点即可,不需要重新创建,节省开销,如图

这里需要注意,复用的过程需要把child指向current节点的child,方便后期获取旧的child节点进行Diff对比。

workLoop - 开启循环

准备好workInProgress,就可以调用workLoop开启循环,深度优先构建Fiber树了

workLoop在同步模式下,(这里先不讨论异步和打断)会循环检查workInProgress,只要存在wip,就反复调用performUnitOfWork 即处理一个Fiber单元

/** 递归循环 */
function workLoop() {
  while (workInProgress) {
    performUnitOfWork(workInProgress);
  }
}

 performUnitOfWork - 递归构建FIber

performUnitOfWork就是个递归的过程,先沿着wip.child往下”递“,以此调用beginWork创建Fiber节点,直到叶子节点。

到叶子节点之后开启 "归"的过程,即completeWrok过程,此过程会创建首次挂载的节点,注意,这里只是创建DOM节点的对象,并且赋给wip.stateNode 不会进行挂载。 并且会对lanes以及flags等进行冒泡

如果归的过程中,发现遍历到的节点有sibling兄弟,则继续开始"递"的过程,直到wip为null 完成Fiber树的创建

/**
 * 处理单个fiber单元 包含 递,归 2个过程
 * @param fiber
 */
function performUnitOfWork(fiber: FiberNode) {
  // beginWork 递的过程
  const next = beginWork(fiber, wipRootRenderLane);
  // 递的过程结束,保存pendingProps
  fiber.memorizedProps = fiber.pendingProps;
  // 这里不能直接给workInProgress赋值,如果提前赋workInProgress为null 会导致递归提前结束
  // 如果next为 null 则表示已经递到叶子节点,需要开启归到过程
  if (next === null) {
    /** 开始归的过程 */
    completeUnitOfWork(fiber);
  } else {
    // 继续递
    workInProgress = next;
  }
  // 递的过程可打断,每执行完一个beginWork 切分成一个任务
  // complete归的过程不可打断,需要执行到下一个有sibling的节点/根节点 (return === null)
}

function completeUnitOfWork(fiber: FiberNode) {
  // 归
  while (fiber !== null) {
    completeWork(fiber);

    if (fiber.sibling !== null) {
      // 有子节点 修改wip 退出继续递的过程
      workInProgress = fiber.sibling;
      return;
    }

    /** 向上归 修改workInProgress */
    fiber = fiber.return;
    workInProgress = fiber;
  }
}

commit准备工作,设置finishedWork

commit阶段也是个递归的过程,在renderRoot结束之后,需要把FIberRootNode的finishedWork指针指向新创建但是还没commit的Fiber树,提供commit阶段使用

// performWorkOnRoot 
 root.finishedWork = root.current.alternate;
 commitRoot(root);

次阶段结束之后Fiber如图所示 

commit阶段主要包含,Mutation,Layout,Passive三个子阶段,其实现如下

/** commit阶段 */
export function commitRoot(root: FiberRootNode) {
  const finishedWork = root.finishedWork;

  if (finishedWork === null) return;

  const lane = root.finishedLane;
  root.finishedWork = null;
  root.finishedLane = NoLane;

  // 从root.pendingLanes去掉当前的lane
  markRootFinished(root, lane);

  /** 设置调度 执行passiveEffect */
  /** 真正执行会在commit之后 不影响渲染 */
  /** commit阶段会收集effect到root.pendingPassiveEffect */
  // 有删除 或者收集到Passive 都运行
  if (
    (finishedWork.flags & PassiveMask) !== NoFlags ||
    (finishedWork.subTreeFlags & PassiveMask) !== NoFlags
  ) {
    // 调度副作用
    scheduler.scheduleCallback(
      PriorityLevel.NORMAL_PRIORITY,
      flushPassiveEffect.bind(null, root.pendingPassiveEffects)
    );
  }

  /** hostRootFiber是否有effect  */
  const hostRootFiberHasEffect =
    (finishedWork.flags & (MutationMask | PassiveMask)) !== NoFlags;

  /** hostRootFiber的子树是否有effect  */
  const subtreeHasEffect =
    (finishedWork.subTreeFlags & (MutationMask | PassiveMask)) !== NoFlags;

  /** 有Effect才处理 */
  if (hostRootFiberHasEffect || subtreeHasEffect) {
    commitMutationEffects(finishedWork, root);
  }
  // commit完成 修改current指向新的树
  root.current = finishedWork;
  // commitLayout阶段 处理Attach Ref
  commitLayoutEffects(finishedWork, root);
  // 确保可以继续调度
  ensureRootIsScheduled(root);
}

 其中,下面的代码是把Passive阶段的useEffect执行加入scheduler,也就是加入宏任务队列,在渲染之后执行.

   scheduler.scheduleCallback(
      PriorityLevel.NORMAL_PRIORITY,
      flushPassiveEffect.bind(null, root.pendingPassiveEffects)
    );

commitMutationEffects(finishedWork, root);为开启Mutation阶段,处理dom元素的挂载,删除,更新等。

此时新的DOM树已经生成完成,root.current = finishedWork; 修改current指向新的Fiber树

commitLayoutEffects(finishedWork, root); 开启layout阶段,处理Ref和useLayoutEffect

最后需要重新调用ensureRootIsSchedule 重新开启新的调度

调用顺序

scheduleUpdateOnFiber -> markUpdateFromFiberToRoot -> ensureRootIsUpdated -> performWorkOnRoot -> renderRoot -> prepareRefreshStack -> workLoop -> PerformUnitOfWork -> beginWork -> completeWork ->  commitRoot

 

下一篇,我们聊一下BeginWork的实现以及reconcile协调过程


http://www.kler.cn/a/550210.html

相关文章:

  • 使用DeepSeek建立一个智能聊天机器人0.11
  • 【1.8w字深入解析】从依赖地狱到依赖天堂:pnpm 如何革新前端包管理?
  • 《Keras 3 :使用 DeepLabV3+ 的多类语义分割》
  • 公然上线传销项目,Web3 的底线已经被无限突破
  • 第35次CCF计算机软件能力认证 python 参考代码
  • 训练数据为什么需要Shuffle
  • 放大镜效果
  • Redis原理简述及发布订阅消息队列
  • 技术速递|Copilot Edits(预览版)介绍
  • ubuntu桌面东西没了,右键只有更换壁纸,显示设置和设置
  • Android 14输入系统架构分析:图解源码从驱动层到应用层的完整传递链路
  • 笔记9——循环语句:for语句、while语句
  • 通过TDE工业通讯网关解决设备通讯问题
  • 在自有ARM系统上离线安装MongoDB的坎坷历程与解决方案
  • 5.【线性代数】—— 转置,置换和向量空间
  • OpenCV(1):简介、安装、入门案例、基础模块
  • 使用 Spring Boot 和 Canal 实现 MySQL 数据库同步
  • 二、从0开始卷出一个新项目之瑞萨RZT2M双核架构通信和工程构建
  • 【实战项目】BP神经网络识别人脸朝向----MATLAB实现
  • 【蓝桥杯集训·每日一题2025】 AcWing 6122. 农夫约翰的奶酪块 python