Amada Coding Club

[React Core Deep Dive] How does React re-render internally? 본문

Mash-Up/Study

[React Core Deep Dive] How does React re-render internally?

아마다회장 2024. 4. 22. 18:46

다음은 리액트가 리렌더링 될 때 어떤 과정이 일어나는 지에 대한 내용이다. 우선 직역을 해보자

React가 초기 마운트를 수행하고 전체 DOM을 처음부터 생성하는 방법에 대해 설명했습니다. 초기 마운트 후 다시 렌더링할 때 React는 재조정 과정을 통해 가능한 한 DOM을 재사용하려고 합니다. 이 에피소드에서는 아래 데모에서 버튼을 클릭한 후 React가 리렌더링할 때 실제로 어떤 일이 일어나는지 알아보겠습니다.

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는 첫 마운트에서 파이버 트리와 DOM 트리를 구성하며, 완료되면 아래와 같이 두 개의 트리가 생깁니다.

왼쪽이 만들어진 파이버 트리, 오른쪽이 DOM 트리다

1.1 lanes and childLanes

Lane은 보류 중인 작업의 우선순위입니다. Fiber Node의 경우

Lane은 비트마스킹 형태로 되어있다. 또한, 리액트에서 effect tag나 여러 태그값을 비트마스크로 관리한다.
const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000
const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000

const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000010

const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000001000

const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000100000

const SyncUpdateLanes: Lane = /*                */ 0b0000000000000000000000000101010

const TransitionLanes: Lanes = /*                       */ 0b0000000011111111111111110000000
const TransitionLane1: Lane = /*                        */ 0b0000000000000000000000010000000
/*...*/
const TransitionLane16: Lane = /*                       */ 0b0000000010000000000000000000000

const RetryLanes: Lanes = /*                            */ 0b0000111100000000000000000000000
const RetryLane1: Lane = /*                             */ 0b0000000100000000000000000000000
/*...*/
const RetryLane4: Lane = /*                             */ 0b0000100000000000000000000000000​


  1. lanes => 자체의 보류 중인 작업
  2. childLanes => 하위 트리의 보류 중인 작업을 위한 것

버튼을 클릭하면 setState() 가 호출됩니다:

  1. 루트에서 대상 Fiber까지의 경로에 lanes와 childLanes가 표시되어 다음 렌더링에서 확인해야 할 위치를 나타냅니다.
  2. scheduleUpdateOnFiber()에 의해 업데이트가 예약되고, 결국 ensureRootIsScheduled( )가 호출되고 스케줄러에서 performConcurrentWorkOnRoot( )가 예약됩니다. 이는 첫 번째 마운트와 매우 유사합니다.

명심해야 할 한 가지 중요한 점은 이벤트의 우선순위에 따라 업데이트의 우선순위가 결정된다는 것입니다. 클릭 이벤트의 경우 우선순위가 높은 SyncLane에 매핑되는 DiscreteEventPriority입니다.

 

자세한 내용은 여기서는 생략하겠지만 결국에는 Fiber Tree를 따라 작업하게 됩니다.

 

각 Fiber Node에 적혀있는 lanes 값에 따라 작업하는 Node로 이동함

2. Re-render in Render phase.

2.1 기본 렌더링 로직은 초기 마운트와 동일합니다.

클릭 이벤트에서 렌더링 레인은 차단 레인인 SyncLane이기 때문에. 초기 마운트와 마찬가지로 performConcurrentWorkOnRoot() 내부에서는 여전히 동시 모드가 활성화되지 않습니다.

 

아래는 전체 프로세스를 요약한 코드입니다.

 

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);
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
	//위 라인은 중요하기 때문에 2.5 memoizedProps vs pendingProps에서 다룬다.

  if (next === null) {
    // 이렇게 해도 새 작업이 생성되지 않으면 현재 작업을 완료하세요.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

여기서는 React가 Fiber Tree를 탐색하고 필요한 경우 Fiber를 업데이트한다는 점만 기억하세요.

 

2.2 React는 새로운 노드를 생성하기 전에 중복된 파이버 노드를 재사용합니다.

 

초기 마운트에서 우리는 Fiber가 처음부터 생성되는 것을 볼 수 있습니다. 하지만 실제로 React는 먼저 파이버 노드를 재사용하려고 시도합니다.

 

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  /*
    current는 현재 버전
    대체 버전은 그 이전 버전입니다.
  */
  
  //%%%%%%%
  //즉 current는 기존의 fiber를 의미하는데 
  //alternate는 지금 사용하고 있는 fiber이외의 다른 fiber를 의미한다. 
  //만약 alternate, 즉 파이버트리가 이미 두 개 존재하는 경우 다른 파이버를 사용한다!
  //%%%%%%%

  if (workInProgress === null) {
  /*처음부터 새로 만들어야 하는 경우*/
  //%%%%%%%%fiber가 하나만 있는 경우%%%%%%%%%%%

  // 이중 버퍼링 풀링 기법을 사용하는 이유는 다음과 같습니다.
  // 트리의 버전은 최대 두 개만 필요하기 때문입니다. 
  // 사용하지 않는 "다른" 노드를 풀링하여 재사용할 수 있도록 합니다. 
  // 이 풀링 기법은 절대 사용하지 않는 객체에 대해
  // 여분의 객체를 할당하지 않기 위해 느리게 만들어집니다.
  // 또한, 필요한 경우 여분의 메모리를 회수할 수도 있습니다.
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    ...
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
  /*
    이전 버전을 재사용할 수 있다면?
  */

    workInProgress.pendingProps = pendingProps;
    
    /*
      재사용할 수 있으므로 파이버 노드를 만들 필요가 없습니다.
      속성을 업데이트하여 재사용할 수 있습니다.
    */

    // 블록은 유형에 데이터를 저장하기 때문에 필요합니다.
    workInProgress.type = current.type;
    // 이미 다른 대안이 있습니다.
    // effect tag를 리셋.
    workInProgress.flags = NoFlags;
    // effects가 더 이상 유효하지 않습니다.
    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null;
  }
  // 정적 effect를 제외한 모든 effect 초기화.
  // 정적 effect는 렌더링에만 국한되지 않음.
  workInProgress.flags = current.flags & StaticMask;
  workInProgress.childLanes = current.childLanes;
  workInProgress.lanes = current.lanes;
  /*
	lanes and childLanes가 복사됨!
  */

  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;
  ...
  return workInProgress;
  //%%%%%%%%workInProgress가 이제 사용할 fiber를 나타낸다.%%%%%%%%%
}

 

FiberRootNode는 current를 통해 현재의 Fiber Tree를 가리키기 때문에 지금 트리에 없는 모든 Fiber Node를 재사용할 수 있습니다.

리렌더링 프로세스에서 중복된 HostRootprepareFreshStack()에서 재사용됩니다.

 

function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  ...
  workInProgressRoot = root;
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  /*
  	루트의 현재 상태는 HostRoot의 FiberNode입니다.
  */

  workInProgress = rootWorkInProgress;
  workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
  workInProgressRootExitStatus = RootInProgress;
  workInProgressRootFatalError = null;
  workInProgressRootSkippedLanes = NoLanes;
  workInProgressRootInterleavedUpdatedLanes = NoLanes;
  workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
  workInProgressRootPingedLanes = NoLanes;
  workInProgressRootConcurrentErrors = null;
  workInProgressRootRecoverableErrors = null;
  finishQueueingConcurrentUpdates();
  return rootWorkInProgress;
}

 

따라서 다음 시작점부터 다시 렌더링을 시작합니다.

 

2.3 The Update branch in beginWork()

beginWork()는 업데이트에서 제일 중요한 함수 로직

컴포넌트의 상태나 Props의 변경을 확인해서 렌더링이 필요한지 필요 없는지 체크한다.

렌더링이 생략되는 경우는 다음과 같다.

1. current !== null

  current가 null이 아니다. 즉, 이미 렌더링이 된 적 있는 상태여야한다. 

2. oldProps === newProps

  이전 Props와 새로운 Props의 값이 같아야 한다. 

  (여기서 말하는 Props는 우리가 생각하는 함수의 arguments 가 맞다.)
  여기서 얕은 비교?를 한다는데 이거는 잘 모르겠다. 그래서 그 useMemo 이런거에 조심해야하는 그런건가

3. hasLegacyContextChanged() === false
  변경된 Context값이 없어야 한다!
  Consumer일 경우에 Context가 변경되면 렌더링이 일어나는 게 맞기 때문에 당연히 필요하다.

4. hasScheduledUpdateOrContext === false

  이 컴포넌트가 업데이트가 예약되어 있지 않아야 한다. 


 

function beginWork(
  current: Fiber | null,
  /*
  current는 페인트 중인 현재 버전입니다.
  */

  workInProgress: Fiber,
  /*
  workInProgress는 새 버전으로 칠할 새 버전입니다.
  */

  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
  /*
  현재가 null이 아니라면 초기 마운트가 아님을 의미합니다,
  이전 버전의 파이버 노드와 DOM 노드가 있다는 뜻입니다.
  따라서 React는 하위 트리에서 더 깊게 들어가는 것을 피함으로써
  최적화할 수 있습니다 - 구제 조치 방법!
  */


    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (
      oldProps !== newProps ||
      /* 
      여기서는 === 아닌 얕은 등호를 사용하여 React 렌더링의 중요한 동작을 유도합니다.
      */

      hasLegacyContextChanged() ||
      // 핫 리로드로 인해 구현이 변경된 경우 강제로 다시 렌더링하기:
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // props이나 context가 변경된 경우 fiber가 작업을 수행한 것으로 표시합니다..
      // 나중에 props가 동일하다고 판단되면 이 설정이 해제될 수 있습니다(메모)..
      didReceiveUpdate = true;
    } else {
      // props나 레거시 컨텍스트는 변경되지 않습니다. 
      // 보류 중인 업데이트 또는 컨텍스트 변경이 있는지 확인하세요.
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
      /*
      이렇게 하면 fiber의 lanes를 확인합니다.
      */

        current,
        renderLanes,
      );
      if (
        !hasScheduledUpdateOrContext &&
        // 오류 또는 서스펜스 경계의 두 번째 통과인 경우
        // 'current'에 예약된 작업이 없을 수 있으므로 이 플래그를 확인합니다.
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        // 보류 중인 업데이트 또는 컨텍스트가 없습니다. 탈퇴(종료?)합니다.
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(
        /*
          이 파이버에 대한 업데이트가 없는 경우
          React는 프롭이나 컨텍스트 변경이 없는 경우에만 바이아웃을 시도합니다.
        */
          current,
          workInProgress,
          renderLanes,
        );
      }
     ...
    }
  } else {
    didReceiveUpdate = false;
    /*
      이것은 이전에 이미 다룬 마운트 브랜치입니다.
    */

    ...
  }
  workInProgress.lanes = NoLanes;
  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderLanes,
      );
    }
    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 Bailout logic inside attemptEarlyBailoutIfNoScheduledUpdate()

리렌더링이 필요없는 컴포넌트의 경우(위 4가지 조건을 가지는 경우) 리렌더링 작업을 탈출시켜줘야 되는데 그 함수가 attemptEarlyBailoutIfNoScheduledUpdate() 이다.

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) {
    // 이전 디펜던시를 재사용
    workInProgress.dependencies = current.dependencies;
  }
  // 자식 노드에게서 보류 중인 작업(lanes)이 있는지 확인합니다.
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
  /*
  	여기에서는 childLanes가 체크된 것을 볼 수 있습니다.
  */

    // 자식 노드들에 보류 중인 작업이 없는 경우, 건너뛸 수 있습니다. 
    // TODO: 다시 재시작을 추가한 후에는 자식들이 진행 중인 세트인지 확인해야 합니다. 
    // 그렇다면 그 효과를 전송해야 합니다.
    if (enableLazyContextPropagation && current !== null) {
      // 종료되기 전에 자식에게 컨텍스트 변경 사항이 있는지 확인하세요.
      lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        return null;
      }
    } else {
      return null;
      /*
      따라서 파이버 자체와 그 하위 트리에 대한 업데이트가 없는 경우 
      당연히 null을 반환하여 트리에서 더 이상 작업을 진행하지 않을 수 있습니다.
      */

    }
  }
  // 이 파이버에는 작업이 없지만 그 하위 트리에는 작업이 있습니다. 하위 파이버를 복제하고 계속합니다.
  cloneChildFibers(current, workInProgress);
  /*
  이름은 복제이지만 실제로는 새로운 자식 노드를 만들거나 이전 노드를 재사용합니다.
  */
  return workInProgress.child;
  /*
  	자식을 바로 반환하고, React는 다음 fiber로 처리합니다 
  */
}
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;
  }
  let currentChild = workInProgress.child;
  let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
  /*
  	따라서 cloneChildFibers()에서 자식 fiber는 이전 버전에서 생성되지만
    조정 중에 설정되는 새로운 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;
}

 

그리고 bailoutOnAlreadyFinishedWork에서 자식 노드에서 업데이트가 발생했는지 확인 후에 업데이트가 있는 경우 cloneChildFiber를 통해 자식 노드를 작업할 수 있도록 함

 

강제 종료(bailout) 프로세스를 요약해 보겠습니다.

  1. fiber에 props/컨텍스트 변경이 없고 보류 중인 작업(빈 lanes)이 없는 경우
  2. 그렇지 않으면 React는 먼저 리렌더링을 시도한 다음 그 자식에게 이동합니다.

2.5 memoizedProps vs pendingProps.

 

beginWork()에서 workInProgress는 current와 비교됩니다. props의 경우, workInProgress.pendingProps가 current.memoizedProps와 비교됩니다. memoizedProps는 현재 프로퍼티이고, pendingProps는 다음 버전이라고 생각할 수 있습니다.

 

React는 렌더링 단계에서 새로운 fiber tree를 생성한 다음 현재 fiber tree와 비교합니다. pendingProps는 실제로 workInProgress 생성을 위한 매개변수라는 것을 알 수 있습니다.

 

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

  if (workInProgress === null) {
  /*
  처음부터 새로 만들어야 하는 경우
  */

    //이중 버퍼링 풀링 기법을 사용하는 이유는 
    //트리의 버전이 최대 두 개만 필요하다는 것을 알기 때문입니다. 
    //사용하지 않는 "다른" 노드는 자유롭게 재사용할 수 있도록 풀링합니다. 
    //이는 업데이트되지 않는 항목에 여분의 오브젝트를 할당하지 않기 위해 느리게 생성됩니다. 
    //또한 필요한 경우 여분의 메모리를 회수할 수 있습니다.
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    ...
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
  /*
  이전 버전을 재사용할 수 있다면
  */

    workInProgress.pendingProps = pendingProps;
    /*
    재사용이 가능하므로 파이버 노드를 생성할 필요 없이 필요한 프로퍼티를 업데이트하여 재사용하면 됩니다.
    */

    //블록은 유형에 데이터를 저장하기 때문에 필요합니다.
    workInProgress.type = current.type;
    // 이미 다른 대안이 있습니다.
    // effect tag를 리셋
    workInProgress.flags = NoFlags;
    // effect는 더이상 유효하지 않음
    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null;
  }
  // 정적 이펙트를 제외하고 모두 리셋
  // 정적 이펙트는 렌더링에만 국한되지 않음
  workInProgress.flags = current.flags & StaticMask;
  workInProgress.childLanes = current.childLanes;
  workInProgress.lanes = current.lanes;
  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;
  // 종속성 객체를 복제합니다. 렌더링 단계에서 변경되므로 현재 파이버와 공유할 수 없습니다.
  const currentDependencies = current.dependencies;
  workInProgress.dependencies =
    currentDependencies === null
      ? null
      : {
          lanes: currentDependencies.lanes,
          firstContext: currentDependencies.firstContext,
        };
  // 이는 부모가 조정하는 동안 재정의됩니다.
  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;
  workInProgress.refCleanup = current.refCleanup;
  return workInProgress;
}

실제로 루트 FiberNode 생성자에는 pendingProps가 매개변수로 있습니다.

 

function createFiber(
  tag: WorkTag,
  pendingProps: mixed, //가지고 있음!
  key: null | string, 
  mode: TypeOfMode,
): Fiber {
  // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
  return new FiberNode(tag, pendingProps, key, mode);
}
function FiberNode(
  this: $FlowFixMe,
  tag: WorkTag,
  pendingProps: mixed, //가지고 있음!
  key: null | string,
  mode: TypeOfMode,
) {
  ....

따라서 파이버 노드를 만드는 것이 첫 번째 단계입니다. 나중에 작업해야 합니다.

그리고 memoizedProps는 파이버에 대한 리렌더링이 완료될 때 pendingProps로 설정되며, 이는 performUnitOfWork() 내부에 있습니다.

 

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();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  /*
  작업이 완료된 후 memoizedProps가 업데이트됩니다.
  */

  if (next === null) {
    // 이렇게 해도 새 작업이 생성되지 않으면 현재 작업을 완료합니다.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
  ReactCurrentOwner.current = null;
}

 

이제 데모를 살펴보겠습니다.

  1. React는 HostRoot(lanes: 0, childLanes: 1)에서 작동합니다. HostRoot에는 props가 없고 memoizedProps와 pendingProps는 모두 null이므로 React는 바로 그 자식인 복제된 App으로 이동합니다.
  2. React는 <App/>(lanes: 0, childLanes: 1)에서 작동합니다. App 컴포넌트는 다시 렌더링되지 않으므로 memoizedProps와 pendingProps가 동일하므로 React는 그 자식인 복제된 div로 바로 이동합니다.
  3. React는 <div/>(lanes: 0, childLanes: 1)에서 작동합니다. App에서 자식을 가져오지만 App이 재실행되지 않으므로 자식(<Link>, <br/> 및 <Component/>)이 변경되지 않으므로 다시 React는 <Link/>로 바로 이동합니다.
  4. React는 <Link/>(lanes: 0, childLanes: 0)에서 작동합니다. 이번에는 React가 더 깊이 들어갈 필요도 없으므로 여기서 멈추고 그 형제 - <br/>로 이동합니다.
  5. React는 <br/>(lanes: 0, childLanes: 0)에서 작동하고, 구제금융이 다시 발생하고, React는 <Component/>로 이동합니다.

이제 뭔가 조금 달라졌습니다. <Component/>의 레인은 1이므로 React가 다시 렌더링하고 자식들을 조정해야 하는데, 이 작업은 updateFunctionComponent(current, workInProgress)를 통해 수행됩니다.

 

지금까지 다음과 같은 상태를 얻습니다.

 

2.6 updateFunctionComponent()는 함수 컴포넌트를 다시 렌더링하고 자식을 조정합니다.

해당 함수에서 컴포넌트에 대한 리렌더링 과정과 자식의 재조정 작업이 발생한다. 

 

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);
  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;
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  /*
  nextChildren을 전달하고 reconcileChildren()이 호출됩니다.
  */

  return workInProgress.child;
}

우리는 React가 초기 마운트를 수행하는 방법에서 reconcideChildren( )을 만났습니다. 이 함수는 내부적으로 자식 유형에 따라 몇 가지 변형이 있습니다. 그중 세 가지에 집중하겠습니다.

새로운 자식 엘리먼트만 생성하지만 기존 엘리먼트를 재사용하려고 시도한다는 점을 기억하세요.

 

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(
          reconcileSingleElement(
          /*
          자식이 하나인 경우 실행
          */

            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
          ),
        );
      case REACT_PORTAL_TYPE:
       ...
      case REACT_LAZY_TYPE:
        ...
    }
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
      /*
      자식이 배열값인 경우
      key값이 중요하게 작용함
      */

      
    }
    ...
  }
  if (
    (typeof newChild === 'string' && newChild !== '') ||
    typeof newChild === 'number'
  ) {
    return placeSingleChild(
      reconcileSingleTextNode(
        returnFiber,
        currentFirstChild,
        '' + newChild,
        lanes,
      ),
      /*
      자식이 문자열인 경우
      */

    );
  }
  // 나머지 케이스는 모두 자식이 비어 있는 것으로 처리됩니다.
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

 

<Component/>의 경우 단일 div를 반환합니다. 이제 reconcideSingleElement()로 이동하겠습니다.

 

2.7 reconcileSingleElement().

 

function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement,
  /*
  여기서는 <div/>의 요소인 Component()의 반환값입니다.
  */

  lanes: Lanes,
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  while (child !== null) {
    // TODO: key === null이고 child.key === null인 경우, 이는 목록의 첫 번째 항목에만 적용됩니다.
    if (child.key === key) {
      const elementType = element.type;
      if (elementType === REACT_FRAGMENT_TYPE) {
        ...
      } else {
        if (
          child.elementType === elementType ||
          /*
          타입이 같으면 재사용할 수 있습니다. 그렇지 않으면 그냥 deleteChild()을 실행
          */

          // 이 검사는 인라인으로 유지하여 잘못된 경로에서만 실행되도록 합니다:
          (__DEV__
            ? isCompatibleFamilyForHotReloading(child, element)
            : false) ||
          // lazy type은 해결된 유형을 조정해야 합니다. 
          // 핫 리로딩은 다시 일시 중단되지 않기 때문에 
          // prod와 의미가 다르기 때문에 위의 핫 리로딩 검사 후에 이 작업을 수행해야 합니다.
          // 따라서 아래 호출을 일시 중단할 수 없습니다.
          (typeof elementType === 'object' &&
            elementType !== null &&
            elementType.$$typeof === REACT_LAZY_TYPE &&
            resolveLazy(elementType) === child.type)
        ) {
          deleteRemainingChildren(returnFiber, child.sibling);
          const existing = useFiber(child, element.props);
          /*
          기존 fiber를 새로운 props element.props의 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;
  }
  if (element.type === REACT_FRAGMENT_TYPE) {
    ...
  } else {
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
  }
}

 

그리고 useFiber에서 React는 이전 버전을 생성하거나 재사용합니다.

만약 재사용할 수 없는 경우(자식의 type이 아예 다른 경우 ) 자식을 삭제하고 재생성?  

앞서 언급했듯이, 자식을 포함하는 pendingProps가 설정됩니다.

 

function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
  // 반환하기 전에 잊어버리기 쉽기 때문에 현재 여기서는 형제자매를 null로 설정하고 인덱스를 0으로 설정합니다. 예를 들어 단일 자식 케이스의 경우입니다.
  const clone = createWorkInProgress(fiber, pendingProps);
  clone.index = 0;
  clone.sibling = null;
  return clone;
}

 

따라서 컴포넌트가 다시 렌더링된 후 React는 새로운 <div/>인 자식으로 이동하며, 현재 버전은 빈 Lanes과 childLanes을 모두 가지고 있습니다.

 

2.8 컴포넌트가 다시 렌더링되면 기본적으로 해당 하위 트리가 다시 렌더링됩니다.

<div/>와 그 자식에는 예약된 작업이 없으므로 강제 종료 로직(Bailout)이 발생한다고 생각할 수 있지만 그렇지 않습니다.

beginWork()에서 memoizedProps와 pendingProps를 검사한다는 점을 기억하세요.

컴포넌트가 다시 렌더링되면 기본적으로 하위 트리가 다시 렌더링됩니다.

 

const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
  oldProps !== newProps ||
  /*
  얕은 비교가 아닌 === 비교를 사용함
  */

  hasLegacyContextChanged() ||
  // 핫 리로드로 인해 구현이 변경된 경우 강제로 다시 렌더링하기:
  (__DEV__ ? workInProgress.type !== current.type : false)
) {
  // props나 컨텍스트가 변경된 경우 Fiber가 작업을 수행한 것으로 표시합니다.
  // 나중에 props이 동일하다고 판단되면 이 설정이 해제될 수 있습니다. (memo).
  didReceiveUpdate = true;
}

 

컴포넌트가 렌더링될 때마다 React 엘리먼트가 포함된 새로운 객체를 생성하므로 매번 pendingProps가 새로 생성되는 반면, props를 비교할 때는 얕은 비교가 사용되지 않는다는 점에 유의하세요.

 

<div/>의 경우 Component()가 실행되면 항상 새로운 프로퍼티를 가져오기 때문에 bailout이 전혀 일어나지 않습니다.
따라서 React는 업데이트 분기인 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) {
    // 호스트 노드의 direct text child를 특수 처리합니다. 이것은 일반적인 경우입니다. 
    // 재정의된 자식으로 처리하지 않습니다. 대신 이 프로퍼티에 대한 액세스 권한이 있는 호스트 환경에서 처리합니다. 
    // 이렇게 하면 다른 HostText fiber를 할당하고 트래버스하는 것을 피할 수 있습니다.
    nextChildren = null;
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
    // direct text child에서 일반 child으로 전환하는 경우 또는 비어 있는 경우,
    // 텍스트 콘텐츠가 재설정되도록 예약해야 합니다.
    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는 이를 reconcideChildrenArray()로 조정합니다.

그리고 현재 memoizedProps는.

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

 

2.10 reconcideChildrenArray()는 필요에 따라 fiber를 생성하고 삭제합니다.

reconcideChildrenArray()는 약간 복잡합니다. 요소의 재정렬을 체크해 추가 최적화를 수행하고 key가 존재하는 경우 fiber를 재사용하려고 시도합니다.

 

하지만 데모에서는 key가 없으므로 그냥 기본 분기점으로 이동합니다.

 

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;
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    /*
    하위 요소에 대한 현재 fiber 확인
    */

      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      /*
      여기에서 목록의 각 fiber를 새로운 props로 확인합니다.
      */

      if (newFiber === null) {
        //TODO: 이것은 널 자식처럼 빈 슬롯에서 중단됩니다. 
        // 이는 항상 느린 경로를 트리거하기 때문에 안타까운 일입니다. 
        // 미스인지, 널인지, 부울인지, 정의되지 않았는지 등을 
        // 더 잘 전달할 수 있는 방법이 필요합니다.
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          // 슬롯을 일치시켰지만 기존 파이버를 재사용하지 않았으므로 
          // 기존 자식을 삭제해야 합니다.
          deleteChild(returnFiber, oldFiber);
        }
        /*
        파이버를 재사용할 수 없는 경우, 삭제됨으로 표시되며 커밋 단계에서 해당 DOM 노드가 삭제됩니다.
        */
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      /*
      이렇게 하면 fiber를 삽입으로 표시하려고 합니다.
      */

      if (previousNewFiber === null) {
        // TODO: 루프 밖으로 이동합니다. 이는 첫 번째 실행에만 발생합니다.
        resultingFirstChild = newFiber;
      } else {
        // TODO: 이 슬롯에 적합한 인덱스가 아닌 경우 형제를 연기합니다. 
        // 즉, 이전에 null 값이 있었다면 각 null 값에 대해 이 작업을 연기하고 싶습니다. 
        // 하지만 이전 슬롯으로 updateSlot을 호출하고 싶지는 않습니다.
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }
    if (newIdx === newChildren.length) {
      // 새로운 children에 거의 다 도달했습니다. 나머지는 삭제할 수 있습니다.
      deleteRemainingChildren(returnFiber, oldFiber);
      if (getIsHydrating()) {
        const numberOfForks = newIdx;
        pushTreeFork(returnFiber, numberOfForks);
      }
      return resultingFirstChild;
    }
    ...
    return resultingFirstChild;
  }

 

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

text를 제외하면 key와 type이 모두 같은 경우에만 current를 재사용한다. 만약 적절한 key값을 사용하지 않는 경우에는 예상치못한 재사용이 발생할 수 있다.? 

 

function updateSlot(
  returnFiber: Fiber,
  oldFiber: Fiber | null,
  newChild: any,
  lanes: Lanes,
): Fiber | null {
  // 키가 일치하면 fiber를 업데이트하고, 그렇지 않으면 null을 반환합니다.
  const key = oldFiber !== null ? oldFiber.key : null;
  if (
    (typeof newChild === 'string' && newChild !== '') ||
    typeof newChild === 'number'
  ) {
    // 텍스트 노드에는 키가 없습니다. 
    // 이전 노드에 암시적으로 키가 있는 경우 텍스트가 아니더라도 중단하지 않고 계속 대체할 수 있습니다. 
    // 노드가 아니더라도 중단하지 않고 계속 교체할 수 있습니다.
    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 ||
      /*
      재사용할 수 있는 경우
      */

      // 이 검사는 인라인으로 유지하여 잘못된 경로에서만 실행되도록 합니다:
      (__DEV__
        ? isCompatibleFamilyForHotReloading(current, element)
        : false) ||
      // 지연 유형은 해결된 유형을 조정해야 합니다. 
      // 핫 리로딩은 다시 일시 중단되지 않기 때문에 prod와 의미가 다르기 때문에 위의 핫 리로딩 검사 후에 이 작업을 수행해야 합니다. 
      // 따라서 아래 호출을 일시 중단할 수 없습니다.
      (typeof elementType === 'object' &&
        elementType !== null &&
        elementType.$$typeof === REACT_LAZY_TYPE &&
        resolveLazy(elementType) === current.type)
    ) {
      // 인덱스를 기반으로 이동
      const existing = useFiber(current, element.props);
      /*
      여기서 다시 useFiber()를 볼 수 있습니다.
      */

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

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

 

따라서 <div/>에서 updateSlot()은 현재가 span이지만 b를 원하기 때문에 4번째를 제외한 3개의 자식을 성공적으로 재사용하므로 span의 fiber는 처음부터 생성되고 deleteChild()를 통해 b의 fiber가 삭제됩니다. 새로 생성된 span은 placeChild()로 표시됩니다.

 

UpdateSlot은 current를 재사용할 수 있으면 재사용을 하고 type이 다르면 fiber을 새로 만들어 줌

 

2.11 placeChild() 및 deleteChild()는 플래그가 있는 fiber를 표시합니다.

Flag를 설정하는 이유는 이후 Commit 단계에서 이동이나 삽입 연산이 발생했다는 걸 알리는 목적으로 설정함

 

컴포넌트 아래 <div>의 자식에는 fiber Node를 표시하는 이 두 가지 함수가 있습니다.

function placeChild(
  newFiber: Fiber,
  lastPlacedIndex: number,
  newIndex: number,
): number {
  newFiber.index = newIndex;
  if (!shouldTrackSideEffects) {
    // hydration 중에 useId 알고리즘은 어떤 fiber가 하위 목록(배열, 반복자)의 일부인지 알아야 합니다.
    newFiber.flags |= Forked;
    return lastPlacedIndex;
  }
  const current = newFiber.alternate;
  if (current !== null) {
    const oldIndex = current.index;
    if (oldIndex < lastPlacedIndex) {
      // 움직이는 것을 나타냅니다
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    } else {
      // 이 항목은 그대로 유지해도 됩니다.
      return oldIndex;
    }
  } else {
    // 삽입 연산을 나타냅니다.
    newFiber.flags |= Placement;
    return lastPlacedIndex;
  }
}
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
  if (!shouldTrackSideEffects) {
    // Noop.
    return;
  }
  const deletions = returnFiber.deletions;
  if (deletions === null) {
    returnFiber.deletions = [childToDelete];
    returnFiber.flags |= ChildDeletion;
  } else {
    deletions.push(childToDelete);
  }
}

 

삭제해야 하는 fiber가 부모 fiber의 배열에 임시로 배치되는 것을 볼 수 있습니다. 이는 삭제 후 새 fiber tree에 더 이상 존재하지 않지만 커밋 단계에서 처리해야 하므로 어딘가에 저장해야 하기 때문에 필요합니다.

 

<div> 작업이 종료되었습니다. 

 

기존에 연결되어 있던&nbsp; 로직이 지워지고(사실 임시경로에 저장되고) 새로운 button으로 연결된다고 이해하면 되려나

 

다음 React는 버튼으로 이동합니다. 다시 말하지만, 일정이 작동하지 않는다고 생각했지만, props가 ["click me-", "1"]에서 ["click me-", "2"]로 변경되었기 때문에 React는 여전히 updateHostComponent()를 사용하여 작동합니다.

HostText의 경우 props는 문자열이므로 첫 번째 "click me -"는 제외됩니다. 그리고 차례로 React는 updateHostText()로 텍스트를 조정하려고 시도합니다.(1-> 2)

 

2.12 updateHostText().

 

function updateHostText(current, workInProgress) {
  if (current === null) {
    tryToClaimNextHydratableInstance(workInProgress);
  }
  // 여기서 할 일이 없습니다. 여기는 터미널입니다. 완료 단계는 바로 이어서 진행하겠습니다..
  return null;
}

 

업데이트가 완료 단계인 completeWork()로 표시되기 때문에 다시 한 번 아무것도 아닌 것이 아닙니다. 이는 초기 마운트에서도 설명합니다.

 

2.13 completeWork()는 호스트 컴포넌트의 업데이트를 표시하고 필요한 경우 DOM 노드를 생성합니다.

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  // Note: 현재 트리 공급자 fiber와 비교하면 속도가 빠르고 오류가 덜 발생하기 때문에 
  // 의도적으로 hydration 여부를 확인하지 않습니다. 
  // 이상적으로는 hydration만을 위한 특별한 버전의 작업 루프가 있어야 합니다.
  popTreeContext(workInProgress);
  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      bubbleProperties(workInProgress);
      return null;
    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);
        }
      } 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);
      } else {
      /*
      이전 에피소드에서 살펴본 초기 마운트 지점입니다.
      */

        ...
        if (wasHydrated) {
          if (prepareToHydrateHostTextInstance(workInProgress)) {
            markUpdate(workInProgress);
          }
        } else {
          workInProgress.stateNode = createTextInstance(
            newText,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
        }
      }
      bubbleProperties(workInProgress);
      return null;
    }
    ...
  }
}
updateHostText = function(
/*
이것은 완료 단계에서 다른 updateHostText()입니다.
*/

  current: Fiber,
  workInProgress: Fiber,
  oldText: string,
  newText: string,
) {
  // 텍스트가 다르면 업데이트로 표시하세요. 모든 작업은 커밋워크에서 완료됩니다.
  if (oldText !== newText) {
    markUpdate(workInProgress);
  }
};
updateHostComponent = function(
  current: Fiber,
  workInProgress: Fiber,
  type: Type,
  newProps: Props,
  rootContainerInstance: Container,
) {
  // 대안이 있다면 이는 업데이트이며 
  // 업데이트를 수행하기 위해 부수적인 일정을 잡아야 한다는 의미입니다.  
  const oldProps = current.memoizedProps;
  if (oldProps === newProps) {
    // 수정 모드에서는 다음과 같은 이유로 bailout에 적합합니다. 
    // 자식이 변경되더라도 이 노드를 건드리지 않기 때문입니다.
    return;
  }
  // 자식 중 하나가 업데이트되어 업데이트를 받으면 
  // 새로운 프로퍼티가 없으므로 다시 사용해야 합니다.
  // TODO: 업데이트 API를 props와 children에 대해 별도로 분리해야함. 
  // 자식은 아예 특수 케이스가 아니라면 더 좋을 것입니다.
  const instance: Instance = workInProgress.stateNode;
  const currentHostContext = getHostContext();
  // TODO: oldProps가 null인 오류가 발생했습니다. 
  // 호스트 컴포넌트가 재개 경로에 부딪히고 있음을 나타냅니다. 
  // 이유를 알아보세요. '숨김'과 관련이 있을 수 있습니다.
  const updatePayload = prepareUpdate(
    instance,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
    currentHostContext,
  );
  // TODO: 이 컴포넌트 타입에 맞게 입력해야합니다.
  workInProgress.updateQueue = (updatePayload: any);
the update is put in updateQueue

This actually is also used for hooks like Effect Hooks

  // 데이트 페이로드에 변경 사항이 있거나 새로운 참조가 있는 경우 이를 업데이트로 표시합니다. 
  // 모든 작업은 commitWork에서 이루어집니다.
  if (updatePayload) {
    markUpdate(workInProgress);
  }
};
function markUpdate(workInProgress: Fiber) {
  // 업데이트 효과로 fiber에 태그를 지정합니다. 
  // 이렇게 하면 Placement가 PlacementAndUpdate로 바뀝니다.
  workInProgress.flags |= Update;
  /*
  네, 또 다른 flag입니다!
  */
}

 

렌더링 단계가 끝나면 다음과 같이 됩니다.

1. b에 삽입
2. span  삭제
3. 호스트 텍스트(텍스드 노드)에 대한 업데이트
4. 버튼에 대한 업데이트(후드 아래 비어 있음).

한 가지 지적하고 싶은 것은 버튼과 그 부모 div 모두에 대해 prepareUpdate()가 실행되지만 div에 대해서는 null을 생성하지만 버튼에 대해서는 빈 배열을 생성한다는 점입니다. 여기서는 다루지 않을 까다로운 에지 케이스 처리입니다.

 

 

이제 커밋 단계로 가보죠.

 

3. Re-render in Commit Phase

3.1 commitMutationEffectsOnFiber()는 삽입/삭제/업데이트 커밋을 시작합니다.

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
  // effect flag는 fiber tag가 더 구체적이므로 fiber type을 구체화한 후에 확인해야 합니다. 
  // 조정과 관련된 flag는 모든 fiber type에 설정할 수 있으므로 예외입니다.
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      /*
      재귀적으로 자식을 먼저 처리
      */

      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: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      /*
      재귀적으로 자식을 먼저 처리
      */

      commitReconciliationEffects(finishedWork);
      /*
      그런 다음 삽입합니다.
      */

      if (supportsMutation) {
        // TODO: ContentReset은 커밋 단계에서 자식에 의해 지워집니다. 
        // 이것은 리팩터링 위험인데, 
        // `commitReconciliationEffects`가 이미 실행된 후에 플래그를 읽어야 하므로 
        // 순서가 중요하기 때문입니다. 
        // ContentReset이 커밋 중에 플래그를 변경하는 데 의존하지 않도록 리팩터링해야 합니다. 
        // 대신 렌더링 단계에서 플래그를 설정하는 것이 좋습니다.
        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) {
            // 앞서 준비한 작업을 커밋합니다.
            const newProps = finishedWork.memoizedProps;
            // hydration를 위해 업데이트 경로를 재사용하지만, 
            // 이전 Prop을 새로운 Prop으로 취급합니다. 
            // 이 경우 업데이트 페이로드에는 실제 변경 사항이 포함됩니다.
            const oldProps =
              current !== null ? current.memoizedProps : newProps;
            const type = finishedWork.type;
            // TODO: 호스트 컴포넌트별로 업데이트 큐를 입력합니다.
            const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
            finishedWork.updateQueue = null;
            if (updatePayload !== null) {
              try {
                commitUpdate
                /*
                호스트 컴포넌트의 경우, props만 업데이트하고 있습니다
                */

                  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;
          // hydration를 위해 업데이트 경로를 재사용하지만 이전 Props는 새로운 Props로 취급합니다. 
          // 이 경우 업데이트 Payload에는 실제 변경 사항이 포함됩니다.
          const oldText: string =
            current !== null ? current.memoizedProps : newText;
          try {
            commitTextUpdate(textInstance, oldText, newText);
            /*
            HostText의 경우 TextContent를 업데이트하는 중입니다
            */

          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }
  }
}

 

재귀적 프로세스임을 알 수 있으며 각 유형의 mutation를 자세히 살펴 보겠습니다.

 

3.2 삭제가 먼저 처리된 후 자식 및 본인 처리가 진행됩니다.

 

function recursivelyTraverseMutationEffects(
  root: FiberRoot,
  parentFiber: Fiber,
  lanes: Lanes,
) {
  // 삭제 효과는 모든 유형의 fiber에 대해 예약할 수 있습니다. 
  // 삭제 효과는 자식 효과가 실행되기 전에 발생해야 합니다.
  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);
}

 

삭제는 자식 요소를 처리하기 전이라도 먼저 처리됩니다.

function commitDeletionEffects(
  root: FiberRoot,
  returnFiber: Fiber,
  deletedFiber: Fiber,
) {
  if (supportsMutation) {
    // 삭제된 최상위 fiber만 있지만 모든 터미널 노드를 찾으려면 
    // 그 하위 노드를 재귀적으로 찾아야 합니다.
    // 부모에서 모든 호스트 노드를 재귀적으로 삭제하고, 레퍼런스를 분리하고, 
    // 마운트된 layout effect를 정리한 다음 componentWillUnmount를 호출합니다.
    // 각 브랜치에서 최상위 호스트 자식만 제거하면 됩니다. 
    // 하지만 마운트 해제 이펙트, 레퍼런스와 cWU를 계속 탐색해야 합니다.
    // TODO: 이를 두 개의 개별 트래버스 함수로 나눌 수 있는데, 두 번째 함수에는 removeChild 로직이 포함되지 않습니다. 
    // 이것은 아마도 "disappearLayoutEffects"(또는 레이아웃 단계가 재귀를 사용하도록 리팩터링된 후에 바뀌는 함수)와 같은 함수일 것입니다. 
    // 시작하기 전에 스택에서 가장 가까운 호스트 부모를 찾아서 
    // 자식을 제거할 인스턴스/컨테이너를 파악하세요.
    // TODO: 삭제할 때마다 파이버 리턴 경로를 검색하는 대신, 
    // 커밋 단계에서 트리를 이동하면서 JS 스택에서 가장 가까운 호스트 컴포넌트를 추적할 수 있습니다. 
    // 이렇게 하면 삽입 속도도 빨라집니다.
    let parent = returnFiber;
    findParent: while (parent !== null) {
    /*
    부모 노드가 반드시 백킹 DOM을 가지고 있다는 것을 의미하지는 않으므로 
    여기서는 백킹 DOM을 가지고 있는 가장 가까운 파이버 노드를 찾습니다.
    */
      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 {
    // 레퍼런스를 분리하고 전체 서브트리에서 componentWillUnmount()를 호출합니다.
    commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
  }
  detachFiberMutation(deletedFiber);
}
function commitDeletionEffectsOnFiber(
  finishedRoot: FiberRoot,
  nearestMountedAncestor: Fiber,
  deletedFiber: Fiber,
) {
  onCommitUnmount(deletedFiber);
  // 이 외부 스위치의 케이스는 하위 트리로 이동하기 전에 스택을 수정합니다. 
  // 내부 스위치에는 스택을 수정하지 않는 더 간단한 케이스가 있습니다.
  switch (deletedFiber.tag) {
    case HostComponent: {
      if (!offscreenSubtreeWasHidden) {
        safelyDetachRef(deletedFiber, nearestMountedAncestor);
      }
      // 의도적으로 다음 지점으로 넘어갑니다
    }
    // eslint-disable-next-line-no-fallthrough
    case HostText: {
      // 가장 가까운 호스트 자식만 제거하면 됩니다. 
      // 중첩된 자식을 제거할 필요가 없음을 나타내려면 스택에서 호스트 부모를 `null`로 설정합니다.
      if (supportsMutation) {
        const prevHostParent = hostParent;
        const prevHostParentIsContainer = hostParentIsContainer;
        hostParent = null;
        recursivelyTraverseDeletionEffects(
          finishedRoot,
          nearestMountedAncestor,
          deletedFiber,
        );
        hostParent = prevHostParent;
        hostParentIsContainer = prevHostParentIsContainer;
        if (hostParent !== null) {
          // 이제 모든 자식 효과가 마운트 해제되었으므로 트리에서 노드를 제거할 수 있습니다.
          if (hostParentIsContainer) {
            removeChildFromContainer(
              ((hostParent: any): Container),
              /*
              이 호스트 Parent는 루프 중에 이전 호스트에서 다시 시도됩니다
              */

              (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;
    }
  }
}

 

3.3 다음에 삽입 처리를 합니다.

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

function commitReconciliationEffects(finishedWork: Fiber) {
  // 배치 효과(삽입, 재주문)는 모든 fiber 타입에 대해 예약할 수 있습니다. 
  // 배치 효과는 자식 효과가 발동된 후 이 fiber에 대한 효과가 발동되기 전에 발생해야 합니다.
  const flags = finishedWork.flags;
  if (flags & Placement) {
    try {
      commitPlacement(finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
    // effect 태그에서 '배치'를 지워 componentDidMount와 같은 수명 주기가 호출되기 전에 삽입되었음을 알 수 있도록 합니다.
    // TODO: findDOMNode는 더 이상 이에 의존하지 않지만 isMounted는 의존하고 있으며 
    // 어쨌든 isMounted는 더 이상 사용되지 않으므로 이를 제거할 수 있어야 합니다.
    finishedWork.flags &= ~Placement;
  }
  if (flags & Hydrating) {
    finishedWork.flags &= ~Hydrating;
  }
}
function commitPlacement(finishedWork: Fiber): void {
  if (!supportsMutation) {
    return;
  }
  // 모든 호스트 노드를 부모 노드에 재귀적으로 삽입합니다.
  const parentFiber = getHostParentFiber(finishedWork);
  // Note: 이 두 변수는 항상 함께 업데이트되어야 합니다.
  switch (parentFiber.tag) {
    case HostComponent: {
      const parent: Instance = parentFiber.stateNode;
      if (parentFiber.flags & ContentReset) {
        // 삽입을 수행하기 전에 상위의 텍스트 콘텐츠를 재설정합니다.
        resetTextContent(parent);
        // 이펙트 태그에서 ContentReset 지우기
        parentFiber.flags &= ~ContentReset;
      }
      const before = getHostSibling(finishedWork);
      /*
      중요합니다. 
      Node.insertBefore()에는 형제 노드가 필요합니다. 
      형제 노드를 찾을 수 없는 경우 끝에 추가하면 됩니다.
      */

      // 우리는 삽입된 최상위 파이버만 가지고 있지만 
      // 모든 터미널 노드를 찾으려면 그 하위 노드를 재귀적으로 찾아야 합니다.
      insertOrAppendPlacementNode(finishedWork, before, parent);
      break;
    }
    case HostRoot:
    case HostPortal: {
      const parent: Container = parentFiber.stateNode.containerInfo;
      const before = getHostSibling(finishedWork);
      insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
      break;
    }
    // eslint-disable-next-line-no-fallthrough
    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) {
    // 삽입 자체가 포털인 경우, 우리는 그 자식들을 따라 내려가지 않을 것입니다. 
    // 대신 포털의 각 하위 항목에서 직접 삽입을 가져옵니다.
  } 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) {
    // 삽입 자체가 포털인 경우, 우리는 그 자식들을 따라 내려가지 않을 것입니다. 
    // 대신 포털의 각 하위 항목에서 직접 삽입을 가져옵니다.
  } 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;
      }
    }
  }
}

 

3.4 업데이트는 최종적으로 처리됩니다

업데이트 지점은 commitMutationEffectsOnFiber() 안에 있습니다.

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
  // 이펙트 플래그는 fiber 태그가 더 구체적이므로 fiber type을 구체화한 후에 확인해야 합니다. 
  // 조정과 관련된 플래그는 모든 fiber type에 설정할 수 있으므로 예외입니다.
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      if (flags & Update) {
      /*
      함수 컴포넌트의 경우, 이는 훅을 실행해야 함을 의미합니다
      */

        try {
          commitHookEffectListUnmount(
            HookInsertion | HookHasEffect,
            finishedWork,
            finishedWork.return,
          );
          commitHookEffectListMount(
            HookInsertion | HookHasEffect,
            finishedWork,
          );
        } catch (error) {
          captureCommitPhaseError(finishedWork, finishedWork.return, error);
        }
        // Layout effects는 mutation 단계에서 소멸되므로 모든 파이버에 대한 모든 소멸 함수가 생성 함수보다 먼저 호출됩니다. 
        // 이렇게 하면 형제 컴포넌트 효과가 서로 간섭하는 것을 방지할 수 있습니다. 
        // 예를 들어 한 컴포넌트의 destroy 함수가 동일한 커밋 중에 
        // 다른 컴포넌트의 create 함수가 설정한 참조를 재정의해서는 안 됩니다.
        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) {
        // TODO: ContentReset은 커밋 단계에서 자식에 의해 지워집니다. 
        // 이것은 refactor hazard인데, `commitReconciliationEffects`가 이미 실행된 후에 플래그를 읽어야 하므로
        // 순서가 중요하기 때문입니다. ContentReset이 커밋 중에 플래그를 변경하는 데 의존하지 않도록 리팩터링해야 합니다. 
        // 대신 렌더링 단계에서 플래그를 설정하는 것이 좋습니다.
        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) {
            // 앞서 준비한 작업을 커밋합니다.
            const newProps = finishedWork.memoizedProps;
            // hydration를 위해 업데이트 경로를 재사용하지만, 이전 Prop을 새로운 Prop으로 취급합니다. 
            // 이 경우 업데이트 페이로드에는 실제 변경 사항이 포함됩니다.
            const oldProps =
              current !== null ? current.memoizedProps : newProps;
            const type = finishedWork.type;
            // TODO: 호스트 컴포넌트별로 업데이트 큐를 입력합니다.
            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;
          // hydration를 위해 업데이트 경로를 재사용하지만, 이전 Prop을 새로운 Prop으로 취급합니다. 
          // 이 경우 업데이트 페이로드에는 실제 변경 사항이 포함됩니다.
          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 {
  // DOM 노드에 diff를 적용합니다
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
  // 현재 이벤트 핸들러가 있는 props이 어떤 props인지 알 수 있도록 props handle을 업데이트합니다.
  updateFiberProps(domElement, newProps);
}
export function commitTextUpdate(
  textInstance: TextInstance,
  oldText: string,
  newText: string,
): void {
  textInstance.nodeValue = newText;
}

 

데모에서는 트리 구조로 인해 아래 순서대로 mutations이 처리됩니다.

1. span에서 삭제
2. 호스트 텍스트에서 업데이트
3. 버튼에서 업데이트( 해당 업데이트 작업이 내부적으로는 어떠한 변경도 발생시키지 않음  )
4. b 삽입

 

4. Summary

 

휴, 정말 많네요. 리렌더링 프로세스를 대략적으로 요약하면 다음과 같습니다.

1. 상태 변경 후, 대상 파이버 노드에 대한 경로에 Lanes 및 childLanes을 표시하여 해당 노드 또는 하위 트리를 다시 렌더링해야 하는지를 나타냅니다.
2. React는 불필요한 리렌더링을 피하기 위해 bailout 최적화를 통해 전체 파이버 트리를 리렌더링합니다.
3. 컴포넌트가 다시 렌더링되면 새로운 React 엘리먼트가 생성되고, 그 자식들은 모두 동일하더라도 새로운 프로퍼티를 얻게 되므로 React는 기본적으로 전체 파이버 트리를 다시 렌더링합니다. 사용메모()가 필요한 이유는 다음과 같습니다.
4. "리렌더링"을 통해 React는 현재 파이버 트리에서 새로운 파이버 트리를 생성하고, 필요한 경우 파이버 노드에 Placement,  ChildDeletion 및 Update 플래그를 표시합니다.
5. 새 파이버 트리가 완료되면 React는 위의 플래그가 있는 파이버 노드를 처리하고 커밋 단계에서 호스트 DOM에 변경 사항을 적용합니다.
6. 그러면 새 파이버 트리가 현재 파이버 트리로 가리키게 됩니다. 이전 파이버 트리의 노드는 다음 렌더링에 재사용할 수 있습니다.

 

출처
https://jser.dev/2023-07-18-how-react-rerenders/

https://goidle.github.io/react/in-depth-react-reconciler_3/

https://d2.naver.com/helloworld/2690975