39초 전
리액트는 Trigger
, Render
, Commit
이 3단계로 초기 마운트를 진행한다.
이 3단계를 자세하게 살펴보겠다. (초기 마운트와 크게 관련이 없는 부분은 생략했다.)
(영상을 먼저 보면 아래 내용을 이해하기 더 쉽다.)
function FiberNode(tag, pendingProps, key, mode) {
this.tag = tag; // FiberNode의 종류
this.stateNode = null; // 실제 DOM 노드
// 트리와 같은 구조를 위한 속성
this.return = null;
this.child = null;
this.sibling = null;
this.pendingProps = pendingProps; // 현재 렌더링 중인 업데이트에서 사용되는 props값
this.memoizedProps = null; // 이전 렌더링에서 사용된 props 값
this.flags = NoFlags; // 특정 Fiber에서 수행해야 할 작업(커밋 단계에서 사용된다.)
this.alternate = null; // 이전 버전의 FiberNode
...
}
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // 함수(0)인지 클래스(1)인지 결정되기 전
export const HostRoot = 3; // Fiber Tree의 루트 노드
export const HostComponent = 5; // div, p와 같은 실제 DOM 요소
export const HostText = 6;
export const Mode = 8; // StrictMode
...
이 FiberNode들이 트리와 같은 구조로 되어 있고, 리액트는 이런 트리 구조를 순회하면서 컴포넌트를 렌더링한다. 이 트리 구조를 구성하는 속성은 return
, child
, sibling
이다.
return
: FiberNode의 부모
child
: FIberNode의 자식 (만약 자식이 여러 개일 경우, 첫 번째 자식만 child
로 설정한다.)
sibling
: FiberNode의 다음 형제
리액트는 DFS
방식으로 로 FiberTree를 순회한다.
// 예시
function traverseFiberTree(fiberNode) {
if(fiberNode.child !== null) {
// child로 이동
traverseFiberTree(fiberNode.child)
...
} else if(fiberNode.sibling !== null) {
traverseFiberTree(fiberNode.sibling)
...
} else {
let parent = fiberNode.return;
while (parent && !parent.sibling) {
parent = parent.return;
}
if (parent && parent.sibling) {
traverseFiberTree(parent.sibling);
}
}
}
https://github.com/facebook/react/tree/v18.2.0/packages/react-reconciler
위와 같이 깃허브의 리액트 소스코드를 보면서 내부 구조를 이해하는 것은 어렵기 때문에,
리액트 프로젝트를 하나 생성한 후, 크롬 개발자 도구에서 디버거 기능을 이용하여 분석했다.
[브레이크 포인트]
브레이크 포인트로 특정 코드 줄에서 실행을 일시 중지할 수 있다.
이를 이용하여 어떤 함수가 어떤 매개변수를 받고 어떤 값을 return 하는 지를 쉽게 볼 수 있다.
[콜 스택]
현재 함수가 어떤 함수에 의해 호출되었는지 추적할 수 있다.
[디버거의 동작]
스크립트 실행 계속하기
: 다음 줄 실행
Step Over
(다음 함수 호출) : 해당 함수 혹은 코드가 관련 없다고 생각하면 건너뛴다.
Step Out
(함수 벗어나기) : 현재 함수 실행을 마치고 호출한 함수로 이동한다.
createRoot() : FiberRootNode
와 HostRoot
를 생성하고 연결한다.
.render() : 스케줄러에 렌더 단계를 시작하는 함수를 등록한다.
createRoot(document.getElementById("root")!)
...
// container = div#root, options = undefined
function createRoot(container, options) {
...
var root = createContainer(container, ConcurrentRoot, ...);
...
return new ReactDOMRoot(root);
}
function createContainer(containerInfo, tag, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError, transitionCallbacks) {
...
return createFiberRoot(containerInfo, tag, hydrate, initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError);
}
// containerInfo = div#root, tag = 1
function createFiberRoot(containerInfo, tag, ...)
// FiberRootNode를 생성한다.
var root = new FiberRootNode(containerInfo, tag, hydrate, identifierPrefix, onRecoverableError);
// HostRoot를 생성한다.
var uninitializedFiber = createHostRootFiber(tag, isStrictMode);
// FiberRootNode.current = hostRoot
root.current = uninitializedFiber;
// hostRoot.stateNode = FiberRootNode
uninitializedFiber.stateNode = root;
...
return root;
}
먼저 생성자 함수인 new FiberRootNode()
로 RootFiberNode
인 root
를 생성한다. 이 과정에서 RootFiberNode
에 containerInfo
속성에 document.getElementById("root")!
인 div#root
가 할당된다.
다만 RootFiberNode
는 초기 마운트 작업에서는 주로 사용되지는 않는다.
createHostRootFiber()
를 호출하여 HostRoot
를 생성한다. HostRoot
는 FiberTree
의 루트 노드이다. FiberTree
를 이용한 작업(예: 초기 마운트, 리렌더링)들은 HostRoot
에서 시작하여 HostRoot
에서 끝난다.
마지막으로 FiberRootNode
와 HostRoot
를 생성하여 둘을 연결한다.
render()
함수가 실행되기 전에 JSX(<StrictMode><App /></StrictMode>
)를 React Element로 변환한다. 아래에 생성된 React Element를 볼 수 있다.
주의해야 할 점이 있는데, 이 과정에서 모든 계층의 요소들이 한 번에 React Element로 변환되는 것이 아니다.
함수 컴포넌트는 렌더링 단계에서 함수가 실행되어 자식에 대한 React Element를 생성된다.
(사진에서 type: f App()
인 React Element는 props
가 빈 객체인 것을 볼 수 있다.)
render
는 렌더 단계를 시작하는 함수를 스케줄러에 등록하는 함수이다.
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
// children = <StrictMode><App /></StrictMode> -> React Element
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function (children) {
var root = this._internalRoot;
...
updateContainer(children, root, null, null);
};
// element = React Element, container = RootFiberNode
function updateContainer(element, container, parentComponent, callback) {
var current$1 = container.current; // HostRoot
var eventTime = requestEventTime();
var lane = requestUpdateLane(current$1);
...
var update = createUpdate(eventTime, lane);
...
update.payload = {
element: element
};
...
const root = enqueueUpdate(current$1, update, lane) // RootFiberNode
...
if (root !== null) {
scheduleUpdateOnFiber(root, current$1, lane, eventTime);
...
}
...
}
eventTime과 lane을 설정하고, createUpdate
로 update
객체를 생성하고, update.payload
에 element(children)
을 할당한다.
업데이트를 시작한 정확한 시간을 얻는다.
var eventTime = requestEventTime(); // localPerformance.now(); = 2238.5
Date.now()
와는 다르게, performance.now()
는 마이크로초 정밀도까지 표현될 수 있는 부동 소수점으로 시간을 나타낸다. 또한 Date.now()
는 시스템 시계에 의존하기 때문에 시스템과 시계 왜곡 등과 같은 사용자의 시계 조정에 영향을 받을 수 있다. 반면 performance.now()
메서드는 현재 시간이 감소하거나 조정되지 않는 모노토닉 시계인 timeOrigin
속성을 기준으로 한다.
Lane
은 React 18부터 렌더링 우선순위 관리를 위해 도입된 시스템이다. 즉, 업데이트들을 우선순위에 따라 다른 Lane
으로 분리해서 처리한다.
리액트에서 useTransition()
와 useDeferredValue()
를 본 적이 있을텐데 이것이 Lane을 활용한 React API이다.
렌더 단계에서 16
이라는 Lane을 자주 볼 수 있는데, 이 Lane은 DefaultLane인 0b10000
이다.
function requestUpdateLane(fiber) {
...
var eventLane = getCurrentEventPriority();
return eventLane;
}
function getCurrentEventPriority() {
var currentEvent = window.event; // undefined
if (currentEvent === undefined) {
return DefaultEventPriority; // 16
}
return getEventPriority(currentEvent.type);
}
return eventLane;
...
export const DefaultEventPriority: EventPriority = DefaultLane;
export const DefaultLane: Lane = 0b0000000000000000000000000010000; // 16
이 업데이트를 시작한 시간과 Lane
을 이용하여 업데이트 객체를 생성한다.
var update = createUpdate(eventTime, lane);
// eventTime = 2238.5, lane = 16
function createUpdate(eventTime, lane) {
var update = {
eventTime: eventTime,
lane: lane,
tag: UpdateState, // 0
payload: null,
callback: null,
next: null
};
return update;
}
function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
...
if ((executionContext & RenderContext) !== NoLanes && root === workInProgressRoot) {
...
} else {
...
ensureRootIsScheduled(root, eventTime);
...
}
}
// root: FiberRootNode,
function ensureRootIsScheduled(root, currentTime) {
var existingCallbackNode = root.callbackNode; // null
...
var newCallbackPriority = getHighestPriorityLane(nextLanes); // 16
var newCallbackNode;
if (newCallbackPriority === SyncLane) {
...
} else {
var schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
...
case DefaultEventPriority:
schedulerPriorityLevel = NormalPriority; // 3
break;
...
}
newCallbackNode = scheduleCallback$1(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
}
}
var taskQueue = [];
// priorityLevel = 3, callback = f() = performConcurrentWorkOnRoot(root), options = undefined
function scheduleCallback(priorityLevel, callback, options) {
var currentTime = exports.unstable_now(); // localPerformance.now() = 2238.5
var timeout;
switch (priorityLevel) {
...
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
break;
}
var expirationTime = startTime + timeout; // 2238.5 + 5000 = 7238.5
var newTask = {
id: taskIdCounter++,
callback: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
sortIndex: -1
};
if (startTime > currentTime) {
...
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// heap 자료구조로 sortIndex를 오름차순으로 taskQueue를 관리한다.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork); // flushWork는 렌더 단계를 시작하는 함수이다.
}
}
}
requestHostCallback
에 콜백함수로 flushWork
를 전달한다.
requestHostCallback()
이 실행되기 전에, 아래와 같이 비동기 작업 예약을 위한 세팅이 먼저 되어있다.
var schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
// Node.js 와 IE 환경
...
} else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker 환경
// setTimeout의 최소 4ms 문제 때문에 MessageChannel을 선호한다.
var channel = new MessageChannel(); // 1
var port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline; // 2
schedulePerformWorkUntilDeadline = function () { // 3
port.postMessage(null);
};
} else {
...
}
MessageChannel
객체를 생성하면 포트(port1, port2) 두 개가 생성된다.
메시지 이벤트 핸들러를 등록한다. port1이 메시지를 받을 때, performWorkUnitlDeadLine
함수가 실행된다.
schedulePerformWorkUntilDeadline
가 호출되면, post.postMessage(null)
로 port2
에서 메시지를 보내고, port1
이 이 메시지를 받고, 등록된 핸들러 performWorkUnitlDeadLine
를 실행한다.
MessageChannel
의 postMessage
는 마이크로태스크 큐에 쌓인다. 즉, Promise.then과 같은 우선순위를 갖고, setTimeout보다 먼저 실행된다.
var scheduledHostCallback = null;
// callback = flushWork;
function requestHostCallback(callback) {
scheduledHostCallback = callback; // flushWork
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
스케줄러에 함수를 등록하는 과정은 아래와 같다.
requestHostCallback()
을 호출하면 scheduledHostCallback
에 flushWork()
함수를 할당하고, schedulePerformWorkUntilDeadline()
를 호출한다.
schedulePerformWorkUntilDeadline()
이 호출되면 port1
에 메시지를 보낸다.
port1
이 메시지를 받고, 태스크 큐에 performWorkUntilDeadline()
를 추가한다.
콜스택이 비었으면, 이벤트 루프가 태스크 큐에서 함수를 가져와 performWorkUntilDeadline()
실행.
performWorkUntilDeadline()
에서 scheduledHostCallback
를 실행하여 flushWork
함수가 실행된다.
Trigger 단계의 마지막에서 비동기적으로 전달한 함수
let scheduledHostCallback; // flushWork(hasTimeRemaining, initialTime)
var performWorkUntilDeadline = function () {
if (scheduledHostCallback !== null) {
var currentTime = exports.unstable_now(); // localPerformance.now() = 384003.399...
startTime = currentTime;
var hasTimeRemaining = true;
var hasMoreWork = true;
try {
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime); // flushWork()
} finally {
...
}
} else {
...
}
};
scheduledHostCallback 함수(flushWork)를 호출하여 렌더 단계를 시작한다.
var currentPriorityLevel = NormalPriority; // 3
// hasTimeRemaining = true, initialTime = 384003.399
function flushWork(hasTimeRemaining, initialTime) {
...
var previousPriorityLevel = currentPriorityLevel; // 3
try {
if (enableProfiling) {
...
} else {
return workLoop(hasTimeRemaining, initialTime);
}
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
}
}
function workLoop(hasTimeRemaining, initialTime) {
var currentTime = initialTime; // 384003.399...
advanceTimers(currentTime);
currentTask = peek(taskQueue); // 아까 scheduleCallback에서 생성한 newTask
while (currentTask !== null && !(enableSchedulerDebugging )) {
...
var callback = currentTask.callback; // ensureRootIsScheduled()에서 설정한 performConcurrentWorkOnRoot(root)
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel; // 3
// 7238 <= 384003.399...
var didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// performConcurrentWorkOnRoot(root)로 진입!
var continuationCallback = callback(didUserCallbackTimeout);
...
} else {
...
}
...
}
...
}
여기서 “렌더 단계”를 Concurrent 모드로 할 것인지, Sync 모드로 할 것인지 결정한다.
초기 마운트는 화면에 UI를 먼저 표시하는 게 우선적이므로, Sync 모드로 진행한다.
// root = div#root
function performConcurrentWorkOnRoot(root, didTimeout) {
...
currentEventTime = NoTimestamp // -1;
currentEventTransitionLane = NoLanes // 0;
...
var originalCallbackNode = root.callbackNode; // task
...
// 16
var lanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
...
var shouldTimeSlice = !includesBlockingLane(root, lanes) && !includesExpiredLane(root, lanes) && ( !didTimeout);
var exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
...
return null;
}
function includesBlockingLane(root, lanes) {
var SyncDefaultLanes = InputContinuousHydrationLane | InputContinuousLane | DefaultHydrationLane | DefaultLane;
// 10000 & 11111 -> 10000 !== 0 -> true
return (lanes & SyncDefaultLanes) !== NoLanes;
}
// DefaultLane은 SyncLane = BlockingLane이다.
// !true -> false && ... -> 최종적으로 false가 된다.
var shouldTimeSlice = !includesBlockingLane(root, lanes) && !includesExpiredLane(root, lanes) && ( !didTimeout);
var exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
function renderRootSync(root, lanes) {
var prevExecutionContext = executionContext;
executionContext |= RenderContext;
...
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
...
prepareFreshStack(root, lanes);
}
...
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
...
}
function prepareFreshStack(root, lanes) {
...
workInProgressRoot = root;
var rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
...
return rootWorkInProgress;
}
FiberTree
를 위한 첫 시작점인 rootWorkInProgress를 생성한다.
리액트는 workInProgress
를 이용하여 새로운 FiberTree를 만든다. workInProgress
는 작업의 단위이자, FiberNode
이다.
createRoot()
에서 생성한 HostRoot
로 새로운 HostRoot
인 workInProgress
를 생성한다.
function createWorkInProgress(current, pendingProps) {
// 초기 마운트시에는 이전 FiberTree가 없기 때문에 current.alternate가 null이다.
var workInProgress = current.alternate; // null
if (workInProgress === null) {
// HostRoot로 workInProgress = FiberNode를 생성한다.
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
...
// 새로 만든 workInProgress(새로운 버전의 HostRoot)와 이전 HostRoot(createRoot에서 생성한 HostRoot)를 연결한다.
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
...
}
// 이전 HostRoot의 속성을 이용하여 새로 만든 workInProgress 속성을 할당?한다.
workInProgress.flags = current.flags & StaticMask;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.sibling = current.sibling;
...
{
// HostRoot는 tag가 3이다.
switch (workInProgress.tag) {
case IndeterminateComponent: // tag: 2
case FunctionComponent: // tag: 0
case SimpleMemoComponent: // tag: 15
workInProgress.type = resolveFunctionForHotReloading(current.type);
break;
case ClassComponent: // tag: 1
workInProgress.type = resolveClassForHotReloading(current.type);
break;
case ForwardRef: // tag: 11
workInProgress.type = resolveForwardRefForHotReloading(current.type);
break;
}
}
return workInProgress;
} // Used to reuse a Fiber for a second pass.
workLoopSync()
는 Fiber 노드를 순회하는 동기적인 루프이다.
workInProgress
가 null이 아닐 때까지 반복하면서 performUnitOfWork()
를 호출한다.
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
현재 workInProgress는 HostRoot이다. performUnitOfWork
가 어떻게 처리되는지 확인해보자.
function performUnitOfWork(unitOfWork) {
...
var current = unitOfWork.alternate; // createRoot에서 생성한 HostRoot
...
var next;
if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
...
next = beginWork(current, unitOfWork, subtreeRenderLanes);
...
} else {
...
}
...
}
function beginWork(current, workInProgress, renderLanes) {
...
if (current !== null) {
var oldProps = current.memoizedProps; // null
var newProps = workInProgress.pendingProps; // null
if (oldProps !== newProps || hasContextChanged() || (
} else {
...
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
...
} else {
didReceiveUpdate = false;
}
}
} else {
...
}
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
...
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
...
}
}
function updateHostRoot(current, workInProgress, renderLanes) {
...
var nextState = workInProgress.memoizedState;
...
// <StrictMode><App /></StrictMode> -> React Element 객체
var nextChildren = nextState.element;
if (prevState.isDehydrated) {
...
} else {
...
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
return workInProgress.child;
}
nextChildren
인 React Element를 이용하여 자식 FiberNode들을 생성한다.
초기 마운트에서는 HostRoot만 current 값을 가지고 있으므로 ( var current = unitOfWork.alternate;
) reconcileChildFibers
를 호출한다.
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
// 여기로 진입
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
}
}
...
var reconcileChildFibers = ChildReconciler(true);
var mountChildFibers = ChildReconciler(false);
function ChildReconciler(shouldTrackSideEffects) {...}
shouldTrackSideEffects
값은 placeSingleChild
에서 flags를 설정하기 위해 사용된다.
newChild
= nextChildren
(<StrictMode />
에 대한 React Element)는 자식이 1개 이므로 reconcileSingleElement
로 진입한다.
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
...
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: // Symbol(react.element)
// reconcileSingleElement()로 진입
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
...
}
...
}
...
}
...
}
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
...
if (element.type === REACT_FRAGMENT_TYPE) {
...
} else {
// createFiberFromElement()로 진입
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);
_created4.ref = coerceRef(returnFiber, currentFirstChild, element);
_created4.return = returnFiber;
return _created4;
}
}
function createFiberFromElement(element, mode, lanes) {
...
var type = element.type; // Symbol(react.strict_mode)
var key = element.key; // null
var pendingProps = element.props; // { children: {...} } -> <App />
var fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes);
...
return fiber;
}
function createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes) {
var fiberTag = IndeterminateComponent;
var resolvedType = type; // Symbol(react.strict_mode)
if (typeof type === 'function') {
...
} else if (typeof type === 'string') {
...
} else {
getTag: switch (type) {
...
case REACT_STRICT_MODE_TYPE:
fiberTag = Mode; // 8
mode |= StrictLegacyMode; // 11
if ( (mode & ConcurrentMode) !== NoMode) {
// Strict effects should never run on legacy roots
mode |= StrictEffectsMode; // 27
}
break;
...
}
var fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type; // Symbol(react.strict_mode)
fiber.type = resolvedType; // Symbol(react.strict_mode)
fiber.lanes = lanes; // 16
...
return fiber;
}
// tag = 8 (StrictMode), pendingProps = <App/>의 React Element,
var createFiber = function (tag, pendingProps, key, mode) {
// $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
return new FiberNode(tag, pendingProps, key, mode);
};
createFiber()
함수로 StrictMode에 대한 FiberNode를 생성한다.
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
...
if (element.type === REACT_FRAGMENT_TYPE) {
...
} else {
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);
// 여기로 나오게 된다.
_created4.ref = coerceRef(returnFiber, currentFirstChild, element); // null
_created4.return = returnFiber; // return(Fiber Tree의 부모)으로 HostRoot를 할당한다.
return _created4;
}
}
트리와 같은 구조를 위해 자식 FiberNode의 return
으로 부모 FiberNode(HostRoot)를 할당한다.
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
...
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: // Symbol(react.element)
// placeSingleChild로 진입
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
...
}
...
}
...
return reconcileChildFibers;
}
HostRoot만 shouldTrackSideEffects
이 true이므로 HostRoot의 자식(StrictMode)의 flags 값이 Placement
로 설정된다.
이는 나중에 커밋 단계에서 containerInfo
를 연결하기 위해 설정된다.
function placeSingleChild(newFiber) {
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}
export type Flags = number;
export const NoFlags = /* */ 0b00000000000000000000000000;
export const PerformedWork = /* */ 0b00000000000000000000000001;
export const Placement = /* */ 0b00000000000000000000000010;
export const Update = /* */ 0b00000000000000000000000100;
export const Deletion = /* */ 0b00000000000000000000001000;
export const ChildDeletion = /* */ 0b00000000000000000000010000;
...
function performUnitOfWork(unitOfWork) {
...
if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
// next에 StrictMode FiberNode를 할당한다.
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
...
}
if (next === null) {
...
} else {
workInProgress = next;
}
...
}
workInProgress = next = StrictMode에 대한 FiberNode를 한다.
workLoopSync()
에서 workInProgress ≠ null이므로 StrictMode에 대한 FiberNode로 performUnitOfWork()
를 다시 호출한다.
function performUnitOfWork(unitOfWork) {
var current = unitOfWork.alternate; // null
setCurrentFiber(unitOfWork);
var next;
if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
// beginWork로 진입
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
...
}
resetCurrentFiber();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.ㄹ
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner$2.current = null;
}
// current = null, workInProgress = ReactStrictMode FiberNode, renderLanes = 16
function beginWork(current, workInProgress, renderLanes) {
...
if (current !== null) {
...
} else {
didReceiveUpdate = false;
}
...
switch (workInProgress.tag) {
...
case Mode:
return updateMode(current, workInProgress, renderLanes);
...
}
}
HostRoot에서와는 다르게 beginWork
에서 updateMode()
를 호출한다.
function updateMode(current, workInProgress, renderLanes) {
// <App />의 React Element
var nextChildren = workInProgress.pendingProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
updateMode
에서도 동일하게 reconcileChildren
을 호출한다.
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
...
}
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
...
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// reconcileSingleElement로 진입
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
...
}
...
}
...
}
function reconcileSingleElement(
returnFiber,
currentFirstChild,
element,
lanes
) {
var key = element.key;
var child = currentFirstChild;
...
if (element.type === REACT_FRAGMENT_TYPE) {
...
} else {
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);
_created4.ref = coerceRef(returnFiber, currentFirstChild, element);
_created4.return = returnFiber;
return _created4;
}
}
function createFiberFromElement(element, mode, lanes) {
...
var type = element.type; // f App()
var key = element.key;
var pendingProps = element.props;
var fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes);
{
fiber._debugSource = element._source;
fiber._debugOwner = element._owner;
}
return fiber;
}
function createFiberFromTypeAndProps(type, // React$ElementType
key, pendingProps, owner, mode, lanes) {
var fiberTag = IndeterminateComponent; // 2
...
// tag = 2(IndeterminateComponent), pendingProps = {}, key = null, mode= 27
var fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type; // f App()
fiber.type = resolvedType; // f App()
fiber.lanes = lanes; // lane = 16
return fiber;
}
자식인 <App/>
에 대한 FiberNode가 생성된다.
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
...
if (element.type === REACT_FRAGMENT_TYPE) {
...
} else {
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);
// 여기로 빠져나옴
_created4.ref = coerceRef(returnFiber, currentFirstChild, element); // null
_created4.return = returnFiber; // react.strictMode FiberNode
return _created4;
}
}
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
...
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// placeSingleChild로 진입
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
...
}
...
}
...
}
function placeSingleChild(newFiber) {
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}
shouldTrackSideEffects
이 false
이므로 flags
가 설정되지 않는다.
function performUnitOfWork(unitOfWork) {
...
if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
// 여기로 빠져나옴
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
...
}
...
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// {children: <App/>에 대한 React Element}
if (next === null) {
...
} else {
workInProgress = next;
}
ReactCurrentOwner$2.current = null;
}
workInProgress가 App에 대한 FiberNode로 설정된다.
이제부터는 과정들이 비슷하므로 다른 부분들만 살펴보겠다.
function beginWork(current, workInProgress, renderLanes) {
...
if (current !== null) {
...
} else {
didReceiveUpdate = false;
...
}
workInProgress.lanes = NoLanes; // 0
switch (workInProgress.tag) {
case IndeterminateComponent:
{
return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
}
...
}
}
App은 workInProgress = StrictMode
의 createFiberFromTypeAndProps
에서 tag가 IndeterminateComponent
로 설정됐었다. 그래서 mountIndeterminateComponent
로 진입하게된다.
// Component = workInProgress.type = f() App
function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) {
var props = workInProgress.pendingProps; // {}
var context; // {}
...
var value; // undefined
var hasId; // undefined
...
{
...
setIsRendering(true);
ReactCurrentOwner$1.current = workInProgress;
// renderWithHooks로 진입
value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes);
hasId = checkDidRenderIdHook();
setIsRendering(false);
}
...
}
2.0 : React Element 생성
에서 모든 계층의 요소들이 한 번에 React Element로 만들어지지 않는다고 말했었는데, renderWithHooks
에서 App 함수가 실행되면서 App 컴포넌트의 자식 요소들이 React Element로 만들어진다.
// Component = workInProgress.type = f() App
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
renderLanes = nextRenderLanes;
currentlyRenderingFiber$1 = workInProgress;
...
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
...
// f App()를 실행한다.
var children = Component(props, secondArg);
...
return children;
}
function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) {
...
value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes);
// 여기로 빠져나온다.
...
if (
typeof value === 'object' && value !== null && typeof value.render === 'function' && value.$$typeof === undefined) {
...
} else {
// 함수 컴포넌트로 결정된다!
workInProgress.tag = FunctionComponent; // 0
...
reconcileChildren(null, workInProgress, value, renderLanes);
}
}
renderWithHooks
로 자식 요소들을 React Element로 변환하고, 이 React Element로 어떤 컴포넌트인지 결정한다. 나머지 reconcileChildren() → reconcileChildFibers() → reconcileSingleElement() … 과정은 위와 동일하다.
function beginWork(current, workInProgress, renderLanes) {
...
if (current !== null) {
...
} else {
didReceiveUpdate = false;
...
}
workInProgress.lanes = NoLanes; // 0
switch (workInProgress.tag) {
case HostComponent:
{
return updateHostComponent(current, workInProgress, renderLanes);
}
...
}
}
div
와 같이 실제 DOM 요소에 대한 FiberNode는 tag가 HostComponent이다.
function updateHostComponent(current, workInProgress, renderLanes) {
...
var type = workInProgress.type; // div
// { children: Array(2) }
// { 0: $$typeof: Symbol(react.element), props: {children: 'Hello'}, type: "p", ... }
// { 1: $$typeof: Symbol(react.element), props: {}, type: ƒ Component(), ... }
var nextProps = workInProgress.pendingProps;
var prevProps = current !== null ? current.memoizedProps : null; // null
var nextChildren = nextProps.children; // Array(2)
...
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
// current = null, workInProgress = App에 대한 FiberNode
// nextChildren: renderWithHooks에서 렌더링한 App의 React Element
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
...
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
...
}
}
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
...
if (typeof newChild === 'object' && newChild !== null) {
...
if (isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
...
}
...
}
function App() {
return (
<div>
<p>App</p>
<Component />
</div>
);
}
다른 FiberNode와는 다르게 div
는 자식이 배열이므로 reconcileChildrenArray()
로 진입한다.
newChildren을 순회하면서 Child에 대한 FiberNode를 모두 생성한다.
newIdx = 0이면 previousNewFiber가 null이므로 resultingFirstChild = _newFiber;
newIdx = 1이면 previousNewFiber가 null이 아니므로 previousNewFiber = _newFiber
를 한다.
이런 식으로 같은 계층인 요소들은 sibling으로 연결된다.
다 순회하면 첫 번째로 생성한 Child를 return 한다.
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
...
var resultingFirstChild = null;
var previousNewFiber = null;
var oldFiber = currentFirstChild;
var lastPlacedIndex = 0;
var newIdx = 0;
var nextOldFiber = null;
...
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
// 여기로 빠져나옴
if (_newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = _newFiber;
} else {
previousNewFiber.sibling = _newFiber;
}
previousNewFiber = _newFiber;
}
if (getIsHydrating()) {
var _numberOfForks = newIdx;
pushTreeFork(returnFiber, _numberOfForks);
}
return resultingFirstChild;
} // Add all children to a key map for quick lookups.
...
}
newIdx = 0;
newIdx = 1;
function beginWork(current, workInProgress, renderLanes) {
...
if (current !== null) {
...
} else {
didReceiveUpdate = false;
...
}
workInProgress.lanes = NoLanes; // 0
switch (workInProgress.tag) {
case HostComponent:
{
return updateHostComponent(current, workInProgress, renderLanes);
}
...
}
}
function updateHostComponent(current, workInProgress, renderLanes) {
...
var type = workInProgress.type; // p
// { children: "Hello" }
var nextProps = workInProgress.pendingProps;
var prevProps = current !== null ? current.memoizedProps : null; // null
var nextChildren = nextProps.children; // Hello
var isDirectTextChild = shouldSetTextContent(type, nextProps); // true
if (isDirectTextChild) {
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
...
}
...
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
p는 자식 노드가 <p>, <div>와 같은 노드가 아닌 텍스트 노드이다. <p>Hello</p>
그래서 더 깊이 진입하지 않고, 그냥 빠져나온다
// current = null, workInProgress = p에 대한 FiberNode
// nextChildren: null
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
...
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
...
}
}
beginWork
에서 자식 FiberNode가 생성되지 않으므로 next가 null이 되고, completeUnitOfWork
을 호출한다.
function performUnitOfWork(unitOfWork) {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
var current = unitOfWork.alternate;
setCurrentFiber(unitOfWork);
var next;
if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentFiber();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 여기로 진입
completeUnitOfWork(unitOfWork);
} else {
...
}
ReactCurrentOwner$2.current = null;
}
function completeUnitOfWork(unitOfWork) {
var completedWork = unitOfWork;
do {
var current = completedWork.alternate; // null
var returnFiber = completedWork.return; // div FiberNode
// 1048576: 100000000000000000000 (2진수)
// 32768: 1000000000000000 (2진수)
// 결과 -> 0
if ((completedWork.flags & Incomplete) === NoFlags) {
setCurrentFiber(completedWork);
var next = void 0;
if ( (completedWork.mode & ProfileMode) === NoMode) {
...
} else {
startProfilerTimer(completedWork);
// 여기로 진입
next = completeWork(current, completedWork, subtreeRenderLanes); // Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
}
...
}
function completeWork(current, workInProgress, renderLanes) {
var newProps = workInProgress.pendingProps; // "Hello"
popTreeContext(workInProgress);
switch (workInProgress.tag) {
...
case HostComponent:
{
...
var rootContainerInstance = getRootHostContainer(); // div#root
var type = workInProgress.type; // p
if (current !== null && workInProgress.stateNode != null) {
...
} else {
if (!newProps) {
...
}
...
var _wasHydrated = popHydrationState(workInProgress); // false
if (_wasHydrated) {
...
} else {
// 여기로 진입
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
...
}
...
}
...
}
}
}
function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
var parentNamespace; // undefined
...
var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
...
return domElement;
}
function createElement(type, props, rootContainerElement, parentNamespace) {
var isCustomComponentTag;
var ownerDocument = getOwnerDocumentFromRootContainer(rootContainerElement);
var domElement;
var namespaceURI = parentNamespace;
...
if (namespaceURI === HTML_NAMESPACE) {
...
if (type === 'script') {
...
} else if (typeof props.is === 'string') {
...
} else {
domElement = ownerDocument.createElement(type);
...
}
} else {
...
}
...
return domElement;
}
function getOwnerDocumentFromRootContainer(rootContainerElement) {
// 1 === 9 -> false
return rootContainerElement.nodeType === DOCUMENT_NODE
? rootContainerElement
: rootContainerElement.ownerDocument;
}
function completeWork(current, workInProgress, renderLanes) {
var newProps = workInProgress.pendingProps; // "Hello"
popTreeContext(workInProgress);
switch (workInProgress.tag) {
...
case HostComponent:
{
...
var rootContainerInstance = getRootHostContainer(); // div#root
var type = workInProgress.type; // p
if (current !== null && workInProgress.stateNode != null) {
...
} else {
if (!newProps) {
...
}
...
var _wasHydrated = popHydrationState(workInProgress); // false
if (_wasHydrated) {
...
} else {
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
// 여기로 빠져나옴
appendAllChildren(instance, workInProgress, false, false);
// FiberNode의 stateNode에는 document.createElement로 만든 실제 요소가 할당된다.
workInProgress.stateNode = instance;
...
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
markUpdate(workInProgress);
}
}
...
}
...
}
}
}
// p는 자식이 없다.
appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {
var node = workInProgress.child; // null
// node === null
while (node !== null) {
...
}
};
function finalizeInitialChildren(domElement, type, props, rootContainerInstance, hostContext) {
setInitialProperties(domElement, type, props, rootContainerInstance);
switch (type) {
...
default:
return false;
}
}
function setInitialProperties(domElement, tag, rawProps, rootContainerElement) {
...
var props; // undefined
switch (tag) { // tag = "p"
...
default:
props = rawProps;
}
...
setInitialDOMProperties(tag, domElement, rootContainerElement, props, isCustomComponentTag);
...
}
function setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) {
for (var propKey in nextProps) { // propKey = "children"
...
var nextProp = nextProps[propKey]; // "Hello"
if (propKey === STYLE) {
...
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
...
} else if (propKey === CHILDREN) {
if (typeof nextProp === 'string') {
var canSetTextContent = tag !== 'textarea' || nextProp !== ''; // true
if (canSetTextContent) {
setTextContent(domElement, nextProp);
}
} else if (typeof nextProp === 'number') {
setTextContent(domElement, '' + nextProp);
}
} else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING) ; else if (propKey === AUTOFOCUS) ; else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
if ( typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}
DOM API를 이용하여 실제 DOM에 텍스트를 추가한다.
var setTextContent = function (node, text) {
if (text) {
var firstChild = node.firstChild; // null
...
}
node.textContent = text; // 실제 DOM 요소에 text(Hello) 설정
};
p에 대한 FiberNode는 형제 노드인 Component FiberNode가 있으므로 workInProgress로 이 Component FiberNode를 설정한다.
function completeUnitOfWork(unitOfWork) {
...
var siblingFiber = completedWork.sibling; // f Component() FiberNode
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
...
} while (completedWork !== null); // We've reached the root.
...
}
이런식으로 DFS로 순회하면서 beginWork
로 FiberNode를 생성하고, completeWork
으로 HostComponent FiberNode에 대하여 실제 DOM 요소를 생성하면서 렌더 단계가 진행된다.
(tag가 HostComponent가 아닌 HostRoot, FunctionComponent는 completeWork()
이 끝까지 실행 안 되고 중간에 return 된다.)
모든 completeWork()
가 끝나게 되면, workInProgress
는 트리의 루트인 HostRoot로 이동된다.
렌더 단계가 마무리되면, 커밋 단계로 진입한다.
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted; // 5
}
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus; // 5
function performConcurrentWorkOnRoot(root, didTimeout) {
...
var shouldTimeSlice = !includesBlockingLane(root, lanes) && !includesExpiredLane(root, lanes) && ( !didTimeout);
var exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
// 여기로 빠져나옴
if (exitStatus !== RootInProgress) {
if (exitStatus === RootDidNotComplete) {
...
} else {
root.finishedWork = finishedWork; // HostRoot
root.finishedLanes = lanes; // 16
finishConcurrentRender(root, exitStatus, lanes);
}
}
ensureRootIsScheduled(root, now());
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}
commitRoot를 호출하여 커밋 단계가 진행된다.
function finishConcurrentRender(root, exitStatus, lanes) {
...
switch (exitStatus) {
case RootCompleted:
{
commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions);
break;
}
...
}
}
function commitRoot(root, recoverableErrors, transitions) {
...
try {
...
commitRootImpl(root, recoverableErrors, transitions, previousUpdateLanePriority);
} finally {
...
}
return null;
}
function commitRootImpl(root, recoverableErrors, transitions, renderPriorityLevel) {
...
var finishedWork = root.finishedWork;
var lanes = root.finishedLanes;
...
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
root.callbackPriority = NoLane;
...
if (subtreeHasEffects || rootHasEffect) {
...
commitMutationEffects(root, finishedWork, lanes);
...
} else {
...
}
...
}
function commitMutationEffects(root, finishedWork, committedLanes) {
...
commitMutationEffectsOnFiber(finishedWork, root);
...
}
function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
var current = finishedWork.alternate; // HostRoot
var flags = finishedWork.flags; // 1024
switch (finishedWork.tag) {
...
case HostRoot:
{
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
{
if (current !== null) {
var prevRootState = current.memoizedState;
if (prevRootState.isDehydrated) {
try {
commitHydratedContainer(root.containerInfo);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
}
}
return;
}
...
}
}
function recursivelyTraverseMutationEffects(root, parentFiber, lanes) {
...
if (parentFiber.subtreeFlags & MutationMask) {
var child = parentFiber.child; // StrictMode FiberNode
while (child !== null) {
...
commitMutationEffectsOnFiber(child, root);
...
}
}
setCurrentFiber(prevDebugFiber);
}
function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
var current = finishedWork.alternate; // null
var flags = finishedWork.flags; // 2
switch (finishedWork.tag) {
...
default:
{
// 여기로 진입
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
return;
}
}
}
// 그냥 빠져나옴
function recursivelyTraverseMutationEffects(root, parentFiber, lanes) {
...
}
// finishWork = StrictMode
function commitReconciliationEffects(finishedWork) {
var flags = finishedWork.flags; // 2
// StrictMode의 flags는 Placement이다.
if (flags & Placement) {
try {
commitPlacement(finishedWork);
} catch (error) {
...
}
}
...
}
function commitPlacement(finishedWork) {
var parentFiber = getHostParentFiber(finishedWork); // HostRoot
switch (parentFiber.tag) {
...
case HostRoot:
case HostPortal:
{
var _parent = parentFiber.stateNode.containerInfo; // div#root
var _before = getHostSibling(finishedWork); // null
insertOrAppendPlacementNodeIntoContainer(finishedWork, _before, _parent);
break;
}
...
}
}
function insertOrAppendPlacementNodeIntoContainer(node, before, parent) {
var tag = node.tag; // 8
var isHost = tag === HostComponent || tag === HostText; // false
if (isHost) {
var stateNode = node.stateNode;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal) ; else {
var child = node.child; // App FiberNode
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
var sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
tag가 HostComponent일 때까지 재귀 호출한다.
function insertOrAppendPlacementNodeIntoContainer(node, before, parent) {
var tag = node.tag; // 8
var isHost = tag === HostComponent || tag === HostText; // false
if (isHost) {
var stateNode = node.stateNode;
if (before) {
...
} else {
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal) ; else {
...
}
}
child를 따라 내려가다가 Node의 Tag가 HostComponent면 appendChildToContainer
를 호출하여 containerInfo
의 자식으로 Node를 추가한다.
function appendChildToContainer(container, child) {
var parentNode;
if (container.nodeType === COMMENT_NODE) {
...
} else {
parentNode = container; // div#root
parentNode.appendChild(child); // div#root.appendChild(div)
}
...
}
Node의 flags 를 Placement에서 0으로 초기화한다. finishedWork.flags &= ~Placement;
이제 콜스택에 남아있는 함수를 다 빠져나오면서 커밋 단계가 마무리된다.