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)
}
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;
}
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
}
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
}
}
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
}
}
}
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)
}
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
}
// 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에 저장한다.
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)
}
}
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
}
}
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
}
}
}