Skip to content

render 阶段

对于组件,我们一般可以将其简单分为三种状态: mount(初次渲染)、update(更新)、unMount(卸载),那么对于一个组件的 render 阶段其在这三种状态下分别做哪些工作?

Mount(初次渲染)

组件的初次渲染一般发生于两种情况: ReactDOM.render()、更新时初次加载。其都是一套流程,下面我们主要按照 ReactDOM.render()的过程来说明。

对于 ReactDOM.render()的时候,其在 legacyRenderSubtreeIntoContainer()的时候初始化生成了 FiberRoot 和 FiberRootNode 两个对象,并通过 updateContainer() 将整个 render 的流程放到任务调度中去,(对于 updateContianer 我们可以看 updateContainer 的说明),最终其执行的是 scheduleUpdateOnFiber(current, lane, eventTime) ,

scheduleUpdateOnFiber(fiber: Fiber, lane : Lane , eventTime : number )

项目初次渲染(render)或者触发更新的主要入口函数。

  • ReactDOM.render()的时候 fiber 是 FiberRootNode
  • 组件触发更新(如 setState)的时候 fiber 是 组件的 FiberNode

其主要流程分为以下几个过程

  1. checkForNestedUpdates() 排除无限嵌套更新的问题

    检查工作,主要怕开发人员在组件更新的时候产生无限更新。 如 useEffect(() => { setVisible(!visible) } ,[ visible ])

  2. 更新优先级及找到 FiberRootNode

    • 在当前 FiberNode 实例的 lanes 和 所有父节点的 childLanes 中添加当前 Update.lane

      当组件触发更新的时候 可以通过 FiberNode.childLanes 找到当前需要更新节点线,以便在 beginWork 的时候作为判断 FiberNode 是否可以复用的依据。

      缓存当前 Update 的优先级到 FiberNode.lanes 上,可以在调度的时候判断当前优先级下是否是该组件触发的更新

    • 通过 FiberNode.return 向上找到根 FiberRoot

      因为 根 FiberRootNode.tag === HostRoot ,然后通过 FiberRootNode.stateNode 找到 FiberRoot

  3. 完成节点之后赋值 effect 链

  4. markRootUpdated(root, lane, eventTime) 标记更新

    在根 FiberRoot 上标记当前存在此优先级的 Update

    主要通过二级制位的方式在根FiberRoot.pendingLanes上标记当前存在此优先级的更新,如 setState 的优先级为 0b0000000000000000000000000000001,那么通过按位与的方式就可以在 pendingLanes 缓存 root.pendingLanes |= updateLane; 这样 pendingLanes 的结果就变成 0b????????????????????1

  5. 优先级 及 不同环境 的处理

js
// 最高优先级
if (lane === SyncLane) {
  if (
    // Check if we're inside unbatchedUpdates
    (executionContext & LegacyUnbatchedContext) !== NoContext &&
    // Check if we're not already rendering
    (executionContext & (RenderContext | CommitContext)) === NoContext
  ) {
    schedulePendingInteractions(root, lane)
    // 这是一个遗留的边界情况。reactdom .render的初始装入batchedUpdates内部的根应该是同步的,但是布局更新应该延迟到批处理结束。
    performSyncWorkOnRoot(root)
  } else {
    ensureRootIsScheduled(root, eventTime)
    schedulePendingInteractions(root, lane)
    if (executionContext === NoContext) {
      resetRenderTimer()
      flushSyncCallbackQueue()
    }
  }
} else {
  ensureRootIsScheduled(root, eventTime)
  schedulePendingInteractions(root, lane)
}

可以看出对于所有的更新或者初始渲染主要分为两种:

  1. performSyncWorkOnRoot(root) 为核心的同步处理流程
  2. ensureRootIsScheduled(root, eventTime); 为核心的异步处理流程

问题: 为什么 ReactDOM.render 触发的首次渲染是一个同步的过程呢?不是说在新的 Fiber 架构下,render 阶段是一个可打断的异步过程。

下面我们看 performSyncWorkOnRoot ,因为对于 异步处理流程(ensureRootIsScheduled)其主要还是调用 performSyncWorkOnRoot 处理。

performSyncWorkOnRoot(root)

这是不经过 Scheduler 的同步任务的入口点。

image

从上图可以看出对于 performSyncWorkOnRoot 其主要工作分为以下几个

  1. effect 的处理
  2. 获取更新优先级并进入 render 阶段
  3. 对 render 阶段错误处理
  4. commit 阶段
  5. ensureRootIsScheduled(root, now())

renderRootSync(root: FiberRoot, lanes: Lanes)

主要工作为

prepareFreshStack(root, lanes)

重置调度队列。

此方法主要做的时候进行

  1. workInProgressRoot 的缓存。

  2. 回退上次 render 未完成的 FiberNode 的堆栈数据

如果此次更新未完成的时候就因为进程需要交付给渲染流程,那么下次恢复 render 的时候,因为 workInProgress 是一个全局变量不为空,所以需要向上遍历对整条 FiberNode 进行堆栈信息的回退

js
// workInProgress 代表当前正在更新的Fibre对象
if (workInProgress !== null) {
  // 向上遍历当前 fiber 的父节点,执行 unwindInterruptedWork 操作
  // unwindInterruptedWork 用于回退一些堆栈信息
  let interruptedWork = workInProgress.return
  while (interruptedWork !== null) {
    unwindInterruptedWork(interruptedWork)
    interruptedWork = interruptedWork.return
  }
}
  1. **workInProgress 的初始化 **(基于 root)

为什么需要进行prepareFreshStack的流程

  • 初次渲染 , workInProgressRoot 为 null
  • 恢复 render 流程 , 异步处理流程中当渲染处理完毕,进程空闲的时候,这时候如果存在 Update 那么就会再次进行 render,这时候如果遇到高优先级的使得渲染优先级改变,那么这时候也需要进行 render 的初始化过程
workLoopSync()

基于prepareFreshStack的过程中基于 root 创建一个一个根 FiberNode 的 workInProgress 和 workInProgress.alternate(current,上一次的 FiberNode 对象)进行双链比较处理

重置工作
js
function renderRootSync(root: FiberRoot, lanes: Lanes) {
  const prevExecutionContext = executionContext
  // 切换到渲染上下文
  executionContext |= RenderContext
  const prevDispatcher = pushDispatcher()
  // 上下文的重置工作
  resetContextDependencies()

  if (enableSchedulerTracing) {
    popInteractions(((prevInteractions: any): Set<Interaction>))
  }

  executionContext = prevExecutionContext
  popDispatcher(prevDispatcher)

  if (enableSchedulingProfiler) {
    markRenderStopped()
  }

  // Set this to null to indicate there's no in-progress render.
  // 在 prepareFreshStack(root, lanes); 根据 root 创建了 workInProgressRoot 及 其他属性,这时候也进行重置工作
  workInProgressRoot = null
  workInProgressRootRenderLanes = NoLanes

  return workInProgressRootExitStatus
}

workLoop()

在 renderRootSync 的时候我们根据 root 创建了一个 workInProgress 对象(全局对象),那么在 workLoopSync()或者workLoopConcurrent()的过程中都是对当前的 workInProgress 对象进行一系列的操作。

js
// 同步的流程
function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress)
  }
}
/** @noinline */
// 异步的流程
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress)
  }
}

从上面可以发现 我们本质上都是通过performUnitOfWork(workInProgress);去构建 FiberNode 树,对于同步、异步的区别当前只是通过!shouldYield()去判断我们是否需要继续下去。

performUnitOfWork(workInProgress)

真正区分 beginWork 和 completeUnitOfWork 的过程

image

我们先简单看一下源码

js
function performUnitOfWork(unitOfWork: Fiber): void {
  // 获取其原始的 FiberNode 对象,在update 的时候可以通过 unitOfWork(workInProgress|新FiberNode) 和 current(旧FiberNode)的对比来判断是否可以进行复用等
  const current = unitOfWork.alternate

  let next
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    // 进行beginWork的流程
    next = beginWork(current, unitOfWork, subtreeRenderLanes)
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes)
  }
  unitOfWork.memoizedProps = unitOfWork.pendingProps
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork)
  } else {
    workInProgress = next
  }

  ReactCurrentOwner.current = null
}

其实从performUnitOfWork的源码中我们看不出上图所示的构建流程,在performUnitOfWork中我们只能看出beginWork 返回的 next 为 null 的时候会进行 completeUnitOfWork(unitOfWork)的过程,不为 null 的时候 赋值给 workInProgress 从而再次进入 do 循环。那么

  1. next 是什么?

    先简单看 beginWork 的过程,可见 next 指的是子节点。

    js
    function beginWork( current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,): Fiber | null {
      switch (workInProgress.tag) {
        return updateBlock(current, workInProgress, block, props, renderLanes);
      }
    }
    function updateBlock<Props, Data>(
      current: Fiber | null,
      workInProgress: Fiber,
      block: BlockComponent<Props, Data>,
      nextProps: any,
      renderLanes: Lanes,
    ) {
      // 返回的是当前FiberNode的child
      return workInProgress.child;
    }
  2. 对于没有子的兄弟节点怎么处理

    对于上图的 H1 节点 其 next 为 null 那么就行进入 completeUnitOfWork(unitOfWork);执行 complteWork过程,可见对于上图的4 beginWork --> 5 complteWork 是正确的,那么怎么知道下一步处理兄弟节点 div 呐?

    js
    function completeUnitOfWork(unitOfWork: Fiber): void {
      // Attempt to complete the current unit of work, then move to the next
      // sibling. If there are no more siblings, return to the parent fiber.
      let completedWork = unitOfWork
      do {
        const current = completedWork.alternate
        const returnFiber = completedWork.return
    
        // 如果存在兄弟节点 那么将兄弟节点赋给workInProgress 进入到 兄弟节点的 workLoop 的流程
        const siblingFiber = completedWork.sibling
        if (siblingFiber !== null) {
          // If there is more work to do in this returnFiber, do that next.
          workInProgress = siblingFiber
          return
        }
        // Otherwise, return to the parent
        // 如果 不存在兄弟节点 那么通过 do{} 循环执行 父节点的 completeWork流程
        //  此时 completedWork指向了父节点,那么在父节点完成 completeWork流程 时也会判断其是否存在兄弟节点,这样形成一个深度优先的链表 遍历流程
        completedWork = returnFiber
        // Update the next thing we're working on in case something throws.
        workInProgress = completedWork
      } while (completedWork !== null)
    }

    可见真正形成向下 beginWork, 向上 completedWork 的过程是通过 performUnitOfWork() 和 completeUnitOfWork()

  3. 对于没有子没有兄弟了怎么进入父节点

对于 render 阶段,其最主要的工作就是构建 FiberNode 树,