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