HostRootFiber가 beginWork에서 처리되는 과정을 살펴보자.
beginWork
는 커다란 switch-case문으로 workInProgress의 Fiber tag에 따라 서로 다른 update 함수를 호출한다.
HostRootFiber
는 HostRoot
case로 이동한다.
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
...
switch (workInProgress.tag) {
case FunctionComponent: {
...
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);
...
}
}
Update queue를 처리해 <App/>
을 얻어낸 뒤 reconcileChildren
을 호출한다. 큐의 처리 방법은 넘어가고 다음에 알아보자.
function updateHostRoot(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// trigger phase에서 넘겼던 데이터를 추출해
// workInProgress의 memoizedState에 할당한다.
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
const nextState: RootState = workInProgress.memoizedState;
// root.render에 넘겼던 element을 얻어냈다!
const nextChildren = nextState.element;
if (supportsHydration && prevState.isDehydrated) {
...
} else {
...
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
return workInProgress.child;
}
reconcileChildren
은 아주 핵심적인 함수이다.
nextChildren
(여기서는 <App/>
)을 보며 workInProgress
트리의 child
노드를 만든다.current
트리와 비교하며 Fiber의 삭제, 수정같은 side effect들을 체크한다.export function reconcileChildren(
// updateContainer에서 업데이트 큐에 넣어둔 HostRootFiber
current: Fiber | null,
// prepareFreshStack에서 만든 HostRootFiber
workInProgress: Fiber,
// <App/>
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 지금 current는 HostRootFiber이므로 여기로 간다.
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
current
의 유무에 따라, mountChildFibers
혹은 reconcileChildFibers
를 호출한다. 두 함수의 차이점을 이어서 살펴보자.
두 함수는 createChildReconciler
에 전달되는 인자만 다르다.
export const reconcileChildFibers: ChildReconciler =
createChildReconciler(true);
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
createChildReconciler
는 아래와 같이 구현되어있다. shouldTrackSideEffects
을 사용하는 함수들이 본문에 정의되어있다.
function createChildReconciler(
shouldTrackSideEffects: boolean,
): ChildReconciler {
function deleteChild(){}
function deleteRemainingChildren(){}
...
function placeChild(){}
function placeSingleChild(){}
function updateTextNode(){}
function updateElement(){}
...
function createChild(){}
...
function reconcileChildrenArray(){}
...
function reconcileSingleTextNode(){}
function reconcileSingleElement(){}
...
function reconcileChildFibersImpl(){}
function reconcileChildFibers(){}
return reconcileChildFibers;
}
reconcileChildFibers
면 shouldTrackSideEffects
가 true, shouldTrackSideEffects
가 false이다.
원래 없던 Fiber 노드를 새로 마운트하는 경우에는 side effect가 있을 수 없고 그냥 새로 붙이면 되므로 따로 side effect를 추적하지 않음으로서 최적화하는 것이다.
지금 시점에는 reconcileChildFibers
가 호출된다고 했다. reconcileChildFibers
에 전달하는 인자들을 돌아보자.
workInProgress.child = reconcileChildFibers(
// prepareFreshStack에서 만든 HostRootFiber
workInProgress,
// current는 updateContainer에서 업데이트 큐에 넣어둔 HostRootFiber
// current가 아닌 current.child를 넘겨준다.
current.child,
// <App/>
nextChildren,
renderLanes,
);
reconcileChildFibers
는 reconcileChildFibersImpl
을 호출하고 예외처리를 한 것이다.
reconcileChildFibers
는 넘어가고 바로 reconcileChildFibersImpl
을 살펴보자. newChild의 종류에 따라 분기한다.
function reconcileChildFibersImpl(
// 전달인자명은 workInProgress이었지만 매개변수명은 returnFiber이다
// 현재 처리중인 Fiber의 부모 노드(return필드)가 workInProgress가 될 것이니 의미상 적절하다
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// Handle object types
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
// 여기서는 React Element를 처리한다.
// 예를 들어 <App/>의 $$typeof는 REACT_ELEMENT_TYPE이다.
case REACT_ELEMENT_TYPE: {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
const firstChild = placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
currentDebugInfo = prevDebugInfo;
return firstChild;
}
case REACT_PORTAL_TYPE:
...
case REACT_LAZY_TYPE:
...
}
...
throwOnInvalidObjectType(returnFiber, newChild);
}
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number' ||
typeof newChild === 'bigint'
) {
// 여기서는 텍스트 노드를 처리한다.
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
beginWork
에서 했던 분기와 헷갈릴 수 있는데 그 때는 Fiber의 종류에 따른 분기, 이번에는 newChild Element의 종류에 따른 분기이다.
$$typeof
가 나온김에 살펴보자면 shared/ReactSymbols.js
에 아래와 같이 이것저것 심볼로 정의되어있다:
// The Symbol used to tag the ReactElement-like types.
export const REACT_LEGACY_ELEMENT_TYPE: symbol = Symbol.for('react.element');
export const REACT_ELEMENT_TYPE: symbol = renameElementSymbol
? Symbol.for('react.transitional.element')
: REACT_LEGACY_ELEMENT_TYPE;
export const REACT_PORTAL_TYPE: symbol = Symbol.for('react.portal');
export const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment');
...
console.log(<App/>)
만 찍어봐도 바로 확인할 수 있다. $$typeof
에 대한 더 많은 내용은 https://overreacted.io/why-do-react-elements-have-typeof-property/ 에서 확인할 수 있다.
Element 종류 무관하게 분기들에 reconcileSingleTextNode
와 placeSingleChild
가 공통적으로 사용됨을 확인할 수 있다. 이 둘을 살펴보자.
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// 초기 렌더시에는 child가 null이므로 해당하지 않는다.
...
}
if (element.type === REACT_FRAGMENT_TYPE) {
...
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
coerceRef(created, element);
created.return = returnFiber;
return created;
}
}
간단하다. createFiberFromElement
로 <App/>
element를 나타내는 새로운 Fiber를 만들어 반환한다. createFiberFromElement의 내부 코드를 보면 element의 타입에 따라 Fiber의 tag를 결정하는 길고 긴 switch문이 있음을 확인할 수 있다.
자세히는 살펴보지 않겠지만 여기서 App 함수가 실행되는 것은 아니라는 점을 참고하자. 그저 App 함수 컴포넌트를 나타내는 Fiber 객체를 만들 뿐이다.
만들어진 Fiber 객체를 보면 아래처럼 생겼다:
FiberNode {
...
elementType: App,
tag: 0,
type: App,
...
}
만들어진 Fiber는 placeSingleChild
에서 Placement 플래그가 세워진다.
function placeSingleChild(newFiber: Fiber): Fiber {
// 아까 reconcile vs mount할 때 봤던 shouldTrackSideEffects가 나온다.
// 지금은 reconcile중이라 true이므로 newFiber에 flag를 세운다.
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement | PlacementDEV;
}
return newFiber;
}
placeSingleChild
에서 반환한 값은 beginWork
에서 그대로 반환하고 performUnitOfWork
에서 다음 workInProgress
로 설정된다.
결과적으로 다음 performUnitOfWork
에서는 방금 새로 만든 Fiber에 대해 작업을 시작한다.
로그인 중...