이성열
A

prop 비교해 건너뛰기

초기 렌더때는 당연하게도 모든 컴포넌트들에 대해 beginWork가 호출되었다. 이번에도 그런지 beginWorkconsole.log(current, workInProgress)를 추가하고 버튼을 눌러 리렌더링해보자.

RootFiber / RootFiber
App / App
div / div
// Link 컴포넌트 하위 a는 방문하지 않는다.
Link / Link
br / br
Component / Component
div / div
button / button
text / text
text / text
text / text
// 기존에 없던 Fiber가 생겼다는 의미이다.
undefined / b
text / text

Link 컴포넌트 하위 a는 방문하지 않는 것을 확인할 수 있다. 무언가 처리하는 로직이 있을 것이다:

memoizedProps, pendingProps

우선 중요한 두 필드의 역할을 확인하자. Fiber 객체에는 prop 관련된 두 개의 필드가 있다.

memoizedProps는 이미 적용되어 UI에서 사용중(?)인 prop을, pendingProps는 적용해야할 prop이라고 이해하자.

beginWork

각 Fiber에 대한 작업을 시작하는 beginWork 함수를 보자. 리액트에서는 리렌더링을 건너뛰는 행위를 bail out이라고 하나보다.

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      // 이전 prop과 새로운 prop을 참조 비교한다.
      oldProps !== newProps ||
      hasLegacyContextChanged()
    ) {
      didReceiveUpdate = true;
    } else {
      // prop이나 context가 동일하다면 예약된 리렌더가 있는지 확인한다. 
      // lane을 활용하는데 나중에 알아보자.
      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
      ) {
        // 업데이트도 없으니 이 Fiber는 건너뛰어도 좋다.
        didReceiveUpdate = false;

        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
      ...
    }
  } else {
    // 초기 렌더 때 여기로 온다 
  }

  // 위 체크들에서 해당사항이 없으면 
  // 초기 렌더때와 같게 update 함수들을 호출한다.
  workInProgress.lanes = NoLanes;

  switch (workInProgress.tag) {
    ...
  }
}

prop/context 변화가 없고 예약된 작업(lane)이 없다면 리렌더링을 건너뛰기 위해 attemptEarlyBailoutIfNoScheduledUpdate이 호출된다.

우리 예제에서 각 Fiber별로 로그를 찍어보자.

FiberoldPropsnewPropsprop 같음hasScheduledUpdateOrContext
RootFibernullnulltruefalse
App{}{}truefalse
div{children: Array(3)}{children: Array(3)}truefalse
Link{}{}truefalse
br{}{}truefalse
Component{}{}truetrue
div{children: Array(5)}{children: Array(5)}false-
button{children: Array(2), onClick: f}{children: Array(2), onClick: f}false-
text (‘click’)‘click me - ''click me - ‘truefalse
text (‘count’)‘0''1’false-
text (’ ’)’ '' ‘truefalse
text (’(’)’(''(‘truefalse
text (’)’)’)'')‘truefalse

Link는 prop이 같고 hasScheduledUpdateOrContext도 false여서 bail out되어 a까지 가지 않는다.

Component는 prop은 같지만 hasScheduledUpdateOrContext가 true라서, 즉 예약된게 있어서 bail out되지 않는다.

‘even’과 ‘odd’에 해당하는 Fiber가 안보이는데 이는 기존에 없던 Fiber라서 if (current !== null)에 걸리지 않아서이다. 따라서 bail out할 수 없다.

attemptEarlyBailoutIfNoScheduledUpdate

자 지금까지 bail out되는 조건을 살펴봤고 이제 bail out시 어떤 작업을 하는지 살펴보자.

function attemptEarlyBailoutIfNoScheduledUpdate(
  current: Fiber,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  // This fiber does not have any pending work. Bailout without entering
  // the begin phase. There's still some bookkeeping we that needs to be done
  // in this optimized path, mostly pushing stuff onto the stack.
  switch (workInProgress.tag) {
    case HostRoot: {
      pushHostRootContext(workInProgress);
      const root: FiberRoot = workInProgress.stateNode;
      pushRootTransition(workInProgress, root, renderLanes);
      ...
      break;
    }
    case HostSingleton:
    case HostComponent:
      pushHostContext(workInProgress);
      break;
    ...
  }
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

여기서 말하는 bookkeeping이 뭔지는 아직 모르겠다. 넘어가고 bailoutOnAlreadyFinishedWork 함수를 살펴보자.

bailoutOnAlreadyFinishedWork

주석이 아주 잘 써져있다. 각 분기별로 어떤 상황인지 확인해보자.

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    // Reuse previous dependencies
    workInProgress.dependencies = current.dependencies;
  }

  markSkippedUpdateLanes(workInProgress.lanes);

  // Check if the children have any pending work.
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // The children don't have any work either. We can skip them.
    if (current !== null) {
      // Before bailing out, check if there are any context changes in
      // the children.
      lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
      // 동일한 체크를 다시 한다. 
      // 아직 이해는 잘 안되지만 context 관련 로직인가?
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        return null;
      }
    } else {
      return null;
    }
  }

  // This fiber doesn't have work, but its subtree does. Clone the child
  // fibers and continue.
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

방금 childLanes의 활용처를 찾았다! 이를 통해 childLanes가 0이 아닌 HostRootFiber, App, div를 처리할 때는 아래 cloneChildFibers가 호출된 후 workInProgress.child에 대해서 순회를 지속함을 알 수 있다. 하지만 childLanes가 0인 Link에서는 null이 반환되어 자식까지 가지 않고 순회를 멈춤을 알 수 있다.

cloneChildFibers는 자세히 살펴보지 않겠지만 current를 재활용할 수 있다면 재활용한다.

댓글을 남기려면 로그인이 필요합니다.