재조정 작업의 목적은 VDOM 트리에서 변경이 발생한 부분을 효과적으로 비교하여 새로운 트리를 만들어내는 것입니다.
휴리스틱 알고리즘
1. 서로 다른 type의 두 엘리먼트는 서로 다른 트리를 만든다.
2. 개발자는 key prop을 통해 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할 지 표시해 줄 수 있다.
function ChildReconciler(shouldTrackSideEffects) {
// 삭제
function deleteChild(){
// shouldTrackSideEffects === false => mount
// shouldTrackSideEffects === true => update
if(!shouldTrackSideEffects) {
// 마운트
// mount이면 그대로 종료(mount할 때는 위치 이동이나 삭제할 필요가 없기 때문)
}
/*...*/
}
// 추가, 위치 이동
function placeChild(){
if(!shouldTrackSideEffects) {
// 마운트
}
/*...*/
}
/*...*/
// 재조정 작업 진행
function reconcileChildFibers(...) {...}
return reconcileChildFibers;
}
// flag를 통해 마운트, 업데이트용 함수를 미리 나눠둠.
const reconcileChildFibers = ChildReconciler(true);
const mountChildFibers = ChildReconciler(false);
function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any, // 업데이트하거나 새로 렌더링해야할 컴포넌트들
// any type인 이유
// -> element 종류가 엄청나게 다양(단일 컴포넌트 / 다중 컴포넌트 / Fragment / Text / null / undefined)
renderExpirationTime: ExpirationTime
) {
if (current === null) {
// current가 null이면, mount 해야한다는 얘기
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime
)
} else {
// 존재하면 update 해야한다는 소리
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime
)
}
}
function reconcileChildFibers(
returnFiber, // workInProgress(새로운 트리)
currentFirstChild, // current.child(하나만 연결되어 있음)
newChild, // nextChildren
expirationTime
) {
// key가 없는 Fragment
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null
// newChild가 key가 없는 최상위 레벨의 Fragment
if (isUnkeyedTopLevelFragment) {
// 최상위이기 때문에 바로 props.childern을 통해 자식으로 newChild를 옮겨줌
newChild = newChild.props.children
}
// isObject => 객체이면서 값이 존재하면 react element라는 애기임.
const isObject = typeof newChild === 'object' && newChild !== null
// React element
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
expirationTime
)
)
/*...*/
}
}
// Text
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
expirationTime
)
)
}
// 복수개의 자식을 담고 있는 배열
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
expirationTime
)
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild)
}
// placeSingleChild()
// => VDOM 입장에서는 트리에 변경점을 불러오는 모든 행위가 side-effect
function placeSingleChild(newFiber: Fiber): Fiber {
// 업데이트인데 상대 tree에 존재하지 않는 경우 -> side Effect가 발생했다고 할 수 있음.
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.effectTag = Placement
}
return newFiber
}
function reconcileSingleElement(
returnFiber: Fiber, // workInProgress(새로운 트리)의 부모 fiber
currentFirstChild: Fiber | null, // current.child(하나만 연결되어 있음)
element: ReactElement, // nextChildren
expirationTime: ExpirationTime
): Fiber {
const key = element.key
let child = currentFirstChild
// newChild와 매칭되는 current를 찾는다.
// child가 null 이면 종료
while (child !== null) {
// current와 element의 key가 같으면
if (child.key === key) {
// key 뿐만 아니라 type까지 같으면
if (
child.tag === Fragment
? element.type === REACT_FRAGMENT_TYPE
: child.elementType === element.type
) {
// key & type이 둘 다 같으면
// 우리가 원하는 한개의 current를 찾은것이므로, 나머지 sibling을 삭제해야함
deleteRemainingChildren(returnFiber, child.sibling)
// current 재사용
const existing = useFiber(
child,
element.type === REACT_FRAGMENT_TYPE
? element.props.children
: element.props,
expirationTime
)
// current 재사용했지만 return은 current를 가르키므로
// workInProgress를 가르키게 바뀌어야 함.
existing.return = returnFiber
return existing
} else {
// key는 같지만 type이 다른 경우
deleteRemainingChildren(returnFiber, child)
break
}
} else {
// current에는 존재하지만, element에는 존재하지 않는다면
// 해당 current는 삭제되어야한다.
deleteChild(returnFiber, child)
}
// 단일 element이지만, current에는 단일이 아닐수도 있으므로
// current에 존재하는 child를 모두 순회해야함.
child = child.sibling
}
// 새 fiber 생성..
}
// 함수명을 그대로 해석하면 남아있는 자식을 삭제 -> current 재사용할 필요없는 fiber 삭제
function deleteRemainingChildren(returnFiber, currentFirstChild) {
if (!shouldTrackSideEffects) {
// 컴포넌트 마운트라면 current가 없으므로 애초에 지울 것도 없다.
return null
}
let childToDelete = currentFirstChild
while (childToDelete !== null) {
// 순차적으로 current children 삭제
deleteChild(returnFiber, childToDelete)
childToDelete = childToDelete.sibling
}
return null
}
function deleteChild(returnFiber, childToDelete) {
if (!shouldTrackSideEffects) {
// deleteRemainingChildren()와 동일
return
}
// 너무 어려워서.. 그냥 삭제하는 과정인데
// effect에는 삭제했음을 명시하는 flag 넣기
// commit 과정에서 삭제됐음을 확인할 수 있음
const last = returnFiber.lastEffect
if (last !== null) {
last.nextEffect = childToDelete
returnFiber.lastEffect = childToDelete
} else {
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete
}
childToDelete.nextEffect = null
childToDelete.effectTag = Deletion
}
function useFiber(
fiber: Fiber,
pendingProps: mixed,
expirationTime: ExpirationTime
): Fiber {
// props를 제외한 나머지를 current 속성 복사하기!
const clone = createWorkInProgress(fiber, pendingProps, expirationTime)
clone.index = 0
clone.sibling = null
return clone
}
current의 연결 리스트 currentFirstChild, 컴포넌트가 반환한 React element를 담고 있는 배열 newChild.
리액트는 O(n)의 시간복잡도로 판별을 완수합니다. 연결리스트든 배열이든 타겟을 가리키는 커서는 해당 위치를 딱 한 번만 지나가게 된다는 뜻이며 이때 사용되는 알고리즘의 핵심은 key와 type 그리고 원소의 위치입니다.
function reconcileChildrenArray(
returnFiber: Fiber, // workinprogresstree 부모 fiber
currentFirstChild: Fiber | null, // current child linkedList
newChildren: Array<*>, // 바꿀 child array
expirationTime: ExpirationTime
// return으로 fiber or null
): Fiber | null {
let resultingFirstChild: Fiber | null = null
let previousNewFiber: Fiber | null = null
let oldFiber = currentFirstChild // current 커서
let lastPlacedIndex = 0
let newIdx = 0 // new child 커서
let nextOldFiber = null
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// newIdx가 더 작다는 얘기는 기존 current에서 component가 추가되었다는 얘기
// 그럼 다음 oldfiber를 기존 oldfiber로 설정하고
// 기존 oldfiber는 삭제
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber
oldFiber = null
} else {
nextOldFiber = oldFiber.sibling
}
// key가 다르다면 null을 같다면 type을 비교하여 생성 혹은 current를 재사용 또는 새로 생성하여 fiber를 반환
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
expirationTime
)
// newFiber가 null인 경우
// newChild.key !== oldFiber.key
// newChild가 fiber로 확장할 수 없는 경우
// -> 확장할 수 없는 경우 newChild 자체가 falsy 값이면
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber
}
break
}
// newfiber가 존재하며
// shouldTrackSideEffects true => update
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// oldFiber가 존재하는데, newfiber 반대편 oldfiber는 없는 경우
// current에는 존재하지만 workinprogress에서 연결된 fiber가 없는 경우
// -> createFiberFromElement인 경우
// 기존 current에 있는 fiber 삭제해야함.
deleteChild(returnFiber, oldFiber)
}
}
// fiber에 위치 index를 새기며 이동, 추가의 경우 placement effect tag도 달아줌.
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx)
if (previousNewFiber === null) {
resultingFirstChild = newFiber
} else {
previousNewFiber.sibling = newFiber
}
previousNewFiber = newFiber
oldFiber = nextOldFiber
}
/*...*/
// current 리스트가 더 긴 경우
// 즉, oldfiber는 존재하지만, newChildren.length == newIdx
if (newIdx === newChildren.length) {
// 그럼 그냥 current에 있는 거 삭제하면 됨
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// newChildren 배열이 더 긴 경우
if (oldFiber === null) {
// 걍 resultingFirstChild에 추가하면 됨.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
expirationTime,
);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
/*...*/
}
// current가 이동한 경우
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
expirationTime,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// fiber가 존재하고 update + current fiber가 존재하는경유
// 재활용된 fiber의 경우 맵에서 제거해준다
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
function updateSlot(
returnFiber: Fiber, // parent
oldFiber: Fiber | null, // current.child
newChild: any, // update
expirationTime: ExpirationTime
): Fiber | null {
const key = oldFiber !== null ? oldFiber.key : null
// Text
if (typeof newChild === 'string' || typeof newChild === 'number') {
// text는 key값이 존재하면 안되므로, return null처리함
if (key !== null) {
return null
}
return updateTextNode(returnFiber, oldFiber, '' + newChild, expirationTime)
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
if (newChild.type === REACT_FRAGMENT_TYPE) {
// type === fragment면서, key가 동일하면
// 재사용
return updateFragment(
returnFiber,
oldFiber,
newChild.props.children,
expirationTime,
key
)
}
return updateElement(returnFiber, oldFiber, newChild, expirationTime)
} else {
// newChild.key !== oldFiber.key
return null
}
}
/*...*/
}
if (isArray(newChild)) {
if (key !== null) {
return null
}
return updateFragment(
returnFiber,
oldFiber,
newChild,
expirationTime,
null
)
}
}
// newChild가 fiber로 확장할 수 없는 경우
return null
}
function updateElement(
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTime
): Fiber {
// type 같은 지 확인하고, current가 존재하는 지 확인
// updateslot에서 확인했는데 한번 더 확인하는 이유
// react가 동기적으로 변하기때문에
// 이중확인을 통해 안전성 확보
if (current !== null && current.elementType === element.type) {
// useFiber(위에서 current fiber를 복사할 때 사용하는 function)
const existing = useFiber(current, element.props, expirationTime)
// workinprogress tree fiber로 return 연결
existing.return = returnFiber
return existing
//
} else {
// 두 type이 다르거나, current가 존재하지 않는 경우
// 이떄는 새로 fiber를 생성한다. element를 통해
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime
)
created.return = returnFiber
return created
}
}
key!의 중요성
- key와 type이 같으면 props만 변경된다.
- key를 설정하지 않으면 key는 null이다.
1이 뒤로 이동했다고 판단할 경우 2, 3을 그대로 두고 1만 조작하던지
1은 가만히 있었는데 2, 3이 앞으로 이동했다고 판단하여 2, 3을 조작하던지
이동의 정의를 인접한 원소를 기준으로 뒤로 이동한 경우 1, 앞으로 이동한 경우를 나타낸 것이 2입니다. 그리고 전자로 로직을 작성한 함수가 placeChild()입니다.
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number
): number {
newFiber.index = newIndex
if (!shouldTrackSideEffects) {
// 마운트의 경우 index 설정만 필요함.
return lastPlacedIndex
}
// update
// newFiber workinprogress tree에 존재하는 fiber
const current = newFiber.alternate
if (current !== null) {
// current index = oldindex
const oldIndex = current.index
if (oldIndex < lastPlacedIndex) {
// 기존보다 뒤로 이동함 !
// 이동
newFiber.effectTag = Placement
return lastPlacedIndex
} else {
// 기존보다 앞으로 이동하거나 동일함
// 동일하면 oldIndex가 맞고
// 앞으로 이동하면 기존에 존재했던 fiber들이 삭제되었음을 의미함.
// placement말고 Deletion라는 태그를 추후 달아줌
// 이동하지 않음
return oldIndex
}
} else {
// 추가
newFiber.effectTag = Placement
return lastPlacedIndex
}
}