Early Riser
생각정리
Early Riser
전체 방문자
오늘
어제
  • 분류 전체보기 (128)
    • JS (19)
    • React (33)
    • React Native (2)
    • Library, Tool (13)
    • CSS (2)
    • Algorithm (40)
    • Computer Science (3)
    • 회고 (3)
    • AI (13)

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • 글쓰기

공지사항

인기 글

태그

  • dfs
  • js
  • 자바스크립트
  • BFS
  • boosting
  • local minima
  • lightgbm
  • 부스트캠프 합격
  • 밑바닥
  • react
  • 손실함수
  • javascript
  • 딥러닝
  • 논문리뷰
  • 부스트캠프 합격 후기
  • 프로그래머스
  • RNN
  • 비동기
  • 백트래킹
  • 백준
  • 알고리즘
  • 구현
  • 부스트캠프 9기
  • useState
  • 오늘의불경
  • 완전탐색
  • useEffect
  • global minima
  • LGBM
  • 파이썬

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Early Riser

생각정리

React

React 톺아보기 4. reconciler (2)

2025. 7. 14. 17:10
function performWorkOnRootViaSchedulerTask(
  root: FiberRoot,
  didTimeout: boolean,
): RenderTaskFn | null {
  if (hasPendingCommitEffects()) {
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return null;
  }

  // Flush any pending passive effects before deciding which lanes to work on,
  // in case they schedule additional work.
  const originalCallbackNode = root.callbackNode;
  const didFlushPassiveEffects = flushPendingEffects(true);
  if (didFlushPassiveEffects) {
    // Something in the passive effect phase may have canceled the current task.
    // Check if the task node for this root was changed.
    if (root.callbackNode !== originalCallbackNode) {
      // The current task was canceled. Exit. We don't need to call
      // `ensureRootIsScheduled` because the check above implies either that
      // there's a new task, or that there's no remaining work on this root.
      return null;
    } else {
      // Current task was not canceled. Continue.
    }
  }

  const workInProgressRoot = getWorkInProgressRoot();
  const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
  const rootHasPendingCommit =
    root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout;
  const lanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
    rootHasPendingCommit,
  );
  if (lanes === NoLanes) {
    // No more work on this root.
    return null;
  }

  // Enter the work loop.
  const forceSync = !disableSchedulerTimeoutInWorkLoop && didTimeout;
  performWorkOnRoot(root, lanes, forceSync);

  // The work loop yielded, but there may or may not be work left at the current
  // priority. Need to determine whether we need to schedule a continuation.
  scheduleTaskForRootDuringMicrotask(root, now());
  if (root.callbackNode != null && root.callbackNode === originalCallbackNode) {
    // The task node scheduled for this root is the same one that's
    // currently executed. Need to return a continuation.
    return performWorkOnRootViaSchedulerTask.bind(null, root);
  }
  return null;
}

performWorkOnRootViaSchedulerTask 함수는 재귀적 호출 구조를 가지고 있지만, 전통적인 재귀 호출과는 다른 방식으로 동작한다. 이 함수는 먼저 performWorkOnRoot를 호출하여 실제 작업을 수행한 후, scheduleTaskForRootDuringMicrotask를 통해 다음 작업을 스케줄링한다. 마지막으로 root.callbackNode가 존재하고 원래의 콜백 노드와 동일한 경우, performWorkOnRootViaSchedulerTask.bind(null, root)를 반환하여 새로운 콜백 함수를 생성한다. 이는 실제 함수를 직접 재귀 호출하는 것이 아니라,

export function performWorkOnRoot(
  root: FiberRoot,
  lanes: Lanes,
  forceSync: boolean,
): void {
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    throw new Error('Should not already be working.');
  }

  const shouldTimeSlice =
    (!forceSync &&
      !includesBlockingLane(lanes) &&
      !includesExpiredLane(root, lanes)) ||
    (enableSiblingPrerendering && checkIfRootIsPrerendering(root, lanes));

  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes, true);

  let renderWasConcurrent = shouldTimeSlice;

  do {
    if (exitStatus === RootInProgress) {
      // Render phase is still in progress.
      if (
        enableSiblingPrerendering &&
        workInProgressRootIsPrerendering &&
        !shouldTimeSlice
      ) {
        const didAttemptEntireTree = false;
        markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
      }
      break;
    } else {
      let renderEndTime = 0;

      // The render completed.
      
      const finishedWork: Fiber = (root.current.alternate: any);
      if (
        renderWasConcurrent &&
        !isRenderConsistentWithExternalStores(finishedWork)
      ) {
        // A store was mutated in an interleaved event. Render again,
        // synchronously, to block further mutations.
        exitStatus = renderRootSync(root, lanes, false);
        // We assume the tree is now consistent because we didn't yield to any
        // concurrent events.
        renderWasConcurrent = false;
        // Need to check the exit status again.
        continue;
      }

      // Check if something threw
      if (
        (disableLegacyMode || root.tag !== LegacyRoot) &&
        exitStatus === RootErrored
      ) {
        const lanesThatJustErrored = lanes;
        const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
          root,
          lanesThatJustErrored,
        );
        if (errorRetryLanes !== NoLanes) {
          if (enableProfilerTimer && enableComponentPerformanceTrack) {
            setCurrentTrackFromLanes(lanes);
            logErroredRenderPhase(renderStartTime, renderEndTime, lanes);
            finalizeRender(lanes, renderEndTime);
          }
          lanes = errorRetryLanes;
          exitStatus = recoverFromConcurrentError(
            root,
            lanesThatJustErrored,
            errorRetryLanes,
          );
          renderWasConcurrent = false;
          // Need to check the exit status again.
          if (exitStatus !== RootErrored) {
        
            continue;
          } else {
            // The root errored yet again. Proceed to commit the tree.
            if (enableProfilerTimer && enableComponentPerformanceTrack) {
              renderEndTime = now();
            }
          }
        }
      }
      if (exitStatus === RootFatalErrored) {
        prepareFreshStack(root, NoLanes);
        // Since this is a fatal error, we're going to pretend we attempted
        // the entire tree, to avoid scheduling a prerender.
        const didAttemptEntireTree = true;
        markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);
        break;
      }

      // We now have a consistent tree. The next step is either to commit it,
      // or, if something suspended, wait to commit it after a timeout.
      finishConcurrentRender(
        root,
        exitStatus,
        finishedWork,
        lanes,
        renderEndTime,
      );
    }
    break;
  } while (true);

  ensureRootIsScheduled(root);
}

  // We disable time-slicing in some cases: if the work has been CPU-bound
  // for too long ("expired" work, to prevent starvation), or we're in
  // sync-updates-by-default mode.

 

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;
  const prevDispatcher = pushDispatcher(root.containerInfo);
  const prevAsyncDispatcher = pushAsyncDispatcher();

  // If the root or lanes have changed, throw out the existing stack
  // and prepare a fresh one. Otherwise we'll continue where we left off.
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    workInProgressTransitions = getTransitionsForLanes(root, lanes);
    resetRenderTimer();
    prepareFreshStack(root, lanes);
  } else {
    // This is a continuation of an existing work-in-progress.
   
    workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes);
  }

  outer: do {
    try {
      if (
        workInProgressSuspendedReason !== NotSuspended &&
        workInProgress !== null
      ) {
        // The work loop is suspended. We need to either unwind the stack or
        // replay the suspended component.
        const unitOfWork = workInProgress;
        const thrownValue = workInProgressThrownValue;
        resumeOrUnwind: switch (workInProgressSuspendedReason) {
          case SuspendedOnError: {
            // Unwind then continue with the normal work loop.
            workInProgressSuspendedReason = NotSuspended;
            workInProgressThrownValue = null;
            throwAndUnwindWorkLoop(
              root,
              unitOfWork,
              thrownValue,
              SuspendedOnError,
            );
            break;
          }
          case SuspendedOnData:
          case SuspendedOnAction: {
            const thenable: Thenable<mixed> = (thrownValue: any);
            if (isThenableResolved(thenable)) {
              // The data resolved. Try rendering the component again.
              workInProgressSuspendedReason = NotSuspended;
              workInProgressThrownValue = null;
              replaySuspendedUnitOfWork(unitOfWork);
              break;
            }
            // The work loop is suspended on data. We should wait for it to
            // resolve before continuing to render.
            const onResolution = () => {
              // Check if the root is still suspended on this promise.
              if (
                (workInProgressSuspendedReason === SuspendedOnData ||
                  workInProgressSuspendedReason === SuspendedOnAction) &&
                workInProgressRoot === root
              ) {
                // Mark the root as ready to continue rendering.
                workInProgressSuspendedReason = SuspendedAndReadyToContinue;
              }
              ensureRootIsScheduled(root);
            };
            thenable.then(onResolution, onResolution);
            break outer;
          }
          case SuspendedOnImmediate: {
            // If this fiber just suspended, it's possible the data is already
            // cached. Yield to the main thread to give it a chance to ping. If
            // it does, we can retry immediately without unwinding the stack.
            workInProgressSuspendedReason = SuspendedAndReadyToContinue;
            break outer;
          }
          case SuspendedAndReadyToContinue: {
            const thenable: Thenable<mixed> = (thrownValue: any);
            if (isThenableResolved(thenable)) {
              // The data resolved. Try rendering the component again.
              workInProgressSuspendedReason = NotSuspended;
              workInProgressThrownValue = null;
              replaySuspendedUnitOfWork(unitOfWork);
            } else {
              // Otherwise, unwind then continue with the normal work loop.
              workInProgressSuspendedReason = NotSuspended;
              workInProgressThrownValue = null;
              throwAndUnwindWorkLoop(
                root,
                unitOfWork,
                thrownValue,
                SuspendedAndReadyToContinue,
              );
            }
            break;
          }
          default: {
            throw new Error(
              'Unexpected SuspendedReason. This is a bug in React.',
            );
          }
        }
      }

      if (__DEV__ && ReactSharedInternals.actQueue !== null) {
        workLoopSync();
      } else if (enableThrottledScheduling) {
        workLoopConcurrent(includesNonIdleWork(lanes));
      } else {
        workLoopConcurrentByScheduler();
      }
      break;
    } catch (thrownValue) {
      handleThrow(root, thrownValue);
    }
  } while (true);
  resetContextDependencies();

  popDispatcher(prevDispatcher);
  popAsyncDispatcher(prevAsyncDispatcher);
  executionContext = prevExecutionContext;

  // Check if the tree has completed.
  if (workInProgress !== null) {
    // Still work remaining.
    if (enableSchedulingProfiler) {
      markRenderYielded();
    }
    return RootInProgress;
  } else {
    // Completed the tree.
    if (enableSchedulingProfiler) {
      markRenderStopped();
    }

    // Set this to null to indicate there's no in-progress render.
    workInProgressRoot = null;
    workInProgressRootRenderLanes = NoLanes;

    // It's safe to process the queue now that the render phase is complete.
    finishQueueingConcurrentUpdates();

    // Return the final exit status.
    return workInProgressRootExitStatus;
  }
}

promise나 error가 throw되면 renderrootconcurrent(sync)의 catch구문의 handlethrow에서 thenable을 catch한다.handlethorw는 throw된 thenable의 종류에 따라 workInProgressSuspendedReason, workInProgressThrownValue를 기록한다.workinprogress는 그대로이기에 무한반복문인 renderrootconcurrent에서 promise를 thorw한 wip를 다시 처리하고, error인 경우에는 unwindWork를 통해 부모컴포넌트들을 찾아가며 적절한 boundary까지 올라가 해당 컴포넌트부터 workloop를 다시 시작한다.에러 타입별 처리 방식:

  • JavaScript 에러: Error Boundary(ClassComponent 타입)를 찾아 올라간다. Error Boundary는 getDerivedStateFromError나 componentDidCatch를 가진 클래스 컴포넌트다.
  • Promise/thenable: Suspense(SuspenseComponent 타입)를 찾아 올라간다. Suspense는 Promise를 잡고 fallback을 렌더링하는 특별한 built-in 컴포넌트다.

promise가 throw된 경우에는 thenable.then(onResolution, onResolution)을 통해 resolution 함수를 등록하고 workloop를 중단한다(break outer). Promise가 resolve되면 onResolution 콜백이 실행되어 workInProgressSuspendedReason = SuspendedAndReadyToContinue로 설정하고 ensureRootIsScheduled를 통해 마이크로태스크를 이용해 work를 다시 스케쥴한다.

function handleThrow(root: FiberRoot, thrownValue: any): void {
  if (
    thrownValue === SuspenseException ||
    thrownValue === SuspenseActionException
  ) {
    thrownValue = getSuspendedThenable();
    workInProgressSuspendedReason =
      !enableSiblingPrerendering &&
      shouldRemainOnPreviousScreen() &&
      !includesNonIdleWork(workInProgressRootSkippedLanes) &&
      !includesNonIdleWork(workInProgressRootInterleavedUpdatedLanes)
        ? // Suspend work loop until data resolves
          thrownValue === SuspenseActionException
          ? SuspendedOnAction
          : SuspendedOnData
        : SuspendedOnImmediate;
  } else if (thrownValue === SuspenseyCommitException) {
    thrownValue = getSuspendedThenable();
    workInProgressSuspendedReason = SuspendedOnInstance;
  } else if (thrownValue === SelectiveHydrationException) {
    workInProgressSuspendedReason = SuspendedOnHydration;
  } else {
    // This is a regular error.
    const isWakeable =
      thrownValue !== null &&
      typeof thrownValue === 'object' &&
      typeof thrownValue.then === 'function';

    workInProgressSuspendedReason = isWakeable
      ? // A wakeable object was thrown by a legacy Suspense implementation.
        // This has slightly different behavior than suspending with `use`.
        SuspendedOnDeprecatedThrowPromise
      : // This is a regular error. If something earlier in the component already
        // suspended, we must clear the thenable state to unblock the work loop.
        SuspendedOnError;
  }

  workInProgressThrownValue = thrownValue;

  const erroredWork = workInProgress;
  if (erroredWork === null) {
    // This is a fatal error
    workInProgressRootExitStatus = RootFatalErrored;
    logUncaughtError(
      root,
      createCapturedValueAtFiber(thrownValue, root.current),
    );
    return;
  }

}

전환 업데이트가 추가로 발생하면(interleaved update) IO-Bound 렌더링의 Lanes를 root의 suspendedLanes로 기록하고 신규 전환 업데이트로 렌더링을 새로 시작한다.

workInProgressRootInterleavedUpdatedLanes와 workInProgressRootSkippedLanes에 idle lane만 있으면 workInProgressSuspendedReason = SuspendedOnAction 혹은 data이고 이는 work loop를 멈추게 된다.

일반적인 suspense라면 SuspendedOnImmediate가 되고, 에러라면 SuspendedOnError가 됨을 알 수 있다. 다시 workloop를 보면 thenable의 resolve 함수에 root schedule를 추가함으로써 resolve될 때에 새로운 work가 스케줄될 것임을 알 수 있다.

일반적인 suspense에서는 workInProgressSuspendedReason = SuspendedAndReadyToContinue;가 됨으로 markRootSuspended를 호출 후 root가 다시 스케줄되고, 다음 work에서

case SuspendedAndReadyToContinue: {
  const thenable: Thenable<mixed> = (thrownValue: any);
  if (isThenableResolved(thenable)) {
    // The data resolved. Try rendering the component again.
    workInProgressSuspendedReason = NotSuspended;
    workInProgressThrownValue = null;
    replaySuspendedUnitOfWork(unitOfWork);
  } else {
    // Otherwise, unwind then continue with the normal work loop.
    workInProgressSuspendedReason = NotSuspended;
    workInProgressThrownValue = null;
    throwAndUnwindWorkLoop(
      root,
      unitOfWork,
      thrownValue,
      SuspendedAndReadyToContinue,
    );
  }
  break;
}

 

를 통해 resolve되었다면 work loop를 재개하고, 아니라면 throwAndUnwindWorkLoop를 통해 가장 가까운 suspense boundary를 찾은 후 fallback을 보여줌을 알 수 있다.

function workLoopSync() {
  // Perform work without checking if we need to yield between fiber.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
function workLoopConcurrentByScheduler() {
  while (workInProgress !== null && !shouldYield()) {
  	performUnitOfWork(workInProgress);
  }
}
function performUnitOfWork(unitOfWork: Fiber): void {
  // The current, flushed, state of this fiber is the alternate.
  const current = unitOfWork.alternate;

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
  } else {
    if (__DEV__) {
    } else {
      next = beginWork(current, unitOfWork, entangledRenderLanes);
    }
  }

  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

 

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

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__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 {
      // Neither props nor legacy context changes. Check if there's a pending
      // update or context change.
      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;
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        // This is a special case that only exists for legacy mode.
        // See https://github.com/facebook/react/pull/19216.
        didReceiveUpdate = true;
      } else {
        // An update was scheduled on this fiber, but there are no new props
        // nor legacy context. Set this to false. If an update queue or context
        // consumer produces a changed value, it will set this to true. Otherwise,
        // the component will assume the children have not changed and bail out.
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;

    if (getIsHydrating() && isForkedChild(workInProgress)) {
      const slotIndex = workInProgress.index;
      const numberOfForks = getForksAtLevel(workInProgress);
      pushTreeId(workInProgress, numberOfForks, slotIndex);
    }
  }

  workInProgress.lanes = NoLanes;

  switch (workInProgress.tag) {
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        disableDefaultPropsExceptForClasses ||
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case SuspenseComponent:
      return updateSuspenseComponent(current, workInProgress, renderLanes);
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
    case ContextConsumer:
      return updateContextConsumer(current, workInProgress, renderLanes)
  }

  throw new Error(
    `Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
      'React. Please file an issue.',
  );
}

 

function updateFunctionComponent(
  current: null | Fiber,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  
  let context;
  let nextChildren;
  let hasId;
  prepareToReadContext(workInProgress, renderLanes);

   nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
   );
   hasId = checkDidRenderIdHook();

  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

  if (getIsHydrating() && hasId) {
    pushMaterializedTreeId(workInProgress);
  }

  workInProgress.flags |= PerformedWork;
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

 

// packages/react-reconciler/src/ReactFiberHooks.js

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber = workInProgress;

  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;

  ReactSharedInternals.H =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;

  let children = Component(props, secondArg);

  // Check if there was a render phase update
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
    // Keep rendering until the component stabilizes (there are no more render
    // phase updates).
    children = renderWithHooksAgain(
      workInProgress,
      Component,
      props,
      secondArg,
    );
  }

  finishRenderingHooks(current, workInProgress, Component);

  return children;
}

 

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

'React' 카테고리의 다른 글

transition과 concurrent render  (0) 2025.12.20
React 톺아보기 3. scheduler (1)  (0) 2025.06.29
React 톺아보기 2. reconciler (1)  (0) 2025.06.09
React 톺아보기 - 1. ReactDom  (0) 2025.06.05
React에서 batch 처리와 렌더링 주기: 무한 재렌더링  (1) 2025.01.15
    'React' 카테고리의 다른 글
    • transition과 concurrent render
    • React 톺아보기 3. scheduler (1)
    • React 톺아보기 2. reconciler (1)
    • React 톺아보기 - 1. ReactDom
    Early Riser
    Early Riser
    2년차 프론트엔드 개발자입니다. https://github.com/EarlyRiser42

    티스토리툴바