React 源码揭秘 | 工作流程

打印 上一主题 下一主题

主题 879|帖子 879|积分 2637

上一篇React源码揭秘 | 启动入口-CSDN博客介绍了React启动的入口,以及一些前置知识,这篇来说一下整个React的工作循环。
上篇说到,调用createRoot(Container).render(<App/>) 函数,启动整个渲染流程,此中render函数如下:
  1. /**
  2. * 更新container 需要传入
  3. * @param element 新的element节点
  4. * @param root APP根节点
  5. */
  6. const updateContainer = (element: ReactElement, root: FiberRootNode) => {
  7.   // 默认情况下 同步渲染
  8.   scheduler.runWithPriority(PriorityLevel.IMMEDIATE_PRIORITY, () => {
  9.     // 请求获得当前更新lane
  10.     const lane = requestUpdateLane();
  11.     // 获hostRootFiber
  12.     const hostRootFiber = root.current;
  13.     // 更新的Element元素入队
  14.     hostRootFiber.updateQueue?.enqueue(
  15.       new Update<ReactElement>(element, lane),
  16.       hostRootFiber,
  17.       lane
  18.     );
  19.     // scheduleUpdateOnFiber 调度更新
  20.     scheduleUpdateOnFiber(root.current, lane);
  21.   });
  22. };
  23. /** 创建根节点的入口 */
  24. export function createRoot(container: Container) {
  25.   // 创建FiberRootNode
  26.   const root = createContainer(container);
  27.   return {
  28.     render(element: ReactElement) {
  29.       // TODO
  30.       // 初始化合成事件
  31.       initEvent(container);
  32.       // 更新contianer
  33.       return updateContainer(element, root);
  34.     },
  35.   };
  36. }
复制代码
render函数做了两件事,
1. 是初始化合成变乱,React通过对Container举行变乱代理的方式管理变乱,对变乱举行一层封装,背面会具体讲。
2. 调用updateContianer函数,此函数调用scheduler.runWithPriority传入一个立刻实行优先级的同步实行回调(runWithPriority具体细节见React源码揭秘 | scheduler 并发更新原理-CSDN博客)
在这个回调中,目前主要关注的是,向hostRootFiber的updateQueue中参加了当前渲染的根ReactElement元素,你目前也不需要关注updateQueue的实现,只需要理解成一个队列即可。
末了调用scheduleUpdateOnFiber 开启调理,同时传入HostRootFiber


scheduleUpdateOnFiber - 探求节点,标记信息

此函数用来调理某个节点的更新,你可以在react-reconciler/workLoop.ts 中找到其实现:
  1. /** 在Fiber中调度更新 */
  2. export function scheduleUpdateOnFiber(fiberNode: FiberNode, lane: Lane) {
  3.   /** 先从更新的fiber节点递归到hostRootFiber
  4.    *  这个过程中,一个目的是寻找fiberRootNode节点
  5.    *  一个是更新沿途的 childLines
  6.    */
  7.   const fiberRootNode = markUpdateLaneFromFiberToRoot(fiberNode, lane);
  8.   // 更新root的pendingLane, 更新root节点的pendingLanes 表示当前正在处理的lanes
  9.   markRootUpdated(fiberRootNode, lane);
  10.   // 保证根节点被正确调度
  11.   ensureRootIsScheduled(fiberRootNode);
  12. }
复制代码
此中,入参fiberNode 大概是恣意的Fiber节点,不肯定是updateContainer中传入的HostRootFiber根节点,由于updateContainer需要从根节点开始更新,而更新大概发生在恣意节点中,比如函数节点中调用useXXX hooks等。
scheduleUpdateOnFiber会调用markUpdateFromFiberToRoot 这个函数看名称就知道其作用是从当前的Fiber节点向上顺着return指针找到根节点,而且在沿途做一些标记,至于是什么标记可以先不消管,这个标记过程类似于一个冒泡的过程,其实现如下:
  1. /**
  2. * 从当前fiberNode找到root节点 并且更新沿途fiber的childLanes
  3. * @param fiberNode
  4. */
  5. export function markUpdateLaneFromFiberToRoot(
  6.   fiberNode: FiberNode,
  7.   lane: Lane
  8. ) {
  9.   let parent = fiberNode.return; // parent表示父节点
  10.   let node = fiberNode; // node标记当前节点
  11.   while (parent !== null) {
  12.     parent.childLanes = mergeLane(parent.childLanes, lane);
  13.     const alternate = parent.alternate;
  14.     if (alternate !== null) {
  15.       alternate.childLanes = mergeLane(alternate.childLanes, lane);
  16.     }
  17.     // 处理parent节点的childLanes
  18.     node = parent;
  19.     parent = parent.return;
  20.   }
  21.   /** 检查当前是否找到了hostRootFiber */
  22.   if (node.tag === HostRoot) {
  23.     return node.stateNode;
  24.   }
  25.   return null;
  26. }
复制代码
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就是负责开启这些流程 其实现如下
  1. /** 从root开始 处理同步任务 */
  2. export function performSyncWorkOnRoot(root: FiberRootNode) {
  3.   // 获取当前的优先级
  4.   const lane = getNextLane(root);
  5.   if (lane !== SyncLane) {
  6.     /**
  7.      * 这里 lane如果不是同步任务了,说明同步任务的lane已经被remove 应该执行低优先级的任务了
  8.      *  此时应该停止执行当前任务 重新调度
  9.      * 【实现同步任务的批处理,当第一次执行完之后 commit阶段remove SyncLane 这里就继续不下去了,
  10.      * 后面微任务中的 performSyncWorkOnRoot都不执行了】
  11.      */
  12.     return ensureRootIsScheduled(root);
  13.   }
  14.   // 开始生成fiber 关闭并发模式
  15.   const exitStatus = renderRoot(root, lane, false);
  16.   switch (exitStatus) {
  17.     // 注意 同步任务一次性执行完 不存在RootInComplete中断的情况
  18.     case RootCompleted:
  19.       // 执行成功 设置finishedWork 和 finishedLane 并且commit
  20.       // 设置root.finishedWork
  21.       root.finishedWork = root.current.alternate;
  22.       root.finishedLane = lane;
  23.       // 设置wipRootRenderLane = NoLane;
  24.       wipRootRenderLane = NoLane;
  25.       commitRoot(root);
  26.     default:
  27.     // TODO Suspense的情况
  28.   }
  29. }
复制代码
目前阶段 你只需要关注,先调用renderRoot 开启render阶段,再调用commitRoot开启commit阶段即可。


renderRoot - 渲染根节点

renderRoot函数主要做两件事情
1. 准备一个工作栈 调用prepareFreshStack 获得一个HostRootFiber根
2. 调用workLoop函数开始深度优先创建Fiber树
  1. /**
  2. * 渲染root 生成fiber对象
  3. * @param root  当前根节点
  4. * @param lane  当前车道
  5. * @param shouldTimeSlice 是否开启并发
  6. */
  7. export function renderRoot(
  8.   root: FiberRootNode,
  9.   lane: Lane,
  10.   shouldTimeSlice: boolean
  11. ) {
  12.   let workLoopRetryTimes = 0;
  13.   if (wipRootRenderLane !== lane) {
  14.     // 避免重新进行初始化
  15.     /** 先进行准备初始化 */
  16.     prepareRefreshStack(root, lane);
  17.   }
  18.   while (true) {
  19.     try {
  20.       // 开启时间片 scheduler调度
  21.       shouldTimeSlice ? workConcurrentLoop() : workLoop();
  22.       break;
  23.     } catch (e) {
  24.       /** 使用try catch保证workLoop顺利执行 多次尝试 */
  25.       workLoopRetryTimes++;
  26.       if (workLoopRetryTimes > 20) {
  27.         console.warn("workLoop执行错误!", e);
  28.         break;
  29.       }
  30.     }
  31.   }
  32.   /** 判断任务是否执行完成 如果执行完成RootCompleted 否则 返回RootInCompleted*/
  33.   if (shouldTimeSlice && workInProgress !== null) {
  34.     return RootInComplete;
  35.   }
  36.   // 任务完成
  37.   return RootCompleted;
  38. }
复制代码


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
  1. /**
  2. * prepareFreshStack 这个函数的命名可能会让人觉得它与“刷新(refresh)”相关,
  3. * 但它的作用实际上是为了 准备一个新的工作栈,而不是刷新。
  4. * @param root
  5. * @param lane 当前车道
  6. */
  7. function prepareRefreshStack(root: FiberRootNode, lane: Lane) {
  8.   // 重新赋finishedWork
  9.   root.finishedWork = null;
  10.   root.finishedLane = NoLane;
  11.   // 设置当前的运行任务lane
  12.   wipRootRenderLane = lane;
  13.   /** 给workInProgress赋值 */
  14.   /** 这里在首次进入的时候 会创建一个新的hostRootFiber
  15.    * 在react中存在两棵fiber树,两个hostRootFiber根节点 用alternate链接,成为双缓存
  16.    */
  17.   workInProgress = createWorkInProgress(root.current, {});
  18. }
复制代码
 createWorkInProgress函数的作用是,创建/复用就节点的方式获取当前待处理的workinprogress节点,其实现如下:
  1. /** 根据现有的Fiber节点,创建更新的Fiber节点
  2. * 如果当前Fiber节点存在alternate 复用
  3. * 弱不存在,创建新的FiberNode
  4. * 将current的内容拷贝过来 包含lane memorizedState/props child 等
  5. *
  6. * 在Fiber节点内容可以复用的情况调用,新的fiber节点的 tag type stateNode 等会复用 | props,lane flags delecation这些副作用 会重置
  7. */
  8. export function createWorkInProgress(
  9.   currentFiber: FiberNode,
  10.   pendingProps: ReactElementProps
  11. ) {
  12.   /** 创建wip 当前的workInProgress 先看看能不能复用.alternate */
  13.   let wip = currentFiber.alternate;
  14.   if (wip === null) {
  15.     /** mount阶段,说明对面不存在alternate节点 */
  16.     wip = new FiberNode(currentFiber.tag, pendingProps, currentFiber.key);
  17.     /** stateNode为fiber对应的真实dom节点 */
  18.     wip.stateNode = currentFiber.stateNode;
  19.     /** 建立双向的alternate链接 */
  20.     wip.alternate = currentFiber;
  21.     currentFiber.alternate = wip;
  22.   } else {
  23.     /** update节点,复用 重置副作用 */
  24.     wip.flags = NoFlags;
  25.     wip.subTreeFlags = NoFlags;
  26.     wip.pendingProps = pendingProps;
  27.     wip.delections = null;
  28.   }
  29.   // 剩下的可以复用
  30.   wip.key = currentFiber.key;
  31.   wip.tag = currentFiber.tag;
  32.   wip.type = currentFiber.type;
  33.   // ref需要传递
  34.   wip.ref = currentFiber.ref;
  35.   wip.memorizedState = currentFiber.memorizedState;
  36.   wip.memorizedProps = currentFiber.memorizedProps;
  37.   wip.updateQueue = currentFiber.updateQueue;
  38.   //  这里需要注意,只需要复用child 可以理解为 新的节点的child指向currentFiber.child 因为后面diff的时候 只需要用的child,仅做对比,
  39.   // 后面会创建新的fiber 此处不需要sibling和return 进行了连接 可以理解成 只复用alternate的内容 不复用其节点之间的关系
  40.   // stateNode也不需要复用 因为alternate和currentFiber之间 如果有关联,那么type一定是相等的
  41.   wip.child = currentFiber.child;
  42.   /** 注意复用的时候 一定要把lane拷贝过去 */
  43.   wip.lanes = currentFiber.lanes;
  44.   wip.childLanes = currentFiber.childLanes;
  45.   return wip;
  46. }
复制代码
此中,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单元
  1. /** 递归循环 */
  2. function workLoop() {
  3.   while (workInProgress) {
  4.     performUnitOfWork(workInProgress);
  5.   }
  6. }
复制代码


 performUnitOfWork - 递归构建FIber

performUnitOfWork就是个递归的过程,先沿着wip.child往下”递“,以此调用beginWork创建Fiber节点,直到叶子节点。
到叶子节点之后开启 "归"的过程,即completeWrok过程,此过程会创建初次挂载的节点,注意,这里只是创建DOM节点的对象,而且赋给wip.stateNode 不会举行挂载。 而且会对lanes以及flags等举行冒泡
假如归的过程中,发现遍历到的节点有sibling兄弟,则继承开始"递"的过程,直到wip为null 完成Fiber树的创建
  1. /**
  2. * 处理单个fiber单元 包含 递,归 2个过程
  3. * @param fiber
  4. */
  5. function performUnitOfWork(fiber: FiberNode) {
  6.   // beginWork 递的过程
  7.   const next = beginWork(fiber, wipRootRenderLane);
  8.   // 递的过程结束,保存pendingProps
  9.   fiber.memorizedProps = fiber.pendingProps;
  10.   // 这里不能直接给workInProgress赋值,如果提前赋workInProgress为null 会导致递归提前结束
  11.   // 如果next为 null 则表示已经递到叶子节点,需要开启归到过程
  12.   if (next === null) {
  13.     /** 开始归的过程 */
  14.     completeUnitOfWork(fiber);
  15.   } else {
  16.     // 继续递
  17.     workInProgress = next;
  18.   }
  19.   // 递的过程可打断,每执行完一个beginWork 切分成一个任务
  20.   // complete归的过程不可打断,需要执行到下一个有sibling的节点/根节点 (return === null)
  21. }
  22. function completeUnitOfWork(fiber: FiberNode) {
  23.   // 归
  24.   while (fiber !== null) {
  25.     completeWork(fiber);
  26.     if (fiber.sibling !== null) {
  27.       // 有子节点 修改wip 退出继续递的过程
  28.       workInProgress = fiber.sibling;
  29.       return;
  30.     }
  31.     /** 向上归 修改workInProgress */
  32.     fiber = fiber.return;
  33.     workInProgress = fiber;
  34.   }
  35. }
复制代码


commit准备工作,设置finishedWork

commit阶段也是个递归的过程,在renderRoot结束之后,需要把FIberRootNode的finishedWork指针指向新创建但是还没commit的Fiber树,提供commit阶段利用
  1. // performWorkOnRoot
  2. root.finishedWork = root.current.alternate;
  3. commitRoot(root);
复制代码
次阶段结束之后Fiber如图所示 

commit阶段主要包含,Mutation,Layout,Passive三个子阶段,其实现如下
  1. /** commit阶段 */
  2. export function commitRoot(root: FiberRootNode) {
  3.   const finishedWork = root.finishedWork;
  4.   if (finishedWork === null) return;
  5.   const lane = root.finishedLane;
  6.   root.finishedWork = null;
  7.   root.finishedLane = NoLane;
  8.   // 从root.pendingLanes去掉当前的lane
  9.   markRootFinished(root, lane);
  10.   /** 设置调度 执行passiveEffect */
  11.   /** 真正执行会在commit之后 不影响渲染 */
  12.   /** commit阶段会收集effect到root.pendingPassiveEffect */
  13.   // 有删除 或者收集到Passive 都运行
  14.   if (
  15.     (finishedWork.flags & PassiveMask) !== NoFlags ||
  16.     (finishedWork.subTreeFlags & PassiveMask) !== NoFlags
  17.   ) {
  18.     // 调度副作用
  19.     scheduler.scheduleCallback(
  20.       PriorityLevel.NORMAL_PRIORITY,
  21.       flushPassiveEffect.bind(null, root.pendingPassiveEffects)
  22.     );
  23.   }
  24.   /** hostRootFiber是否有effect  */
  25.   const hostRootFiberHasEffect =
  26.     (finishedWork.flags & (MutationMask | PassiveMask)) !== NoFlags;
  27.   /** hostRootFiber的子树是否有effect  */
  28.   const subtreeHasEffect =
  29.     (finishedWork.subTreeFlags & (MutationMask | PassiveMask)) !== NoFlags;
  30.   /** 有Effect才处理 */
  31.   if (hostRootFiberHasEffect || subtreeHasEffect) {
  32.     commitMutationEffects(finishedWork, root);
  33.   }
  34.   // commit完成 修改current指向新的树
  35.   root.current = finishedWork;
  36.   // commitLayout阶段 处理Attach Ref
  37.   commitLayoutEffects(finishedWork, root);
  38.   // 确保可以继续调度
  39.   ensureRootIsScheduled(root);
  40. }
复制代码
 此中,下面的代码是把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协调过程

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

王柳

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表