Skip to content

鼓励作者:欢迎 star 或打赏犒劳

beginWork “递”阶段

beginWork 主要工作是根据传入的 Fiber 节点 创建或复用 子 Fiber 节点

beginWork 流程图

beginWork

beginWork 的入参

源码地址 beginWork | react-reconciler/src/ReactFiberBeginWork.old.js

ts
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,

  // 优先级相关(先不管这个参数)
  renderLanes: Lanes,
): Fiber | null {
  // ... 省略函数体
}

在之前解读 performUnitOfWork 时说过 currentworkInProgress 这两个重要变量,这里再重复一下:

  • current 表示当前页面正在使用的 Fiber 节点,即 workInProgress.alternate
  • workInProgress 表示当前正在构建的 Fiber 节点

beginWork 工作的两部分

Fiber 双缓存的构建 时说过,除 rootFiber 以外组件 mountcurrent === null,而组件 update 时,由于已经 mount 过了,所以 current !== null(即 current === null ? mount : update

根据这个特性可以将 beginWork 的工作分为两部分:

  • update:如果 current 存在且满足一定的条件时,可以复用 currentFiber 节点(即上一次更新的 Fiber 节点
  • mount:根据 fiber.tag 进入不同的处理函数,然后调用 reconcileChildren 创建 子 Fiber 节点
ts
function beginWork(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes): Fiber | null {
  /* update 时,如果 current 存在可能存在优化路径,可以复用上一次更新的 Fiber 节点 */
  if (current !== null) {
    const oldProps = current.memoizedProps
    const newProps = workInProgress.pendingProps

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      // Force a re-render if the implementation changed due to hot reload:
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // If props or context changed, mark the fiber as having performed work.
      // This may be unset if the props are determined to be equal later (memo).
      // 当 props 或 context 发生变化时将 Fiber 标记为需要更新
      didReceiveUpdate = true
    } else {
      // checkScheduledUpdateOrContext 函数用于检查是否有挂起的更新或 context 变化
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes)
      if (
        !hasScheduledUpdateOrContext &&
        // If this is the second pass of an error or suspense boundary, there
        // may not be work scheduled on `current`, so we check for this flag.
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        // 当没有挂起的更新或 context 时,复用 current 的节点
        didReceiveUpdate = false
        return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes)
      }
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        // This is a special case that only exists for legacy mode.
        // See https://github.com/facebook/react/pull/19216.
        // legacy 模式强制更新
        didReceiveUpdate = true
      } else {
        // An update was scheduled on this fiber, but there are no new props
        // nor legacy context. Set this to false. If an update queue or context
        // consumer produces a changed value, it will set this to true. Otherwise,
        // the component will assume the children have not changed and bail out.
        // 在这个 Fiber 节点上安排了一个更新,但由于 props 和 context 都没有发生变化,将其设置为 false
        // 如果更新队列或 context 消费者生成了更改的值,则将其设置为 true。否则该组件会设置 children 没有更新并退出
        didReceiveUpdate = false
      }
    }
  } else {
    /* mount 时,根据 tag 创建不同类型的子 Fiber 节点 */
    didReceiveUpdate = false

    if (getIsHydrating() && isForkedChild(workInProgress)) {
      // Check if this child belongs to a list of muliple children in
      // its parent.
      //
      // In a true multi-threaded implementation, we would render children on
      // parallel threads. This would represent the beginning of a new render
      // thread for this subtree.
      //
      // We only use this for id generation during hydration, which is why the
      // logic is located in this special branch.
      const slotIndex = workInProgress.index
      const numberOfForks = getForksAtLevel(workInProgress)
      pushTreeId(workInProgress, numberOfForks, slotIndex)
    }
  }

  // Before entering the begin phase, clear pending update priority.
  // TODO: This assumes that we're about to evaluate the component and process
  // the update queue. However, there's an exception: SimpleMemoComponent
  // sometimes bails out later in the begin phase. This indicates that we should
  // move this assignment out of the common path and into each branch.
  workInProgress.lanes = NoLanes

  // 根据 tag 进入不同的处理函数,然后调用 reconcileChildren 创建 Fiber 节点
  switch (workInProgress.tag) {
    // ... 省略其他 case

    // 函数组件
    case FunctionComponent: {
      const Component = workInProgress.type
      const unresolvedProps = workInProgress.pendingProps
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps)
      return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderLanes)
    }
    // 类组件
    case ClassComponent: {
      const Component = workInProgress.type
      const unresolvedProps = workInProgress.pendingProps
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps)
      return updateClassComponent(current, workInProgress, Component, resolvedProps, renderLanes)
    }
    // rootFiber
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes)
    // 普通 DOM 标签
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes)
    // 文本节点
    case HostText:
      return updateHostText(current, workInProgress)

    // ... 省略其他 case
  }

  // 当 switch 语句中没有匹配到任何类型时,抛出错误
  throw new Error(
    `Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
      'React. Please file an issue.'
  )
}

React 中的节点类型定义如下:

源码地址 react-reconciler/src/ReactWorkTags.js

ts
export const FunctionComponent = 0
export const ClassComponent = 1
export const IndeterminateComponent = 2 // Before we know whether it is function or class
export const HostRoot = 3 // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4 // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5
export const HostText = 6
export const Fragment = 7
export const Mode = 8
export const ContextConsumer = 9
export const ContextProvider = 10
export const ForwardRef = 11
export const Profiler = 12
export const SuspenseComponent = 13
export const MemoComponent = 14
export const SimpleMemoComponent = 15
export const LazyComponent = 16
export const IncompleteClassComponent = 17
export const DehydratedFragment = 18
export const SuspenseListComponent = 19
export const ScopeComponent = 21
export const OffscreenComponent = 22
export const LegacyHiddenComponent = 23
export const CacheComponent = 24
export const TracingMarkerComponent = 25

updateHostComponent

updateHostComponent 函数用于处理普通 DOM 标签

源码地址 updateHostComponent | react-reconciler/src/ReactFiberBeginWork.old.js

ts
function updateHostComponent(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) {
  pushHostContext(workInProgress)

  if (current === null) {
    tryToClaimNextHydratableInstance(workInProgress)
  }

  const type = workInProgress.type
  const nextProps = workInProgress.pendingProps
  const prevProps = current !== null ? current.memoizedProps : null

  let nextChildren = nextProps.children
  // 判断是否是纯文本或是否具有 dangerouslySetInnerHTML 属性(即内容不需要 React 来渲染)
  const isDirectTextChild = shouldSetTextContent(type, nextProps)

  // 如果是纯文本或具有 dangerouslySetInnerHTML 属性,那么将 nextChildren 设置为 null
  if (isDirectTextChild) {
    nextChildren = null
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
    // 如果从纯文本节点切换到普通节点或空节点时,需要打上 ContentReset 标记
    workInProgress.flags |= ContentReset
  }

  markRef(current, workInProgress)
  // 调用 reconcileChildren 创建子 Fiber 节点
  reconcileChildren(current, workInProgress, nextChildren, renderLanes)
  return workInProgress.child
}

shouldSetTextContent

源码地址 shouldSetTextContent | react-dom/src/client/ReactDOMHostConfig.js

ts
function shouldSetTextContent(type: string, props: Props): boolean {
  return (
    type === 'textarea' ||
    type === 'noscript' ||
    typeof props.children === 'string' ||
    typeof props.children === 'number' ||
    (typeof props.dangerouslySetInnerHTML === 'object' &&
      props.dangerouslySetInnerHTML !== null &&
      props.dangerouslySetInnerHTML.__html != null)
  )
}

attemptEarlyBailoutIfNoScheduledUpdate

源码地址 attemptEarlyBailoutIfNoScheduledUpdate | react-reconciler/src/ReactFiberBeginWork.old.js

ts
function attemptEarlyBailoutIfNoScheduledUpdate(
  current: Fiber,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  // 根据 tag 进入不同的处理函数
  switch (workInProgress.tag) {
    case HostRoot:
      pushHostRootContext(workInProgress)
      const root: FiberRoot = workInProgress.stateNode
      pushRootTransition(workInProgress, root, renderLanes)

      if (enableCache) {
        const cache: Cache = current.memoizedState.cache
        pushCacheProvider(workInProgress, cache)
      }
      resetHydrationState()
      break
    case HostComponent:
      pushHostContext(workInProgress)
      break
    case ClassComponent: {
      const Component = workInProgress.type
      if (isLegacyContextProvider(Component)) {
        pushLegacyContextProvider(workInProgress)
      }
      break
    }

    // ... 省略其他 case
  }

  // 调用 bailoutOnAlreadyFinishedWork 函数复用 Fiber 节点
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes)
}

bailoutOnAlreadyFinishedWork

源码地址 bailoutOnAlreadyFinishedWork | react-reconciler/src/ReactFiberBeginWork.old.js

ts
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    // 复用之前的依赖
    workInProgress.dependencies = current.dependencies
  }

  if (enableProfilerTimer) {
    // Don't update "base" render times for bailouts.
    stopProfilerTimerIfRunning(workInProgress)
  }

  markSkippedUpdateLanes(workInProgress.lanes)

  // 检查子节点是否有任何待处理的工作
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    if (enableLazyContextPropagation && current !== null) {
      lazilyPropagateParentContextChanges(current, workInProgress, renderLanes)
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        return null
      }
    } else {
      return null
    }
  }

  // 克隆子 Fiber
  cloneChildFibers(current, workInProgress)
  return workInProgress.child
}

reconcileChildren

reconcileChildren 函数用于创建或更新 Fiber 树,会根据 current === null 的结果执行不同的逻辑

源码地址 reconcileChildren | react-reconciler/src/ReactFiberBeginWork.old.js

ts
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    // If this is a fresh new component that hasn't been rendered yet, we
    // won't update its child set by applying minimal side-effects. Instead,
    // we will add them all to the child before it gets rendered. That means
    // we can optimize this reconciliation pass by not tracking side-effects.
    // mount 阶段,这是一个还未渲染过的全新组件,可以不用考虑对比最小副作用来更新它的子节点
    // 可以在它渲染前添加上所有的子节点,意味着我们可以不对比副作用来优化这个操作
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes)
  } else {
    // If the current child is the same as the work in progress, it means that
    // we haven't yet started any work on these children. Therefore, we use
    // the clone algorithm to create a copy of all the current children.
    // 如果 current 的子节点和 workInProgress 的子节点相同,意味着我们还没有开始对这些子节点进行任何操作
    // 因此,将使用克隆算法来创建当前子节点的副本

    // If we had any progressed work already, that is invalid at this point so
    // let's throw it out.
    // 如果 workInProgress 有 child,但是 current 没有 child,那么说明 workInProgress 的 child 是无效的,需要丢弃

    // update 时,current 为上一次渲染的 Fiber 节点
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    )
  }
}
ts
export const reconcileChildFibers = ChildReconciler(true)
export const mountChildFibers = ChildReconciler(false)

TIP

reconcileChildFibersmountChildFibers 最终都是 ChildReconciler 传递不同的参数返回的函数;在 ChildReconciler 函数中用 shouldTrackSideEffects 来判断是否需要跟踪副作用(即为对应的 Fiber 节点 添加 flags 属性),因此 reconcileChildFibersmountChildFibers 的不同在于对副作用的处理不同

ChildReconciler

ChildReconciler 中定义了大量如 deleteXXXplaceXXXupdateXXXreconcileXXX 的函数,这些函数覆盖了对 Fiber 节点 的创建、增加、删除、修改等操作,而这些函数将直接或间接地被 reconcileChildFibers 所调用

源码地址 ChildReconciler | react-reconciler/src/ReactChildFiber.old.js

ts
function ChildReconciler(shouldTrackSideEffects) {
  /* 以下函数省略函数体 */
  function deleteChild(returnFiber, childToDelete) {}
  function deleteRemainingChildren(returnFiber, currentFirstChild) {}
  function mapRemainingChildren(returnFiber, currentFirstChild) {}
  function useFiber(fiber, pendingProps) {}
  function placeChild(newFiber, lastPlacedIndex, newIndex) {}
  function placeSingleChild(newFiber) {}
  function updateTextNode(returnFiber, current, textContent, lanes) {}
  function updateElement(returnFiber, current, element, lanes) {}
  function updatePortal(returnFiber, current, portal, lanes) {}
  function updateFragment(returnFiber, current, fragment, lanes, key) {}
  function createChild(returnFiber, newChild, lanes) {}
  function updateSlot(returnFiber, oldFiber, newChild, lanes) {}
  function updateFromMap(existingChildren, returnFiber, newIdx, newChild, lanes) {}
  function warnOnInvalidKey(child, knownKeys, returnFiber) {}
  function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {}
  function reconcileChildrenIterator(returnFiber, currentFirstChild, newChildrenIterable, lanes) {}
  function reconcileSingleTextNode(returnFiber, currentFirstChild, textContent, lanes) {}
  function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {}
  function reconcileSinglePortal(returnFiber, currentFirstChild, portal, lanes) {}

  // This API will tag the children with the side-effect of the reconciliation
  // itself. They will be added to the side-effect list as we pass through the
  // children and the parent.
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    debugger
    // This function is not recursive.
    // If the top level item is an array, we treat it as a set of children,
    // not as a fragment. Nested arrays on the other hand will be treated as
    // fragment nodes. Recursion happens at the normal flow.

    // Handle top level unkeyed fragments as if they were arrays.
    // This leads to an ambiguity between <>{[...]}</> and <>...</>.
    // We treat the ambiguous cases above the same.
    // 判断最外层的 newChild 是否是 Fragment 组件
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null
    // 如果最外层是 Fragment 组件,那么将其 children 作为 newChild
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children
    }

    // 判断 newChild 是否是对象
    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        // 处理 jsx 节点
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes),
          )
        // 处理 portal 节点
        case REACT_PORTAL_TYPE:
          return placeSingleChild(
            reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes),
          )
        // 处理 lazy 节点
        case REACT_LAZY_TYPE:
          const payload = newChild._payload
          const init = newChild._init
          // TODO: This function is supposed to be non-recursive.
          return reconcileChildFibers(returnFiber, currentFirstChild, init(payload), lanes)
      }

      // 处理多个子节点
      if (isArray(newChild)) {
        return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes)
      }

      // 处理可迭代的子节点
      if (getIteratorFn(newChild)) {
        return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, lanes)
      }

      throwOnInvalidObjectType(returnFiber, newChild)
    }

    // 处理文本节点
    if ((typeof newChild === 'string' && newChild !== '') || typeof newChild === 'number') {
      return placeSingleChild(
        reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes),
      )
    }

    // 删除剩余的子节点
    return deleteRemainingChildren(returnFiber, currentFirstChild)
  }

  return reconcileChildFibers
}

flags 属性

flags 属性用于标记 Fiber 节点 的状态,其值为二进制数,每一位代表一种状态,如下:

ts
export const Placement = /*                    */ 0b00000000000000000000000010
export const Update = /*                       */ 0b00000000000000000000000100
export const Deletion = /*                     */ 0b00000000000000000000001000
export const ChildDeletion = /*                */ 0b00000000000000000000010000
export const ContentReset = /*                 */ 0b00000000000000000000100000
export const Callback = /*                     */ 0b00000000000000000001000000
export const DidCapture = /*                   */ 0b00000000000000000010000000
export const ForceClientRender = /*            */ 0b00000000000000000100000000
export const Ref = /*                          */ 0b00000000000000001000000000
export const Snapshot = /*                     */ 0b00000000000000010000000000
export const Passive = /*                      */ 0b00000000000000100000000000
export const Hydrating = /*                    */ 0b00000000000001000000000000
export const Visibility = /*                   */ 0b00000000000010000000000000
export const StoreConsistency = /*             */ 0b00000000000100000000000000

在通知 Renderer 将 Fiber 节点 对应的 DOM 节点 渲染到页面上需要满足以下两个条件:

  1. fiber.stateNode 存在(即 Fiber 节点 中保存了对应的 DOM 节点
  2. fiber.flags 中包含 Placement 标记(即 DOM 节点 需要被插入到页面上)

在首屏渲染时如何满足上述两个条件?

  1. fiber.stateNode 会在 completeWork 中创建
  2. mount 时只有 rootFiber 会赋值 Placement flags,其他 Fiber 节点 都不会赋值 Placement flags,因此在 commit 阶段,插入操作仅会在根节点执行一次,避免了重复的 DOM 操作

如果在 mountChildFibers 时也会赋值 Placement flags,那么整棵 Fiber 的每个节点都会具有 Placement flags,这会导致在 commit 段执行大量的 DOM 插入操作,效率极低;

因此 React 进行了优化:mount 时只有 rootFiber 会赋值 Placement flags,从而确保在 commit 阶段只有一次插入操作,有效地提升了渲染性能

如有转载或 CV 的请标注本站原文地址