Re-render

Hee Suh·2024년 6월 4일
1
post-thumbnail

JSer.devReact Internals Deep Dive를 번역하여 정리한 글입니다.

⚠️ React@19commit 7608516을 기반으로 작성되었으며, 최신 버전에서는 구현이 변경되었을 수 있습니다.

📝 How does React re-render internally?

Initial mount에서는 전체 DOM을 처음부터 생성하는 반면, initial mount 이후의 리렌더링의 경우, React에서 재조정(reconciliation)이라는 과정을 통해 DOM을 최대한 많이 재사용하려고 한다.

Demo를 이용하여, 리렌더링의 과정을 Trigger, Render, Commit 세 단계로 나눠서 확인해보자.

출처: https://react.dev/learn/render-and-commit Illustrated by Rachel Lee Nabors

출처: https://react.dev/learn/render-and-commit Illustrated by Rachel Lee Nabors

💻 Demo Code

import { useState } from 'react'

function Link() {
  return <a href="https://jser.dev">jser.dev</a>;
}

function Component() {
  const [count, setCount] = useState(0);
  return (
    <div>
    <button onClick={() => setCount((count) => count + 1)}>
      click me - {count} 
    </button> ({count % 2 === 0 ? <span>even</span> : <b>odd</b>})
    </div>
  );
}

export default function App() {
  return (
    <div>
      <Link />
      <br />
      <Component />
    </div>
  );
}

1. Re-render in Trigger phase

React는 initial mount에서 다음과 같은 Fiber 트리와 DOM 트리를 구성한다.

https://jser.dev/2023-07-18-how-react-rerenders/#1-re-render-in-trigger-phase

출처: https://jser.dev/2023-07-18-how-react-rerenders/#1-re-render-in-trigger-phase

1.1 laneschildLanes

Lane은 보류 중인 작업(pending work)의 우선순위를 나타낸다.

Fiber Node의 경우 다음이 포함된다.

  • lanes ⇒ 해당 노드가 보류 중인 작업에 대한 것이다.
  • childLanes => 해당 노드의 subtree가 보류 중인 작업에 대한 것이다.

Cf. What are Lanes in React source code?

버튼을 클릭하면 setState()가 호출된다.

  1. root에서 타겟 fiber까지의 경로는 다음 렌더링에서 확인해야 하는 곳들을 나타내기 위해 laneschildLanes로 표시된다.

  2. 업데이트는 ScheduleUpdateOnFiber()에 의해 예약되며, ensureRootIsScheduled()가 호출되고 난 후에야 PerformConcurrentWorkOnRoot()가 스케줄러에서 예약된다. 이는 initial mount와 매우 유사하다.

명심해야 할 중요한 점은 이벤트의 우선순위에 따라 업데이트의 우선순위가 결정된다는 것이다. click 이벤트의 경우 DiscreteEventPriority이며, SyncLane(높은 우선순위)에 매핑된다.

https://jser.dev/2023-07-18-how-react-rerenders/#11-lanes-and-childlanes

출처: https://jser.dev/2023-07-18-how-react-rerenders/#11-lanes-and-childlanes

2. Re-render in Render phase

2.1 기본 렌더링 로직은 initial mount와 동일하다.

click 이벤트이므로, 렌더링 lane은 SyncLane이며, blocking lane에 해당된다. 그러므로 initial mount에서와 동일하게, performConcurrentWorkOnRoot() 내부에서 concurrent feature가 여전히 활성화되지 않고 동기적으로 작동한다.

Cf. concurrent feature가 활성화된 경우를 확인하고 싶다면, 다음 글을 참고하자. How does useTransition() work internally in React?

다음은 전체 프로세스를 요약한 코드다.

do {
  try {
    workLoopSync();
    break;
  } catch (thrownValue) {
    handleError(root, thrownValue);
  }
} while (true);

function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;

  next = beginWork(current, unitOfWork, subtreeRenderLanes);

  // 📌 Cf. 2.5 memoizedProps vs pendingProps
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

React는 Fiber 트리를 순회하고 필요한 경우에만 fiber를 업데이트한다.

2.2 React는 새로운 Fiber Nodes를 생성하기 전에 중복 Fiber Nodes를 재사용한다.

Initial mount에서는 Fiber가 처음부터 생성됐다. 하지만 실제로 React는 Fiber Node를 먼저 재사용하려고 한다.

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  // 📌 current는 현재 버전이고, current의 alternate은 그 이전 버전이다.
  let workInProgress = current.alternate;

  // 📌 처음부터 생성해야 하는 경우 (initial mount)
  if (workInProgress === null) {
    // We use a double buffering pooling technique because we know that we'll
    // only ever need at most two versions of a tree. We pool the "other" unused
    // node that we're free to reuse. This is lazily created to avoid allocating
    // extra objects for things that are never updated. It also allow us to
    // reclaim the extra memory if needed.
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    ...
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  // 📌 이전(previous) 버전을 사용할 수 있는 경우 (re-rendering) 
  } else {
    // 📌 재사용이 가능하므로, Fiber Node를 생성하지 않고
    // properties 업데이트만 하면 된다.
    workInProgress.pendingProps = pendingProps;
    
    // Needed because Blocks store data on type.
    workInProgress.type = current.type;
    // We already have an alternate.
    // Reset the effect tag.
    workInProgress.flags = NoFlags;
    // The effects are no longer valid.
    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null;
  }
  // Reset all effects except static ones.
  // Static effects are not specific to a render.
  workInProgress.flags = current.flags & StaticMask;
  // 📌 lanes와 childLanes는 복사되었다.
  workInProgress.childLanes = current.childLanes;
  workInProgress.lanes = current.lanes;

  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;
  ...
  return workInProgress;
}

FiberRootNode가 current를 통해 현재 Fiber 트리를 가리키고 있기 때문에, current 트리에 없는 모든 Fiber Node는 재사용이 가능하다.

리렌더링 과정에서, 중복되는 HostRootprepareFreshStack()에서 재사용될 것이다.

function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  ...
  workInProgressRoot = root;
  // 📌 root의 current는 HostRoot의 FiberNode다.
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  ...
  finishQueueingConcurrentUpdates();
  return rootWorkInProgress;
}

따라서 다음과 같이 리렌더링이 시작된다.

https://jser.dev/2023-07-18-how-react-rerenders/#22-react-reuses-redundant-fiber-nodes-before-creating-new-ones

출처: https://jser.dev/2023-07-18-how-react-rerenders/#22-react-reuses-redundant-fiber-nodes-before-creating-new-ones

2.3 beginWork()의 Update 분기

beginWork()에는 업데이트들을 처리하는 중요한 분기가 있다. (initial mount에서는 다뤄지지 않는다.)

function beginWork(
  // 📌 current는 현재 버전으로, 페인트되어 있다.
  current: Fiber | null,
  // 📌 workInProgress는 새로운 버전으로, 페인트될 것이다.
  workInProgress: Fiber,

  renderLanes: Lanes,
): Fiber | null {
  // 📌 current가 null이 아니라는 것은, NOT initial mount를 의미한다.
  // 이전 버전의 Fiber 노드가 있고, HostComponent인 경우 DOM 노드도 있다.
  // HostComponent의 경우 DOM nodes의 이전 버전도 갖고 있다.
  // 따라서 React는 subtree에서 더 깊은 곳으로 들어가는 것을 피함으로써 - bailout!
  // 최적화할 수 있다. 
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (
	  // 📌 여기에서 shallowEqual()이 아니라, === 을 사용한다.
	  // 이는 React 렌더링의 중요한 동작으로 이어진다.
      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).
      didReceiveUpdate = true;
    } else {
      // 📌 checkScheduledUpdatedOrContext()는 fibers에 있는 lanes를 체크한다.
      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
      ) {
        // No pending updates or context. Bail out now.
        didReceiveUpdate = false;
        // 📌 이 fiber에 업데이트가 없다면, React는 bailout를 시도하지만,
        // props나 context 변경이 없는 경우에만 가능하다.
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
     ...
    }
  } else {
	  // 📌 initial mount에서 다뤘던 mount 분기다.
    didReceiveUpdate = false;
    ...
  }
  workInProgress.lanes = NoLanes;
  switch (workInProgress.tag) {
    ...
    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 HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);
    ...
  }
}

2.4 attemptEarlyBailoutIfNoScheduledUpdate() 안에 있는 Bailout 로직

이 함수는 불필요한 경우 렌더링을 더 빨리 중지하려고 한다.

function attemptEarlyBailoutIfNoScheduledUpdate(
  current: Fiber,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  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;
    ...
  }
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    // Reuse previous dependencies
    workInProgress.dependencies = current.dependencies;
  }
  // Check if the children have any pending work.
  // 📌 여기에서 childLanes가 체크된다.
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // The children don't have any work either. We can skip them.
    if (enableLazyContextPropagation && current !== null) {
      // Before bailing out, check if there are any context changes in
      // the children.
      lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        return null;
      }
    } else {
      // 📌 fiber 자체와 해당 subtree에 대한 업데이트가 없으면,
      // null을 반환하여 더 깊은 트리로 이동하는 것을 멈출 수 있다.
      return null;
    }
  }
  // This fiber doesn't have work, but its subtree does. Clone the child
  // fibers and continue.
  // 📌 이름은 클론이지만, 실제로는 새 children 노드를 생성하거나 이전 노드를 재사용한다.
  cloneChildFibers(current, workInProgress);
  
  // 📌 child를 직접 반환하고, React는 이를 다음 Fiber로 처리한다.
  return workInProgress.child;
}

export function cloneChildFibers(
  current: Fiber | null,
  workInProgress: Fiber,
): void {
  if (current !== null && workInProgress.child !== current.child) {
    throw new Error('Resuming work not yet implemented.');
  }
  
  if (workInProgress.child === null) {
    return;
  }
  
  // 📌 cloneChildFibers()에서, child fibers는 이전 버전에서 생성되지만
  // reconciliation 중에 설정되는 새로운 pendingProps를 사용하여 생성된다.
  let currentChild = workInProgress.child;
  let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
  workInProgress.child = newChild;
  newChild.return = workInProgress;
  while (currentChild.sibling !== null) {
    currentChild = currentChild.sibling;
    newChild = newChild.sibling = createWorkInProgress(
      currentChild,
      currentChild.pendingProps,
    );
    newChild.return = workInProgress;
  }
  newChild.sibling = null;
}

bailout 과정을 요약해보자.

  1. ifif fiber에 props/context 변경이 없고, 보류중인 작업이 없다면(empty lanes)

    • ifif children에 보류중인 작업이 없다면(empty childLanes), bailout이 발생하고 React가 트리에서 더 깊이 이동하지 않는다.

    • otherwiseotherwise React는 이 fiber를 리렌더링하지 않고 바로 children으로 이동한다.

  2. otherwiseotherwise React는 먼저 리렌더링을 시도한 다음 children으로 이동한다.

Cf. How does React bailout work in reconciliation?

2.5 memoizedProps vs pendingProps

beginWork()에서, workInProgresscurrent와 비교(diff)된다. props의 경우, workInProgress.pendingPropscurrent.memoizedProps다. memoizedProps를 현재 props로, pendingProps는 다음 버전의 props라고 생각할 수 있다.

React는 Render 단계에서 새로운 Fiber 트리를 생성한 다음, 현재 Fiber 트리와 비교한다. pendingProps는 실제로 workInProgress 생성을 위한 매개변수임을 알 수 있다.

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  // 📌 current는 현재 버전이고, current.alternate은 그 이전 버전이다.
  let workInProgress = current.alternate;
	// 📌 처음부터 생성해야 하는 경우 (initial mount)
  if (workInProgress === null) {
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    ...
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  // 📌 이전 버전 재사용이 가능한 경우 (re-rendering)
  } else {
    // 📌 재사용이 가능하므로, Fiber Node를 생성하지 않고
    // 필요한 properties 업데이트만 하고 재사용하면 된다.
    workInProgress.pendingProps = pendingProps;
    ...
  }
  ...
  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;
  ...
  return workInProgress;
}

사실, root FiberNode constructor는 pendingProps를 매개변수로 갖고 있다.

function createFiber(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
): Fiber {
  return new FiberNode(tag, pendingProps, key, mode);
}

function FiberNode(
  this: $FlowFixMe,
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  ...
}

즉, Fiber Node를 만드는 것이 첫 번째 단계고, 나중에 작업해야 한다.

그리고 memoizedPropsPerformUnitOfWork() 내부에 있는 fiber에 대한 리렌더링이 완료될 때, pendingProps로 설정된다.

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  setCurrentDebugFiberInDEV(unitOfWork);
  
  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }
  
  resetCurrentDebugFiberInDEV();
  // 📌 작업이 끝나면, memoizedProps가 업데이트된다.
  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;
}

이제 데모를 살펴보자.

  1. React는 HostRoot(lanes: 0, childLanes: 1)에서 작업을 수행한다. HostRoot에는 props가 없고 memoizedPropspendingProps는 모두 null이므로, React는 복제(clone)된 App인 child로 바로 이동한다.

  2. React는 <App/>(lanes: 0, childLanes: 1)에서 작업을 수행한다. App component는 리렌더링되지 않아서 memoizedPropspendingProps는 동일하므로, React는 복제된 div인 child로 바로 이동한다.

  3. React는 <div/>(lanes: 0, childLanes: 1)에서 작업을 수행한다. App에서 children을 가져오지만 App은 다시 실행되지 않아서 해당 children(<Link>, <br/><Component/>) 중 어느 것도 변경되지 않으므로 React는 다시 <Link/>로 바로 이동한다.

  4. React는 <Link/>(lanes: 0, childLanes: 0)에서 작업을 수행한다. 이번에는 React가 더 깊이 들어갈 필요도 없으므로, 여기에서 멈추고 sibling인 <br/>로 이동한다.

  5. React는 <br/>(lanes: 0, childLanes: 0)에서 작업을 수행하고 bailout이 다시 발생하며, React는 <Component/>로 이동한다.

이제 뭔가가 조금 달라졌다. <Component/>lanes1이다. 이는 React가 updateFunctionComponent(current, workInProgress)에 의해 수행되는 children을 다시 렌더링하고 재조정(reconcile)해야 함을 의미한다.

지금까지 우리는 다음과 같은 상태를 얻었다.

https://jser.dev/2023-07-18-how-react-rerenders/#25-memoizedprops-vs-pendingprops

출처: https://jser.dev/2023-07-18-how-react-rerenders/#25-memoizedprops-vs-pendingprops

2.6 updateFunctionComponent()는 function 컴포넌트를 리렌더링하고 children을 재조정한다.

function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {

  let context;
  if (!disableLegacyContext) {
    const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
    context = getMaskedContext(workInProgress, unmaskedContext);
  }
  
  let nextChildren;
  let hasId;
  prepareToReadContext(workInProgress, renderLanes);
  
  // 📌 Component가 새로운 children을 생성하기 위해 실행된다는 것을 의미한다.
  nextChildren = renderWithHooks(
    current,
    workInProgress,
    Component,
    nextProps,
    context,
    renderLanes,
  );

  hasId = checkDidRenderIdHook();
  if (enableSchedulingProfiler) {
    markComponentRenderStopped();
  }
  
  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  
  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
  // 📌 nextChildren을 전달하고, reconcileChildren()이 호출된다.
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);

  return workInProgress.child;
}

reconcileChildren()은 initial mount에서 이미 만난 적이 있다. 내부적으로는 children의 type에 따라 약간의 변형이 있는데, 그 중 3가지에 집중해보자.

React에서 새로운 child fiber들을 생성하는 것뿐 아니라 기존 fiber들을 재사용하려고 시도한다는 것을 기억하자.

function reconcileChildFibersImpl(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
  lanes: Lanes,
): Fiber | null {
  ...
  // Handle object types
  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(
          // 📌 single child의 경우
          reconcileSingleElement(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
          ),
        );
      case REACT_PORTAL_TYPE:
       ...
      case REACT_LAZY_TYPE:
        ...
    }
    // 📌 children이 elements의 배열인 경우
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
    ...
  }
  
  if (
    (typeof newChild === 'string' && newChild !== '') ||
    typeof newChild === 'number'
  ) {
    // 📌 children이 text인 경우
    return placeSingleChild(
      reconcileSingleTextNode(
        returnFiber,
        currentFirstChild,
        '' + newChild,
        lanes,
      ),
    );
  }
  // Remaining cases are all treated as empty.
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

<Component/>의 경우, 단일 div를 리턴하므로, reconcileSingleElement()로 이동하자.

2.7 reconcileSingleElement()

function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  // 📌 여기서 element는 Component()의 리턴 값으로, <div/>의 element다.
  element: ReactElement,
  lanes: Lanes,
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  while (child !== null) {
    if (child.key === key) {
      const elementType = element.type;
      if (elementType === REACT_FRAGMENT_TYPE) {
        ...
      } else {
        // 📌 type이 같다면 재사용할 수 있고, 같지 않다면 그냥 deleteChild()를 한다.
        if (child.elementType === elementType || ...) {
          deleteRemainingChildren(returnFiber, child.sibling);
          // 📌 새로운 props와 기존 fiber를 사용해본다.
          // element.props는 <div/>의 props다.
          const existing = useFiber(child, element.props);
          existing.ref = coerceRef(returnFiber, child, element);
          existing.return = returnFiber;
          return existing;
        }
      }
      // Didn't match.
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      deleteChild(returnFiber, child);
    }
    child = child.sibling;
  }
  ...
}

useFiber에서, React는 이전 버전을 생성하거나 재사용한다. 전에 언급했듯이, pendingProps(children을 포함하고 있음)가 설정된다.

function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
  const clone = createWorkInProgress(fiber, pendingProps);
  clone.index = 0;
  clone.sibling = null;
  return clone;
}

따라서 Component가 리렌더링된 후, React는 새로운 <div/>인 child로 이동하며 현재 버전에서 laneschildLanes가 모두 비어있다.

2.8 컴포넌트가 리렌더링되면, 해당 subtree는 기본적으로 리렌더링된다.

<div/>와 해당 children에 예약된 작업이 없으므로, bailout이 발생할 것이라고 생각할 수 있지만, 그렇지 않다.

beginWork()에서 memoizedPropspendingProps를 체크했던 것을 기억하자.

const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
  // 📌 여기에서 shallowEqual()이 아니라, === 을 사용한다.
  oldProps !== newProps ||
  hasLegacyContextChanged() || ...
) {
  // 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).
  didReceiveUpdate = true;
} 

props를 비교할 때, shallowEqual이 사용되지 않으며, component가 렌더링될 때마다, React elements를 포함하는 새로운 객체(object)가 생성되므로 pendingProps는 매번 새로 생성된다.

<div/>의 경우, Component()가 실행되면 항상 새로운 props를 가져오므로 bailout이 전혀 발생하지 않는다.

따라서 React는 update 분기인 updateHostComponent()로 이동한다.

2.9 updateHostComponent()

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;
  const isDirectTextChild = shouldSetTextContent(type, nextProps);
  
  if (isDirectTextChild) {
    // We special case a direct text child of a host node. This is a common
    // case. We won't handle it as a reified child. We will instead handle
    // this in the host environment that also has access to this prop. That
    // avoids allocating another HostText fiber and traversing it.
    nextChildren = null;
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
    // If we're switching from a direct text child to a normal child, or to
    // empty, we need to schedule the text content to be reset.
    workInProgress.flags |= ContentReset;
  }
  
  markRef(current, workInProgress);
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

nextChildren은 다음과 같다:

[
  {$$typeof: Symbol(react.element), type: 'button'},
  " (", 
  {$$typeof: Symbol(react.element), type: 'b'}, 
  ")"
]

따라서 React는 reconcileChildrenArray()를 이용하여 재조정한다.

그리고 currentmemoizedProps는 다음과 같다.

[
  {$$typeof: Symbol(react.element), type: 'button'},
  " (", 
  {$$typeof: Symbol(react.element), type: 'span'}, 
  ")"
]

2.10 reconcileChildrenArray()는 필요에 따라 fiber를 생성하거나 삭제한다.

reconcileChildrenArray()는 약간 복잡하다. element의 재정렬이 있는지 체크함으로써 추가 최적화를 하고, key가 존재하면 fiber를 재사용하려고 시도한다.

demo에는 key가 없으므로, 기본 분기로 간다.

Cf. key가 있는 경우가 궁금하다면, 다음 글을 참고하자. How does ‘key’ work internally? List diffing in React

function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<any>,
    lanes: Lanes,
  ): Fiber | null {
    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;
    
    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    // 📌 children elements에 대해 현재 fiber를 확인한다.
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      // 📌 여기에서 list에 있는 각 fiber는 새로운 props로 확인된다.
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          // We matched the slot, but we didn't reuse the existing fiber, so we
          // need to delete the existing child.
          // 📌 fiber가 재사용될 수 없다면, Deletion으로 표시된다.
          // commit 단계에서 해당 DOM 노드가 삭제된다.
          deleteChild(returnFiber, oldFiber);
        }
      }
      // 📌 placeChild()는 fiber를 Insertion으로 표시하려고 한다.
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }
    
    if (newIdx === newChildren.length) {
      // We've reached the end of the new children. We can delete the rest.
      deleteRemainingChildren(returnFiber, oldFiber);
      if (getIsHydrating()) {
        const numberOfForks = newIdx;
        pushTreeFork(returnFiber, numberOfForks);
      }
      return resultingFirstChild;
    }
    ...
    return resultingFirstChild;
  }

updateSlot()은 기본적으로 key를 고려하여, 새로운 props로 fiber를 생성하거나 재사용한다.

function updateSlot(
  returnFiber: Fiber,
  oldFiber: Fiber | null,
  newChild: any,
  lanes: Lanes,
): Fiber | null {
  // Update the fiber if the keys match, otherwise return null.
  const key = oldFiber !== null ? oldFiber.key : null;
  
  if (
    (typeof newChild === 'string' && newChild !== '') ||
    typeof newChild === 'number'
  ) {
    // Text nodes don't have keys. If the previous node is implicitly keyed
    // we can continue to replace it without aborting even if it is not a text
    // node.
    if (key !== null) {
      return null;
    }
    return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
  }
  
  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE: {
        if (newChild.key === key) {
          return updateElement(returnFiber, oldFiber, newChild, lanes);
        } else {
          return null;
        }
      }
      ...
    }
  }
  
  return null;
}

function updateElement(
  returnFiber: Fiber,
  current: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
): Fiber {
  const elementType = element.type;
  if (elementType === REACT_FRAGMENT_TYPE) {
    return updateFragment(
      returnFiber,
      current,
      element.props.children,
      lanes,
      element.key,
    );
  }
  if (current !== null) {
    // 📌 재사용될 수 있다.
    if (current.elementType === elementType || ...) {
      // Move based on index
      // 📌 여기에서 useFiber()가 또 등장한다.
      const existing = useFiber(current, element.props);

      existing.ref = coerceRef(returnFiber, current, element);
      existing.return = returnFiber;
      return existing;
    }
  }
  // Insert
  // 📌 type이 달라서 재사용할 수 없다면, fiber를 처음부터 생성한다.
  const created = createFiberFromElement(element, returnFiber.mode, lanes);

  created.ref = coerceRef(returnFiber, current, element);
  created.return = returnFiber;
  return created;
}

<div/>의 경우, updateSlot()은 세 children을 성공적으로 재사용했다. currentspan이지만, b를 원하기 때문에 네 번째 child는 재사용되지 않았으며, 따라서 b의 fiber는 처음부터 생성되고 span의 fiber는 deleteChild()에 의해 제거된다. 새로 생성된 bplaceChild()로 표시된다.

2.11 placeChild()deleteChild()는 fiber를 flag로 표시한다.

Component 아래 있는 <div>의 children의 경우, fiber nodes를 표시하는 두 가지 함수가 있다.

function placeChild(
  newFiber: Fiber,
  lastPlacedIndex: number,
  newIndex: number,
): number {
  newFiber.index = newIndex;
  ...
  const current = newFiber.alternate;
  if (current !== null) {
    const oldIndex = current.index;
    if (oldIndex < lastPlacedIndex) {
      // This is a move.
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    } else {
      // This item can stay in place.
      return oldIndex;
    }
  } else {
    // This is an insertion.
    newFiber.flags |= Placement;
    return lastPlacedIndex;
  }
}
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
  ...
  const deletions = returnFiber.deletions;
  if (deletions === null) {
    returnFiber.deletions = [childToDelete];
    returnFiber.flags |= ChildDeletion;
  } else {
    deletions.push(childToDelete);
  }
}

삭제되어야 하는 fiber가 임시로 해당 parent의 배열 안에 들어가는 것을 봤다. 이것이 필요한 이유는, 삭제 후 더 이상 새로운 fiber 트리에 해당 fiber가 존재하지 않지만, Commit 단계에서 처리가 되어야 하므로, 어딘가에 넣어두는 것이다.

<div>는 완료되었다.

https://jser.dev/2023-07-18-how-react-rerenders/#211-placechild-and-deletechild-marks-fiber-with-flags

출처: https://jser.dev/2023-07-18-how-react-rerenders/#211-placechild-and-deletechild-marks-fiber-with-flags

다음으로 React는 button으로 이동한다. 이번에도 예약된 작업은 없지만 React는 여전히 updateHostComponent()를 사용하여 작업한다. 왜냐하면 props가 ["click me-", "1"]에서 ["click me-", "1"]로 변경되었기 떄문이다.

HostText의 경우 props가 문자열이므로, 첫 번째 "click me-"는 bailout한다. 그리고 차례로, React는 updateHostText()를 사용하여 텍스트를 재조정하려고 시도한다.

2.12 updateHostText()

function updateHostText(current, workInProgress) {
  if (current === null) {
    tryToClaimNextHydratableInstance(workInProgress);
  }
  // Nothing to do here. This is terminal. We'll do the completion step
  // immediately after.
  return null;
}

업데이트가 완료 단계(completeWork())에서 표시되기 때문에, 여기에서는 아무 일이 일어나지 않는다. 이는 initial mount에서도 설명한 적 있다.

2.13 completeWork()는 HostComponent의 업데이트를 표시하고, 필요하다면 DOM 노드를 생성한다.

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  popTreeContext(workInProgress);
  switch (workInProgress.tag) {
	...
    case FunctionComponent:
    ...
    case HostComponent: {
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      // 📌 업데이트 지점이다.
      if (current !== null && workInProgress.stateNode != null) {
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );
        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
      // 📌 initial mount 분기다.
      } else {
        ...
      }
      bubbleProperties(workInProgress);
      return null;
    }
    case HostText: {
      const newText = newProps;
      // 📌 업데이트 지점이다.
      if (current && workInProgress.stateNode != null) {
        const oldText = current.memoizedProps;
        // If we have an alternate, that means this is an update and we need
        // to schedule a side-effect to do the updates.
        updateHostText(current, workInProgress, oldText, newText);
      // 📌 initial mount 분기다.
      } else {
        ...
        if (wasHydrated) {
          if (prepareToHydrateHostTextInstance(workInProgress)) {
            markUpdate(workInProgress);
          }
        } else {
          workInProgress.stateNode = createTextInstance(
            newText,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
        }
      }
      bubbleProperties(workInProgress);
      return null;
    }
    ...
  }
}
updateHostText = function(
  // 📌 complete 단계에 있는 updateHostText()이며,
  // beginWork()에 있는 것과 다르다는 것을 주의하자.
  current: Fiber,
  workInProgress: Fiber,
  oldText: string,
  newText: string,
) {
  // If the text differs, mark it as an update. All the work in done in commitWork.
  if (oldText !== newText) {
    markUpdate(workInProgress);
  }
};

updateHostComponent = function(
  current: Fiber,
  workInProgress: Fiber,
  type: Type,
  newProps: Props,
  rootContainerInstance: Container,
) {
  // If we have an alternate, that means this is an update and we need to
  // schedule a side-effect to do the updates.
  const oldProps = current.memoizedProps;
  if (oldProps === newProps) {
    // In mutation mode, this is sufficient for a bailout because
    // we won't touch this node even if children changed.
    return;
  }
  
  // If we get updated because one of our children updated, we don't
  // have newProps so we'll have to reuse them.
  const instance: Instance = workInProgress.stateNode;
  const currentHostContext = getHostContext();
  const updatePayload = prepareUpdate(
    instance,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
    currentHostContext,
  );
  // 📌 업데이트는 updateQueue에 들어간다.
  // 이것은 실제로 Effect Hook과 같은 hook에서도 사용된다.
  workInProgress.updateQueue = (updatePayload: any);
  // If the update payload indicates that there is a change or if there
  // is a new ref we mark this as an update. All the work is done in commitWork.
  if (updatePayload) {
    markUpdate(workInProgress);
  }
};
function markUpdate(workInProgress: Fiber) {
  // Tag the fiber with an update effect. This turns a Placement into
  // a PlacementAndUpdate.
  // 📌 다른 flag다!
  workInProgress.flags |= Update;
}

Render 단계가 끝났고, 다음을 얻었다.

  1. b의 삽입
  2. span의 삭제
  3. HostText의 업데이트
  4. button의 업데이트 (empty update, do nothing)

한 가지 지적하고 싶은 점은 prepareUpdate()button과 해당 parent인 div 모두에 대해 실행되지만, div에 대해서는 null을 생성하고 button에 대해서는 []를 생성한다는 것이다. 여기서는 다루지 않을 edge case다.

https://jser.dev/2023-07-18-how-react-rerenders/#213-completework-marks-the-update-of-hostcomponent-and-creates-dom-nodes-if-necessary

출처: https://jser.dev/2023-07-18-how-react-rerenders/#213-completework-marks-the-update-of-hostcomponent-and-creates-dom-nodes-if-necessary

이제 Commit 단계에서 업데이트들을 반영할 시간이다.

3. Re-render in Commit Phase

3.1 commitMutationEffectsOnFiber()는 Insertion/Deletion/Update 반영(commit)을 시작한다.

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
  // The effect flag should be checked *after* we refine the type of fiber,
  // because the fiber tag is more specific. An exception is any flag related
  // to reconciliation, because those can be set on all fiber types.
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      // 📌 children부터 재귀적으로 처리한다.
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      // 📌 이후에 삽입(Insertion)을 한다.
      commitReconciliationEffects(finishedWork);

      // 📌 업데이트는 마지막에 한다.
      if (flags & Update) {
        try {
          commitHookEffectListUnmount(
            HookInsertion | HookHasEffect,
            finishedWork,
            finishedWork.return,
          );
          commitHookEffectListMount(
            HookInsertion | HookHasEffect,
            finishedWork,
          );
        } catch (error) {
          captureCommitPhaseError(finishedWork, finishedWork.return, error);
        }
        ...
      }
      return;
    }
    case HostComponent: {
      // 📌 children부터 재귀적으로 처리한다.
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      // 📌 이후에 삽입(Insertion)을 한다.
      commitReconciliationEffects(finishedWork);

      if (supportsMutation) {
        if (finishedWork.flags & ContentReset) {
          const instance: Instance = finishedWork.stateNode;
          try {
            resetTextContent(instance);
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
        // 📌 업데이트는 마지막에 한다.
        if (flags & Update) {
          const instance: Instance = finishedWork.stateNode;
          if (instance != null) {
            // Commit the work prepared earlier.
            const newProps = finishedWork.memoizedProps;
            // For hydration we reuse the update path but we treat the oldProps
            // as the newProps. The updatePayload will contain the real change in
            // this case.
            const oldProps =
              current !== null ? current.memoizedProps : newProps;
            const type = finishedWork.type;
            const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
            finishedWork.updateQueue = null;
            if (updatePayload !== null) {
              try {
                // 📌 HostComponent의 경우, props를 업데이트한다.
                commitUpdate(
                  instance,
                  updatePayload,
                  type,
                  oldProps,
                  newProps,
                  finishedWork,
                );
              } catch (error) {
                captureCommitPhaseError(
                  finishedWork,
                  finishedWork.return,
                  error,
                );
              }
            }
          }
          
        }
      }
      return;
    }
    case HostText: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      if (flags & Update) {
        if (supportsMutation) {
          if (finishedWork.stateNode === null) {
            throw new Error(
              'This should have a text node initialized. This error is likely ' +
                'caused by a bug in React. Please file an issue.',
            );
          }
          const textInstance: TextInstance = finishedWork.stateNode;
          const newText: string = finishedWork.memoizedProps;
          // For hydration we reuse the update path but we treat the oldProps
          // as the newProps. The updatePayload will contain the real change in
          // this case.
          const oldText: string =
            current !== null ? current.memoizedProps : newText;
          try {
            // 📌 HostText의 경우, textContent를 업데이트한다.
            commitTextUpdate(textInstance, oldText, newText);
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }
  }
}

이것이 재귀적인 과정임을 알 수 있으며, 각 mutation 유형을 자세히 살펴보자.

3.2 children과 본인을 처리하기 전에, 삭제(Deletion)가 가장 먼저 수행된다.

function recursivelyTraverseMutationEffects(
  root: FiberRoot,
  parentFiber: Fiber,
  lanes: Lanes,
) {
  // Deletions effects can be scheduled on any fiber type. They need to happen
  // before the children effects hae fired.
  const deletions = parentFiber.deletions;
  if (deletions !== null) {
    for (let i = 0; i < deletions.length; i++) {
      const childToDelete = deletions[i];
      try {
        commitDeletionEffects(root, parentFiber, childToDelete);
      } catch (error) {
        captureCommitPhaseError(childToDelete, parentFiber, error);
      }
    }
  }
  const prevDebugFiber = getCurrentDebugFiberInDEV();
  if (parentFiber.subtreeFlags & MutationMask) {
    let child = parentFiber.child;
    while (child !== null) {
      setCurrentDebugFiberInDEV(child);
      commitMutationEffectsOnFiber(child, root, lanes);
      child = child.sibling;
    }
  }
  setCurrentDebugFiberInDEV(prevDebugFiber);
}

삭제는 children을 처리하기 전이라도, 가장 먼저 처리된다.

function commitDeletionEffects(
  root: FiberRoot,
  returnFiber: Fiber,
  deletedFiber: Fiber,
) {
  if (supportsMutation) {
    // We only have the top Fiber that was deleted but we need to recurse down its
    // children to find all the terminal nodes.
    // Recursively delete all host nodes from the parent, detach refs, clean
    // up mounted layout effects, and call componentWillUnmount.
    // We only need to remove the topmost host child in each branch. But then we
    // still need to keep traversing to unmount effects, refs, and cWU. TODO: We
    // could split this into two separate traversals functions, where the second
    // one doesn't include any removeChild logic. This is maybe the same
    // function as "disappearLayoutEffects" (or whatever that turns into after
    // the layout phase is refactored to use recursion).
    // Before starting, find the nearest host parent on the stack so we know
    // which instance/container to remove the children from.
    let parent = returnFiber;
    // 📌 parent 노드가 필요하지 않다는 것은 backing DOM이 있다는 의미다.
    // 여기에서 backing DOM을 가진 가장 가까운 Fiber 노드를 찾는다.
    findParent: while (parent !== null) {
      switch (parent.tag) {
        case HostComponent: {
          hostParent = parent.stateNode;
          hostParentIsContainer = false;
          break findParent;
        }
        case HostRoot: {
          hostParent = parent.stateNode.containerInfo;
          hostParentIsContainer = true;
          break findParent;
        }
        case HostPortal: {
          hostParent = parent.stateNode.containerInfo;
          hostParentIsContainer = true;
          break findParent;
        }
      }
      parent = parent.return;
    }
    if (hostParent === null) {
      throw new Error(
        'Expected to find a host parent. This error is likely caused by ' +
          'a bug in React. Please file an issue.',
      );
    }
    commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
    hostParent = null;
    hostParentIsContainer = false;
  } else {
    // Detach refs and call componentWillUnmount() on the whole subtree.
    commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
  }
  
  detachFiberMutation(deletedFiber);
}
function commitDeletionEffectsOnFiber(
  finishedRoot: FiberRoot,
  nearestMountedAncestor: Fiber,
  deletedFiber: Fiber,
) {
  onCommitUnmount(deletedFiber);
  // The cases in this outer switch modify the stack before they traverse
  // into their subtree. There are simpler cases in the inner switch
  // that don't modify the stack.
  switch (deletedFiber.tag) {
    case HostComponent: {
      if (!offscreenSubtreeWasHidden) {
        safelyDetachRef(deletedFiber, nearestMountedAncestor);
      }
      // Intentional fallthrough to next branch
    }
    case HostText: {
      // We only need to remove the nearest host child. Set the host parent
      // to `null` on the stack to indicate that nested children don't
      // need to be removed.
      if (supportsMutation) {
        const prevHostParent = hostParent;
        const prevHostParentIsContainer = hostParentIsContainer;
        hostParent = null;
        recursivelyTraverseDeletionEffects(
          finishedRoot,
          nearestMountedAncestor,
          deletedFiber,
        );
        hostParent = prevHostParent;
        hostParentIsContainer = prevHostParentIsContainer;
        if (hostParent !== null) {
          // Now that all the child effects have unmounted, we can remove the
          // node from the tree.
          if (hostParentIsContainer) {
            removeChildFromContainer(
              // 📌 이 hostParent는 이전 while문에서 찾았다.
              ((hostParent: any): Container),
              (deletedFiber.stateNode: Instance | TextInstance),
            );
          } else {
            removeChild(
              ((hostParent: any): Instance),
              (deletedFiber.stateNode: Instance | TextInstance),
            );
          }
        }
      } else {
        recursivelyTraverseDeletionEffects(
          finishedRoot,
          nearestMountedAncestor,
          deletedFiber,
        );
      }
      return;
    }
    ...
    default: {
      recursivelyTraverseDeletionEffects(
        finishedRoot,
        nearestMountedAncestor,
        deletedFiber,
      );
      return;
    }
  }
}

https://jser.dev/2023-07-18-how-react-rerenders/#32-deletion-are-processed-first-before-processing-children-and-self

출처: https://jser.dev/2023-07-18-how-react-rerenders/#32-deletion-are-processed-first-before-processing-children-and-self

3.3 삽입(Insertion)이 그 다음에 처리된다.

이는 새로 생성된 노드를 트리 구조로 설정할 수 있도록 하기 위한 것이다.

function commitReconciliationEffects(finishedWork: Fiber) {
  // Placement effects (insertions, reorders) can be scheduled on any fiber
  // type. They needs to happen after the children effects have fired, but
  // before the effects on this fiber have fired.
  const flags = finishedWork.flags;
  if (flags & Placement) {
    try {
      commitPlacement(finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
    // Clear the "placement" from effect tag so that we know that this is
    // inserted, before any life-cycles like componentDidMount gets called.
    finishedWork.flags &= ~Placement;
  }
  if (flags & Hydrating) {
    finishedWork.flags &= ~Hydrating;
  }
}
function commitPlacement(finishedWork: Fiber): void {
  if (!supportsMutation) {
    return;
  }
  // Recursively insert all host nodes into the parent.
  const parentFiber = getHostParentFiber(finishedWork);
  // Note: these two variables *must* always be updated together.
  switch (parentFiber.tag) {
    case HostComponent: {
      const parent: Instance = parentFiber.stateNode;
      if (parentFiber.flags & ContentReset) {
        // Reset the text content of the parent before doing any insertions
        resetTextContent(parent);
        // Clear ContentReset from the effect tag
        parentFiber.flags &= ~ContentReset;
      }
      
      // 📌 이것은 중요하다. Node.insertBefore()는 sibling 노드가 필요하다.
      // sibling 노드를 찾을 수 없다면, 끝에 추가하면 된다.
      const before = getHostSibling(finishedWork);
      // We only have the top Fiber that was inserted but we need to recurse down its
      // children to find all the terminal nodes.
      insertOrAppendPlacementNode(finishedWork, before, parent);
      break;
    }
    case HostRoot:
    case HostPortal: {
      const parent: Container = parentFiber.stateNode.containerInfo;
      const before = getHostSibling(finishedWork);
      insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
      break;
    }
    default:
      throw new Error(
        'Invalid host parent fiber. This error is likely caused by a bug ' +
          'in React. Please file an issue.',
      );
  }
}

function insertOrAppendPlacementNodeIntoContainer(
  node: Fiber,
  before: ?Instance,
  parent: Container,
): void {
  const {tag} = node;
  const isHost = tag === HostComponent || tag === HostText;
  if (isHost) {
    const stateNode = node.stateNode;
    if (before) {
      insertInContainerBefore(parent, stateNode, before);
    } else {
      appendChildToContainer(parent, stateNode);
    }
  } else if (tag === HostPortal) {
    // If the insertion itself is a portal, then we don't want to traverse
    // down its children. Instead, we'll get insertions from each child in
    // the portal directly.
  } else {
    const child = node.child;
    if (child !== null) {
      insertOrAppendPlacementNodeIntoContainer(child, before, parent);
      let sibling = child.sibling;
      while (sibling !== null) {
        insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

function insertOrAppendPlacementNode(
  node: Fiber,
  before: ?Instance,
  parent: Instance,
): void {
  const {tag} = node;
  const isHost = tag === HostComponent || tag === HostText;
  if (isHost) {
    const stateNode = node.stateNode;
    if (before) {
      insertBefore(parent, stateNode, before);
    } else {
      appendChild(parent, stateNode);
    }
  } else if (tag === HostPortal) {
    // If the insertion itself is a portal, then we don't want to traverse
    // down its children. Instead, we'll get insertions from each child in
    // the portal directly.
  } else {
    const child = node.child;
    if (child !== null) {
      insertOrAppendPlacementNode(child, before, parent);
      let sibling = child.sibling;
      while (sibling !== null) {
        insertOrAppendPlacementNode(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

https://jser.dev/2023-07-18-how-react-rerenders/#33-insertions-are-processed-next

출처: https://jser.dev/2023-07-18-how-react-rerenders/#33-insertions-are-processed-next

3.4 업데이트(Update)가 마지막으로 처리된다.

Update 분기는 commitMutationEffectsOnFiber()에 있다.

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
  
  // The effect flag should be checked *after* we refine the type of fiber,
  // because the fiber tag is more specific. An exception is any flag related
  // to reconciliation, because those can be set on all fiber types.
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      
      // 📌 FunctionComponent의 경우, hooks이 실행되어야 한다는 것을 의미한다.
      if (flags & Update) {
        try {
          commitHookEffectListUnmount(
            HookInsertion | HookHasEffect,
            finishedWork,
            finishedWork.return,
          );
          commitHookEffectListMount(
            HookInsertion | HookHasEffect,
            finishedWork,
          );
        } catch (error) {
          captureCommitPhaseError(finishedWork, finishedWork.return, error);
        }
        // Layout effects are destroyed during the mutation phase so that all
        // destroy functions for all fibers are called before any create functions.
        // This prevents sibling component effects from interfering with each other,
        // e.g. a destroy function in one component should never override a ref set
        // by a create function in another component during the same commit.
        if (
          enableProfilerTimer &&
          enableProfilerCommitHooks &&
          finishedWork.mode & ProfileMode
        ) {
          try {
            startLayoutEffectTimer();
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
          recordLayoutEffectDuration(finishedWork);
        } else {
          try {
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }
    case HostComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      if (flags & Ref) {
        if (current !== null) {
          safelyDetachRef(current, current.return);
        }
      }
      if (supportsMutation) {
        if (finishedWork.flags & ContentReset) {
          const instance: Instance = finishedWork.stateNode;
          try {
            resetTextContent(instance);
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
        
        // 📌 HostComponent의 경우, element 속성들이 업데이트되어야 한다는 것을 의미한다.
        if (flags & Update) {
          const instance: Instance = finishedWork.stateNode;
          if (instance != null) {
            // Commit the work prepared earlier.
            const newProps = finishedWork.memoizedProps;
            // For hydration we reuse the update path but we treat the oldProps
            // as the newProps. The updatePayload will contain the real change in
            // this case.
            const oldProps =
              current !== null ? current.memoizedProps : newProps;
            const type = finishedWork.type;
            const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
            finishedWork.updateQueue = null;
            if (updatePayload !== null) {
              try {
                commitUpdate(
                  instance,
                  updatePayload,
                  type,
                  oldProps,
                  newProps,
                  finishedWork,
                );
              } catch (error) {
                captureCommitPhaseError(
                  finishedWork,
                  finishedWork.return,
                  error,
                );
              }
            }
          }
        }
      }
      return;
    }
    case HostText: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      
      if (flags & Update) {
        if (supportsMutation) {
          if (finishedWork.stateNode === null) {
            throw new Error(
              'This should have a text node initialized. This error is likely ' +
                'caused by a bug in React. Please file an issue.',
            );
          }
          const textInstance: TextInstance = finishedWork.stateNode;
          const newText: string = finishedWork.memoizedProps;
          // For hydration we reuse the update path but we treat the oldProps
          // as the newProps. The updatePayload will contain the real change in
          // this case.
          const oldText: string =
            current !== null ? current.memoizedProps : newText;
          try {
            commitTextUpdate(textInstance, oldText, newText);
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }
    ...
    default: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      return;
    }
  }
}
export function commitUpdate(
  domElement: Instance,
  updatePayload: Array<mixed>,
  type: string,
  oldProps: Props,
  newProps: Props,
  internalInstanceHandle: Object,
): void {
  // Apply the diff to the DOM node.
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
  // Update the props handle so that we know which props are the ones with
  // with current event handlers.
  updateFiberProps(domElement, newProps);
}

export function commitTextUpdate(
  textInstance: TextInstance,
  oldText: string,
  newText: string,
): void {
  textInstance.nodeValue = newText;
}

트리 구조이기 때문에, demo의 mutations는 다음 순서로 처리된다.

  1. span 삭제
  2. HostText 업데이트
  3. button 업데이트 (empty update, do nothing)
  4. b 삽입

💡 Mutation 순서

  • Deletion → Insertion → Update
  • Deletion의 경우, children에 Update나 Insertion이 있더라도 먼저 수행
    Cf. Deletion are processed first, before processing children and self.
  • Insertion의 경우,
    • children에 Update가 있다면 children의 Update 먼저 수행
    • sibling에 Update가 있다면 Insertion 수행 후 Update 수행

4. Summary

리렌더링 과정을 요약해보면 다음과 같다.

  1. state가 변경되면, target Fiber Node로 가는 경로가 laneschildLanes로 표시되어, 해당 노드 또는 subtree의 리렌더링 여부를 나타낸다.

  2. React는 불필요한 리렌더링을 피하기 위해 bailout으로 최적화를 하며, 전체 Fiber 트리를 리렌더링한다.

  3. 컴포넌트가 리렌더링되면, 새 React elements가 생성되고, 해당 children은 변경 사항이 없더라도 모두 새로운 props를 얻으므로, React는 기본적으로 전체 Fiber 트리를 리렌더링한다. 이런 경우 useMemo()가 필요할 수 있다.

  4. “리렌더링”함으로써, Reacts는 현재의 Fiber 트리에서 새로운 Fiber 트리를 생성하고, 필요한 경우 Placement, ChildDeleteionUpdate flag로 Fiber 노드에 표시를 남긴다.

  5. 새로운 Fiber 트리가 완성되면, React는 위의 flag를 사용하여 Fiber 노드를 처리하고 Commit 단계에서 Host DOM에 변경 사항을 적용한다.

  6. 그런 다음 새 Fiber 트리가 현재 Fiber 트리로 지정된다. 이전 Fiber 트리의 노드는 다음 렌더링에 재사용될 수 있다.

JSer가 그린 re-render 순서도 슬라이드를 보면 이해에 큰 도움이 된다!
🔗 https://jser.dev/2023-07-18-how-react-rerenders/#how-react-re-renders-internally

References

profile
원리를 파헤치는 것을 좋아하는 프론트엔드 개발자입니다 🏃🏻‍♀️

0개의 댓글