React 톺아보기 - 05. Reconciler_5

류지승·2024년 8월 23일

React

목록 보기
8/19

Commit Phase

PerformSyncWorkOnRoot()

function performSyncWorkOnRoot(root) {
  /*...*/
  if (workInProgress !== null) {
    /* Render phase.. */
    // executionContext = prevExecutionContext

    // workInProgree가 null이면
    // 더 이상 처리해야할 fiber가 존재하지 않는다.
    // 즉 render phase가 정상적으로 끝났다.
    if (workInProgress !== null) {
      invariant(
        false,
        'Cannot commit an incomplete root. This error is likely caused by a ' +
          'bug in React. Please file an issue.'
      )
    } 
    
    // root -> current 연결
    // containerInfo ReactDOM.render(<App/>, container)
    // pendingChildren -> update props
    // finishedWork -> workInprogress Tree 완성
    else {
      // 완성된 workInProgress Tree를 current의 alternate로 연결
      root.finishedWork = root.current.alternate
      root.finishedExpirationTime = expirationTime
      finishSyncRender(root, workInProgressRootExitStatus, expirationTime)
    }
  }

  return null // 잔여 작업이 없으므로 null을 리턴.
}
// function finishSyncRender 
function finishSyncRender(root, exitStatus, expirationTime) {
  workInProgressRoot = null
  commitRoot(root)
}

finishSyncRender()

function finishSyncRender(root, exitStatus, expirationTime) {
  // Set this to null to indicate there's no in-progress render.
  workInProgressRoot = null;
  commitRoot(root);
}

function commitRoot(root) {
  // 현재 우선순위 확인하는 함수
  const renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority(
    // commit phase는 가장 먼저 실행시켜야하기 떄문에,
    // ImmediatePriority를 할당시켜줘야한다. 
    ImmediatePriority,
    // 실제 DOM을 업데이트 수행시켜주는 함수
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}

CommitRootImpl()

function commitRootImpl(root, renderPriorityLevel) {
  const finishedWork = root.finishedWork
  const expirationTime = root.finishedExpirationTime
  // 한번 더 확인해주죠? 잘 완료가 되었는지?
  if (finishedWork === null) {
    return null
  }

  // finishedWork, expirationTime을 이미 위에 할당했기 때문에 
  // 초기화해도 문제가 없다
  // 초기화
  root.finishedWork = null
  root.finishedExpirationTime = NoWork
  // 다음 실행될 노드의 정보 
  root.callbackNode = null
  root.callbackExpirationTime = NoWork
  root.callbackPriority = NoPriority
  root.nextKnownPendingLevel = NoWork

  // Effect list의 head를 가지고 온다.
  let firstEffect
  if (finishedWork.effectTag > PerformedWork) {
    if (finishedWork.lastEffect !== null) {
      finishedWork.lastEffect.nextEffect = finishedWork
      firstEffect = finishedWork.firstEffect
    } 
    else {
      // finshedWork.lastEffect가 null인 경우 
      // effectList가 한개인경우 
      // 또는 effectTag는 있지만 efffectList가 없는 경우
      firstEffect = finishedWork
    }
  }
  // finishedWork.effectTag가 performedWork이면
  // firstEffect에 null을 할당
  else {
    // There is no effect on the root.
    // root에 effect가 존재하지 않는다. 
    firstEffect = finishedWork.firstEffect
  }
  
  // effectTag가 있다는 얘기임
  // sideEffect를 처리해야함
  if (firstEffect !== null) {
    // 현재 react에서의 실행컨텍스트를 들고와
    // prevExecitonContext에 저장해놓고
    // commit context임을 명시
    const prevExecutionContext = executionContext
    executionContext |= CommitContext

    // 전
    // DOM 변경 직전
    // 컴포넌트가 DOM을 변경하기 전에 상태를 저장하거나,
    // 애니메이션을 설정하는 작업
    
    // 좀 더 세밀하게 말하자면
    // 전에 사용했던 useLayoutEffect를 cleanUp 시키고
    // 현재 사용해야하는 useLayoutEffect 동기적 부수효과를 실행시킴
    nextEffect = firstEffect
    do {
      try {
        commitBeforeMutationEffects()
      } catch (error) {
        invariant(nextEffect !== null, 'Should be working on an effect.')
        captureCommitPhaseError(nextEffect, error)
        nextEffect = nextEffect.nextEffect
      }
    } while (nextEffect !== null)

    // 변형
    // 실제 DOM을 변경하는 과정
    // placement, deletion, update effect tag 소비
    nextEffect = firstEffect
    do {
      try {
        commitMutationEffects(root, renderPriorityLevel)
      } catch (error) {
        invariant(nextEffect !== null, 'Should be working on an effect.')
        captureCommitPhaseError(nextEffect, error)
        nextEffect = nextEffect.nextEffect
      }
    } while (nextEffect !== null)

    // workInProgress tree를 DOM에 적용했으니 이젠 current로 취급한다.
    root.current = finishedWork

    // 후
    // DOM이 변경된 이후 화면에 요소가 그려진 후에 특정 요소의 크기를 측정하거나, 
    // 레이아웃에 따라 추가 작업을 수행
    // 전에 사용했던 useEffect clean up 실행하고
    // 이후 현재 useEffect 부수효과 실행
    nextEffect = firstEffect
    do {
      try {
        commitLayoutEffects(root, expirationTime)
      } catch (error) {
        invariant(nextEffect !== null, 'Should be working on an effect.')
        captureCommitPhaseError(nextEffect, error)
        nextEffect = nextEffect.nextEffect
      }
    } while (nextEffect !== null)

    nextEffect = null

    // 브라우저가 화면을 렌더링 할 수 있도록 scheduler에게 알린다.
    requestPaint()

    executionContext = prevExecutionContext
  } else {
    // No effects.
    root.current = finishedWork
  }

  // Passive effect(useEffect)를 위한 설정
  const rootDidHavePassiveEffects = rootDoesHavePassiveEffects
  if (rootDoesHavePassiveEffects) {
    // 화면이 그려지고 난 후에 실행될 effect들이 아직 남아 있으므로 root를 잡아둔다.
    rootDoesHavePassiveEffects = false
    rootWithPendingPassiveEffects = root
    pendingPassiveEffectsExpirationTime = expirationTime
    pendingPassiveEffectsRenderPriority = renderPriorityLevel
  } else {
    // Passive effect가 없으면 effect를 모두 소비한 것이므로 GC를 위해 참조를 끊어준다.
    nextEffect = firstEffect
    while (nextEffect !== null) {
      const nextNextEffect = nextEffect.nextEffect
      nextEffect.nextEffect = null
      nextEffect = nextNextEffect
    }
  }

  ensureRootIsScheduled(root)
  flushSyncCallbackQueue()
  return null
}

commitBeforeMutationEffects()

function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag

    // 클래스 컴포넌트의 getSnapshotBeforeUpdate()
    // 클래스형 컴포넌트이므로 패스
    if ((effectTag & Snapshot) !== NoEffect) {
      const current = nextEffect.alternate
      commitBeforeMutationEffectOnFiber(current, nextEffect)
    }

    // useEffect()를 사용하면 Passive tag가 달린다.
    if ((effectTag & Passive) !== NoEffect) {
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true // root를 잡아둬야 하는지 알려주는 플래그
        // 다음 프레임이 실행될 수 있도록 Passive effect 소비 함수 전달
        // 스케쥴러한테 commit phase가 끝나고 DOM update가 된 이후 스케쥴링에 의해 실행됨
        // 이 떄 passive Effect가 소비되는 거임.
        scheduleCallback(NormalPriority, () => { 
          flushPassiveEffects()
          return null
        })
      }
    }

    nextEffect = nextEffect.nextEffect
  }
}

flushPassiveEffectImpl()

function flushPassiveEffectsImpl() {
  // commitRoot()에서 잡아두었던 root
  // root라고 생각하면 됨
  if (rootWithPendingPassiveEffects === null) {
    return false
  }

  const root = rootWithPendingPassiveEffects
  // 전역 변수 정리
  rootWithPendingPassiveEffects = null
  pendingPassiveEffectsExpirationTime = NoWork

  invariant(
    (executionContext & (RenderContext | CommitContext)) === NoContext,
    'Cannot flush passive effects while already rendering.'
  )

  // 위에서 작성했음 참고 
  // react context한테 현재 commit 상태임을 명시
  // passive Effect를 소비하는 것 또한 
  // commit phase 과정이어야 함
  const prevExecutionContext = executionContext
  executionContext |= CommitContext
  // 신기한게 결국 executionContext는
  // PassiveEffectContext로 변경되어야하는데
  // 여기서 왜 commitContext로 한 번 변경하는지 궁금
  // commit에서 effect를 소비하는 과정
  let effect = root.current.firstEffect
  while (effect !== null) {
    try {
      commitPassiveHookEffects(effect)
    } catch (error) {
      invariant(effect !== null, 'Should be working on an effect.')
      captureCommitPhaseError(effect, error)
    }
    const nextNextEffect = effect.nextEffect
    // Remove nextEffect pointer to assist GC
    effect.nextEffect = null
    effect = nextNextEffect
  }

  executionContext = prevExecutionContext

  return true
}

// commitPassiveHookEffects
import {
  NoEffect as NoHookEffect,
  UnmountPassive,
  MountPassive,
} from './ReactHookEffectTags';

function commitPassiveHookEffects(finishedWork: Fiber): void {
  if ((finishedWork.effectTag & Passive) !== NoEffect) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork)
        commitHookEffectList(NoHookEffect, MountPassive, finishedWork)
        break
      }
      default:
        break
    }
  }
}

Life cycle effect 생성

import {
  Update as UpdateEffect,
  Passive as PassiveEffect,
} from 'shared/ReactSideEffectTags';
import {
  UnmountMutation,
  MountLayout,
  UnmountPassive,
  MountPassive,
} from './ReactHookEffectTags';

// useEffect()
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    deps
  )
}
// useLayoutEffect()
function mountLayoutEffect(
// create의 첫번째 인자로 넘어오는 함수
// 반환하는 함수는 cleanUp function
// 클린업 함수가 필요한 경우 
// 타이머함수나 이벤트 리스너를 사용하는 경우
// websocket이나 DataStream을 사용한 경우 
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
  return mountEffectImpl(
    UpdateEffect,
    UnmountMutation | MountLayout,
    create,
    deps
  )
}
// fiberEffectTag VS hookEffectTag
// fiberEffectTag는 우리가 아는 effect Tag(placement / deletion / update)
// hookEffectTag는  useEffect, useLayoutEffect 처리할 때 쓰는 tag (mount / unmount / passive / layout)

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  const hook = mountWorkInProgressHook()
  // deps === undefined라는 얘기
  // 빈배열이라는 얘기는 아님
  // 아에 의존성 배열이 제공되지 않는 경우
  // -> 모든 렌더링 후에 실행됨. 
  const nextDeps = deps === undefined ? null : deps
  // sideEffectTag라 함은 -> useEffect | useLayoutEffect가 호출될 때 생성되는 effectTag
  sideEffectTag |= fiberEffectTag
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps)
}

pushEffect

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag, // hookEffectTag
    create, // useEffect function
    destroy, // cleanUp function
    deps, // 의존성 배열
    next: null, 
  }
  
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue() // return { lastEffect: null }
    // circular linked list
    componentUpdateQueue.lastEffect = effect.next = effect
  } 
  else {
    const lastEffect = componentUpdateQueue.lastEffect
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect // circular
    } 
    else {
      const firstEffect = lastEffect.next // circular
      lastEffect.next = effect
      effect.next = firstEffect
      componentUpdateQueue.lastEffect = effect
    }
  }
  return effect
}

updateEffect

// useEffect()
function updateEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
  return updateEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    deps,
  );
}

// useLayoutEffect()
function updateLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
  return updateEffectImpl(
    UpdateEffect,
    UnmountMutation | MountLayout,
    create,
    deps,
  );
}

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
  // hook 생성
  const hook = updateWorkInProgressHook()
  // 의존성 배열
  const nextDeps = deps === undefined ? null : deps
  let destroy = undefined
// 현재 hook이 존재하면? 
  if (currentHook !== null) {
    // 현재 훅의 저장되어있는 state
    const prevEffect = currentHook.memoizedState
    // clean up function
    destroy = prevEffect.destroy
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps
      // 의존성 배열이 전과 후가 같은 지 확인하기 
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 전의 의존성 배열 참조값과 현재 의존성 배열의 참조값이 동일하면? 
        pushEffect(NoHookEffect, create, destroy, nextDeps)
        return
      }
    }
  }

  sideEffectTag |= fiberEffectTag
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps)
}

그니깐 한 번 더 정리하자면, mountEffect와 updateEffect는 최초 마운트 또는 업데이트할 때, 만약 useEffect를 통해 어떤 부수효과를 일어나게 되면, 이걸 한 componentUpdateQueue에 저장한다.

라이플 사이클 Effect를 소비하는 commitHookEffectList()

function commitHookEffectList(unmountTag, mountTag, finishedWork) {
  // 
  const updateQueue = finishedWork.updateQueue
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null
  if (lastEffect !== null) {
    // updateQueue는 원형연결리스트이므로 
    // lastEffect.next는 firstEffect이다. 
    const firstEffect = lastEffect.next
    let effect = firstEffect
    do {
      if ((effect.tag & unmountTag) !== NoHookEffect) {
        // Unmount
        // 언마운트이므로 destory를 실행해야함
        const destroy = effect.destroy
        effect.destroy = undefined
        if (destroy !== undefined) {
          destroy()
        }
      }
      if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount
        // 최초 마운트이므로, create를 통해서 부수효과를 실행시켜야함. 
        const create = effect.create
        // create의 반환값으로 destory를 할당하므로 
        // destory에 create return을 할당
        effect.destroy = create()
      }
      effect = effect.next
      // 종료조건 effect === firstEffect 
      // => 즉 순회를 다 끝낸 상태
    } while (effect !== firstEffect)
  }
}

commitMutationEffect()

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag

    if (effectTag & ContentReset) {
      commitResetTextContent(nextEffect) // node.textContent = text
    }

    let primaryEffectTag = effectTag & (Placement | Update | Deletion)
	// 왜 placement만 effectTag에서 삭제되는지 ?
    // deletion 짜피 그 노드 삭제되므로 effectTag를 삭제할 필요가 없음
    // update 이후 effectTag가 덮어져 씌어짐
    // 하지만 placement는 삭제가 되지 않아
    // 임의적으로 placement tag를 삭제해야함.
    switch (primaryEffectTag) {
      case Placement: {
        commitPlacement(nextEffect)
        nextEffect.effectTag &= ~Placement
        break
      }
      case PlacementAndUpdate: {
        commitPlacement(nextEffect)
        nextEffect.effectTag &= ~Placement
        const current = nextEffect.alternate
        commitWork(current, nextEffect)
        break
      }
      case Update: {
        const current = nextEffect.alternate
        commitWork(current, nextEffect)
        break
      }
      case Deletion: {
        commitDeletion(root, nextEffect, renderPriorityLevel)
        break
      }
    }

    nextEffect = nextEffect.nextEffect
  }
}

commitPlacement

DOM에 element 삽입을 처리하기 위해서 다음 두 가지의 컴포넌트가 필요함.
1. 부모 호스트 컴포넌트
2. placement tag가 달려있지 않은 형제 컴포넌트

function commitPlacement(finishedWork: Fiber): void {
  // Recursively insert all host nodes into the parent.
  // 위로 올라가면서 hostComponent를 찾음
  const parentFiber = getHostParentFiber(finishedWork)

  let parent
  let isContainer
  const parentStateNode = parentFiber.stateNode
  // 부모 HTML element 추출
  switch (parentFiber.tag) {
    case HostComponent:
      parent = parentStateNode
      isContainer = false
      break
    case HostRoot:
      // Host root의 stateNode는 root이므로 containerInfo에서 꺼내야 합니다.
      parent = parentStateNode.containerInfo
      isContainer = true
      break
    /*...*/
    default:
      invariant(
        false,
        'Invalid host parent fiber. This error is likely caused by a bug ' +
          'in React. Please file an issue.'
      )
  }
const before = getHostSibling(finishedWork);

let node: Fiber = finishedWork;
  while (true) {
    const isHost = node.tag === HostComponent || node.tag === HostText;
    // 현재 노드가 hostComponent면?
    if (isHost)) {
      // node의 instance를 들고오고
      const stateNode = node.stateNode;
      // placementTag가 없는 형제가 존재하고 부모가 root면?
      if (before) {
        if (isContainer) {
          insertInContainerBefore(parent, stateNode, before); // parent.insertBefore(stateNode, before)
        } 
        // placementTag가 없는 형제가 존재하고 부모가 root가 아니면?
        else {
          insertBefore(parent, stateNode, before);
        }
      } 
      // placementTag가 없는 형제가 없고 부모가 root면?
      else {
        if (isContainer) {
          appendChildToContainer(parent, stateNode); // appendChild(parent, stateNode)
        } 
        // placementTag가 없는 형제가 없고 부모가 root가 아니면?
        else {
          appendChild(parent, stateNode);
        }
      }
    // 호스트 컴포넌트가 아니라면 밑으로 내려간다.
    } 
    // 호스트 컴포넌트가 아니고, node.child이 존재하면
    else if (node.child !== null) {
      node.child.return = node;
      node = node.child;
      continue;
    }

    // 삽입한 노드가 finishedWork라면 작업완료를 뜻한다.
    if (node === finishedWork) {
      return;
    }
    // 형제가 없다면 위로 올라간다.
    while (node.sibling === null) {
      if (node.return === null || node.return === finishedWork) {
        return;
      }
      node = node.return;
    }
    // 형제로 이동
    node.sibling.return = node.return;
    node = node.sibling;
  }
	
}

function getHostParentFiber(fiber: Fiber): Fiber {
  let parent = fiber.return
  while (parent !== null) {
    if (isHostParent(parent)) { 
      return parent
    }
    parent = parent.return
  }
  invariant(
    false,
    'Expected to find a host parent. This error is likely caused by a bug ' +
      'in React. Please file an issue.'
  )
}

function isHostParent(fiber: Fiber): boolean {
  return (
    fiber.tag === HostComponent ||
    fiber.tag === HostRoot ||
    fiber.tag === HostPortal
  )
}
// 여기는 placement Tag가 달려있지 않는 형제노드를 찾는다는데..
// 먼말이지? 
function getHostSibling(fiber: Fiber): ?Instance {
  let node: Fiber = fiber
  siblings: while (true) {
    while (node.sibling === null) {
      if (node.return === null || isHostParent(node.return)) {
        return null
      }
      node = node.return
    }

    node.sibling.return = node.return
    node = node.sibling
	// 이 여기 순회문이 이해가 안 돼
    while (node.tag !== HostComponent && node.tag !== HostText) {
      if (node.effectTag & Placement) {
        continue siblings
      }
      if (node.child === null) {
        continue siblings
      } else {
        node.child.return = node
        node = node.child
      }
    }
	// 여긴 이해됨.
    // 형제노드가 effectTag로 placement가 없으면
    // 그 노드를 반환하라는 소리임
    if (!(node.effectTag & Placement)) {
      // Found it!
      return node.stateNode
    }
  }
}
profile
성실(誠實)한 사람만이 목표를 성실(成實)한다

0개의 댓글