Fiber는 React 코어 알고리즘을 리팩토링 한 것으로, Meta의 facebook 팀이 2년간 노력을 쏟았습니다.
Fiber 아키텍쳐는 React 16.0 버전에 소개되었으며, 그 디자인 철학의 일부는 배우면 좋다고 생각합니다.
브라우저를 위해서는, 페이지는 프레임 마다 만들어지고, 그 프레임 렌더링 빈도는 기기의 새로고침 빈도와 같습니다. 일반적으로, 화면은 초당 60번 새로고침되며, 60fps를 넘어야 부드럽게 페이지가 렌더링됩니다. 안 그러면 페이지는 동작하지 않을 겁니다.
완전한 프레임에서 일어나는 일들:
1. Input events: click, touch, wheel, keypress...
첫째, input events는 가능한 빠르게 유저에게 피드백을 줍니다.
2. Timers: setTimeout, setInterval...
둘째, 타이머를 확인하여 스케쥴된 시간에 닿았는 지 확인하고 동시에 해당하는 콜백을 실행합니다.
3. Begin frame: scroll, window resize, media query page...
셋째, window.resize
, scroll, media query 변화 등이 포함한 시작 프레임(프레임마다 이벤트들)을 확인합니다.
4. requestAnimationFrame: requestAnimationFrame, frame callback
넷째, requestAnimationFrame을 실행하고 페인트하기 전 함수가 실행되었는 지 확인합니다.
5. Layout: recalculate style -> update layout
다섯째, 레이아웃 계산과 업데이트, 페이지에서 요소 스타일 지정과 표시 방법을 포함한 레이아웃 작업을 진행합니다.
6. Paint: compositing update -> paint invaildation -> record
여섯째, 페인트 작업을 진행합니다. 트리에 있는 각 노드의 크기와 위치를 가져오고, 브라우저에 의해 각 요소 내용이 채워집니다.
7. Idle period: setTimeout, setInterval
이제 브라우저는 유휴기에 들어갔습니다. requestIdleCallback에 등록된 작업을 실행합니다. 해당 함수는 React Fiber의 기초를 다집니다.
자바스크립트 엔진과 페이지 렌더링 기술은 서로 배타적인 방식으로 같은 렌더링 스레드에서 작동합니다. 만약 특정 단계에서 실행되는 작업의 경우 시간이 많이 걸릴 수 있으며 페이지 렌더링이 중단되어 페이지 끊김 현상이 발생한다.
Fiber 구조를 설명하기 전에, React는 재귀적으로 동기화하여 바뀌거나 업데이트된 노드를 찾기 위해 가상 돔 트리를 돔과 비교합니다. 조정이라 불리는 프로세스에서 React는 계속 브라우저 리소스를 사용해서, 브라우저가 사용자가 이벤트를 발동시켜도 반응하지 못할 수 있습니다.
a1
/ \
b1 b2
/ \ | \
c1 c2 c3 c4
순회 순서: a1 -> b1 -> c1 -> c2 -> b2 -> c3 -> c4
위 일곱 노드에서 b1와 b2는 a1의 자식 노드이고, c1과 c2는 b1의 자식 노드 이며, c3와 c4는 b2의 자식 노드입니다. 전통적으로 깊이 우선 탐색은 노드를 찾는 데 사용됩니다.
const root = {
key: 'A1',
children: [{
key: 'B1',
children: [{
key: 'C1',
children: []
}, {
key: 'C2',
children: []
}]
}, {
key: 'B2',
children: [{
key: 'C3',
children: []
}, {
key: 'C4',
children: []
}]
}]
}
const walk = dom => {
console.log(dom.key)
dom.children.forEach(child => walk(child))
}
walk(root)
/*
출력 결과
--------
A1
B1
C1
C2
B2
C3
C4
/*
깊이 우선 탐색은 재귀 호출이므로, 이는 중단될 수 없는 더 깊은 실행 스택으로 이어집니다. 그렇지 않으면 회복되지 못합니다. 재귀가 매우 깊게 간다면, 브라우저는 중간 중간 끊기게 됩니다. 재귀가 100ms동안 진행되면, 브라우저는 그 시간 동안 유저 활동에 대한 반응을 할 수 없습니다. 그러므로 코드 실행시간이 길어질 수록 브라우저가 중간 중간 멈추는 것이 더 심해질 것입니다. 중단 처리 불가능에 편리한 방법은 이상적이지 않으며, 실행 스택이 깊어집니다.
해당 문제를 해결하기 위해 Fiber는 렌더링과 업데이트 프로세스를 작은 작업들로 나누어 집니다. 중간 멈춤을 줄이고 페이지 상호작용을 개선하기 위한 작업을 실행하는 데에 시간대를 지정하기 위한 적절한 스케쥴 동작원리에 따라 작업들을 실행합니다. Fiber 구조는 조정 프로세스에서 중단 작업을 할 수 있습니다.
React 16.0 버전은 Fiber를 사용하지만 Vue는 그렇지 않습니다. 이유는 둘이 최적화하는 방식이 다르기 때문입니다.
setState
가 호출되는 곳 어디든 상관 없이 루트 노드에서 데이터를 업데이트 합니다. 이 결과로 Fiber를 작은 작업으로 나눠야 하는 거대한 업데이트 작업이 필요합니다. 그 자체로 중단 작업과 재개가 사용되고, 메인 스레드는 우선 순위가 높은 작업부터 실행합니다.이제 Fiber가 어떻게 동작하는 하는 지 알아봅시다.
실행 단위 혹은 데이터 구조로 여길 수 있습니다.
실행 단위로써, 매 시간 실행되며, React는 시간이 얼마나 남았는 지 확인합니다. 시간이 충분하지 않다면 React는 조작을 포기합니다.
React Fiber와 브라우저 사이의 키 상호작용을 보여줍니다:
-----------------------------------^ 예
V |
React -> 요청 스케쥴링 -> 실행 작업 -> 다음 작업 단위 존재? 예 -> 작업 단위 실행 -> 실행후 남은 시간이 있는가? -> ...
| ^ 아뇨 아뇨
V | | |
브라우저 -> 이벤트 처리------------------------>(브라우저 작업) -> .....
JS 실행
레이아웃/페인팅
유휴기
이 프로세스에서 큰 작업을 더 작은 작업 단위로 나눕니다. 작은 작업은 중단 없이 실행되야 하지만 브라우저로 제어가 넘어간 인접한 작은 작업들이 중단 될 수 있다. 그러므로 유저는 원래 완료되야 하는 원래 큰 작업을 기다릴 필요 없이 즉시 응답을 얻을 수 있습니다.
Fiber는 데이터 구조로 여길 수 있고, React FIber는 Linked list에서 구현됩니다. 각 가상 돔은 한 Fiber로 이해할 수 있습니다. 각 노드는 한 Fiber이며, 자식(자식 노드), 형제(형제 노드), 그리고 반환(부모 노드)과 같은 속성을 가집니다. React Fiber 동작 원리 구현은 따르는 데이터 구조에 의존합니다. 어떻게 Linked list를 기반으로 Fiber가 구현되었는 지 알아봅시다.
정보: Fiber는 React를 리팩토링하는 핵심 알고리즘이며, Fiber는 데이터 구조에 있는 각 노드를 참조합니다. 아래를 봤을 때 a1과 b1는 Fiber입니다.
Fiber는 requestAnimationFrame(브라우저에서 애니메이션을 그리는 api 제공)을 사용합니다. 다음 프레임을 리페인팅을 하기전 애니메이션을 업데이트할 지정된 콜백 함수를 호출하기 위한 브라우저가 필요합니다.
예를 들어 브라우저에서 100px가 될 때까지 프레임마다 1px씩 div 요소의 너비를 늘리고 싶다면 requestAnimationFrame은 사용될 수 있습니다.
<body>
<div id="div" class="progress-bar "></div>
<button id="start">Start Animation</button>
</body>
<script>
let btn = document.getElementById('start')
let div = document.getElementById('div')
let start = 0
let allInterval = []
const progress = () => {
div.style.width = div.offsetWidth + 1 + 'px'
div.innerHTML = (div.offsetWidth) + '%'
if (div.offsetWidth < 100) {
let current = Date.now()
allInterval.push(current - start)
start = current
requestAnimationFrame(progress)
} else {
console.log(allInterval) // Print all the time intervals of the requestAnimationFrame
}
}
btn.addEventListener('click', () => {
div.style.width = 0
let currrent = Date.now()
start = currrent
requestAnimationFrame(progress)
console.log(allInterval)
})
</script>
이러면 같은 간격으로 프레임이 생성됩니다.
requestIdleCallback은 React Fiber에서 구현된 근본적인 api 입니다. 부드럽게 유저에게 빠른 응답을 하기 위해, 상호 작용을 흐르게 하고, requestIdleCallback을 사용하면 개발자는 애니메이션 재생 및 입력 응답과 같은 중요한 이벤트 중에 대기 시간을 유발하지 않고 기본 이벤트를 실행하는 동안 백그라운드 및 우선 순위가 낮은 작업을 실행할 수 있습니다. 정규 프레임 작업이 16ms 이내에 완료되면, requestIdelCallback에 등록된 작업을 실행하는 필요한 시간이 생깁니다.
requestIdleCallback 메소드를 사용해서 개발자들은 해당 작업을 등록하고 작업이 낮은 우선순위에 있으므로 남는 시간이 각 프레임마다 있다면 실행할 수 있다고 브라우저에게 전합니다. 추가적으로 개발자들은 타임아웃 파라미터를 전달합니다. 브라우저는 window.requestIdleCallback(callback, {timeout: 1000})
타임아웃이 도달했을 때 작업을 수행해야 합니다.
브라우저가 이러한 방향으로 작업을 수행한 이후에, 실행에 필요한 시간이나 작업이 없을 경우, React는 제어를 되돌리고 다른 시간 대에 적용하기 위해 requestIdelCallback 함수를 다시 실행합니다.
기본적으로 window.requestIdleCallback(callback)
의 콜백은 파라미터 마감 시간을 받아야 하며, 마감 시간은 아래를 포함해야 한다.
timeRemain
: 현재 프레임에서 사용 가능한 시간이 얼마나 남았는가didTimeout
: 해당 작업이 타임 아웃이 되었는가requestIdleCallback 메소드는 매우 중요하므로 이해를 돕기 위해 다음 두 가지 예를 제공합니다. 각 예제에서는 여러 작업이 다른 시간대에 실행됩니다. 어떻게 브라우저는 이 작업들을 위한 시간을 할당하는 지 알아 봅시다.
작업1, 작업2, 작업3을 직렬로 실행하면 각 작업은 16ms 이내에 완료될 것입니다:
let taskQueue = [
() => {
console.log('task1 start')
console.log('task1 end')
},
() => {
console.log('task2 start')
console.log('task2 end')
},
() => {
console.log('task3 start')
console.log('task3 end')
}
]
const performUnitWork = () => {
// 실행을 위해 첫 번째 대기열에서 첫 번째 작업을 검색합니다.
taskQueue.shift()()
}
const workloop = (deadline) => {
console.log(`The remaining time of this frame: ${deadline.timeRemaining()}`)
// 남은 시간이 0보다 크거나 정의된 제한 시간에 도달하면 작업이 실행됩니다.
// 남은 시간이 없으면 작업을 포기하고 제어권을 브라우저로 반환합니다.
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
performUnitWork()
}
// 대기 중인 작업이 있는 경우 다음 시간 분할을 적용하기 위해 requestIdleCallback이 호출됩니다.
if (taskQueue.length > 0) {
window.requestIdleCallback(workloop, { timeout: 1000 })
}
}
requestIdleCallback(workloop, { timeout: 1000 })
앞선 예제는 taskQueue
라 명명된 작업 큐와 workloop 함수를 정의합니다. taskQueue
의 작업을 실행하기 위한 window.requestIdleCallback(workloop, { timeout: 1000 })
를 사용합니다. 각 작업마다, console.log를 처리하기 위한 상당히 제한된 시간을 가지므로, 브라우저는 이 프레임에서 15.52ms가 남습니다. 세 작업을 완료하기 위해 충분합니다.
수면 시간에 작업1, 작업2, 그리고 작업3이 추가되었다. 각 작업은 완료되는 데 16ms 이상의 시간이 걸린다.
const sleep = delay => {
for (let start = Date.now(); Date.now() - start <= delay;) {}
}
let taskQueue = [
() => {
console.log('task1 start')
sleep(20)
// 작업이 완료되는 데 한 프레임 이상 걸림, 브라우저에게 제어권을 넘겨야 함.
console.log('task1 end')
},
() => {
console.log('task2 start')
sleep(20)
// 작업이 완료되는 데 한 프레임 이상 걸림, 브라우저에게 제어권을 넘겨야 함.
console.log('task2 end')
},
() => {
console.log('task3 start')
sleep(20)
// 작업이 완료되는 데 한 프레임 이상 걸림, 브라우저에게 제어권을 넘겨야 함.
console.log('task3 end')
}
]
앞선 예제에 따르면, taskQueue
에 있는 각 작업은 16.6ms 이상 걸리도록 일부가 수정되었습니다. 출력된 결과를 보면 첫 프레임의 브라우저의 유휴기는 한 작업을 수행하는 데 충분한 시간인 14ms이다. 2, 3번째 프레임도 마찬가지이다. 그러므로 세 작업은 각각 세 프레임안에 완성된다.
브라우저 프레임 지속시간은 16ms 에 고정되어 있는 게 않고 동적으로 제어된다.
예) 셋째 프레임의 남은 시간은 49.95ms 이다.
하위 작업 소요 시간이 프레임의 남은 시간보다 길다면, 하위 작업은 완료될 때까지 고정된다.
코드에서 끝없는 반복이 있을 경우 브라우저는 충돌할 것이다.
프레임 남은 시간은 0보다 크거나 설정한 타임아웃에 도달하고, 그 시간에 보류 중인 작업이 있다면, 보류 중인 작업이 실행될 것이다. 시간이 없다면, 작업을 포기하고 브라우저에게 제어권이 넘어간다. 여러 작업 총 실행 시간은 유휴 시간 보다 작다면, 한 프레임 안에 모든 작업이 실행된다.
Fiber 구조는 linked list 트리입니다. 더 많은 정보를 알고 싶다면 https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiber.js 를 확인하세요. 후속 Fiber 순회 프로세스를 더 쉽게 이해하기 위한 구조를 살펴 봅시다.
각 후속 단위는 페이로드(데이터)와 nextUpdate
(다음 단위 포인터)을 포함시킨다.
구조:
class Update {
constructor(payload, nextUpdate) {
this.payload = payload // Payload data
this.nextUpdate = nextUpdate // The pointer to the next unit
}
}
다음으로, 시리즈에서 각 단위를 연결하기 위해 큐를 정의합니다.
두 포인터는 다음과 같다:
class UpdateQueue {
constructor() {
this.baseState = null // state
this.firstUpdate = null // The first update
this.lastUpdate = null // The last update
}
}
이제 노드 단위 삽입(enqueueUpdate)과 업데이트 대기열(forceUpdate)이라는 두 가지 메서드를 정의해 보겠습니다.
enqueueUpdate를 실행하기 전에 노드가 이미 존재하는지 확인합니다. 그렇지 않은 경우 firstUpdate 및 lastUpdate를 호출합니다.
forceUpdate는 이러한 Linked list를 탐색하고 페이로드를 기반으로 상태 값을 업데이트합니다.
class UpdateQueue {
//.....
enqueueUpdate(update) {
// 현재 linked list는 비어있음.
if (!this.firstUpdate) {
this.firstUpdate = this.lastUpdate = update
} else {
// Current linked list is not empty
this.lastUpdate.nextUpdate = update
this.lastUpdate = update
}
}
// 상태를 얻어서, linked list를 순회하고, 결과를 업데이트한다.
forceUpdate() {
let currentState = this.baseState || {}
let currentUpdate = this.firstUpdate
while (currentUpdate) {
// 함수인지 객체인지 결정합니다. 함수라면 실행하고, 아니라면 바로 반환하세요.
let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload
currentState = { ...currentState, ...nextState }
currentUpdate = currentUpdate.nextUpdate
}
// Clear the linked list after the update is complete
// linked list를 비웁니다.
this.firstUpdate = this.lastUpdate = null
this.baseState = currentState
return currentState
}
}
큐를 인스턴스화하고, 노드를 추가하고, 큐를 업데이트 합니다:
let queue = new UpdateQueue()
queue.enqueueUpdate(new Update({ name: 'www' }))
queue.enqueueUpdate(new Update({ age: 10 }))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.forceUpdate()
console.log(queue.baseState);
//결과:
// { name:'www',age:12 }
Fiber는 fiber로 작업을 나눕니다. 각각 fiber 트리에 노드로 존재합니다. 가상 돔 노드에 의해 나눠집니다. 가상 돔을 기반으로 Fiber 트리를 생성해야 합니다. 이 세션에서는 각 노드는 fiber로 언급됩니다. 뒤에 오는 예제는 일반적인 fiber 노드의 구조를 나타냅니다.
https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js
{
type: any, // 클래스 컴포넌트를 위해, 생성자를 가리킵니다. 돔 요소를 위해, HTMl 태그를 지정합니다.
key: null | string, // 고유 식별자
stateNode: any, // Fiber 노드와 관련된 요소, 돔 또는 다른 React 요소 유형의 클래스 인턴스에 대한 참조를 저장합니다.
child: Fiber | null, // 첫 자식 노드
sibling: Fiber | null, // 다음 자식 노드
return: Fiber | null, // 부모 노드
tag: WorkTag, // Fiber 행동의 유형을 정의합니다. 자세한 내용은 https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js를 참조하세요.
nextEffect: Fiber | null, // 다음 노드 포인터
updateQueue: mixed, // 업데이트 상태를 위한 큐, 콜백 함수, 그리고 돔 업데이트
memoizedState: any, // 출력 생성을 위한 fiber 상태
pendingProps: any, // React 요소의 새 데이터로 부터 업데이트 되고 돔 자식 컴포넌트와 돔 요소에 적용될 필요가 있는 props
memoizedProps: any, // 이전 렌더링 동안 출력을 생성하는 props
// ……
}
fiber 노드는 다음 속성을 표현합니다:
1. 타입 & 키
다른 속성은 memorizedState
(출력을 만드는 fiber의 상태), pendingProps
(변경될 props), memorizedProps
(지난 렌더링 동안 생성된 props), 그리고 pendingWorkPriority
(작업 우선 순위를 정의함)를 포함합니다.
루트 노드에서 가져온 렌더링과 스케쥴링 프로세스는 render와 commit, 두 단계로 나뉠 수 있습니다.
이 단계에서 노드 생성, 삭제, 그리고 속성 변경과 같은 모든 노드 변화가 식별됩니다. 이 변화들은 집합적으로 효과라고 지칭합니다. Fiber 트리는 가상 돔 노드에 의해 작업을 나누기 위해 만들어 집니다. 모든 가상 돔 노드 작업을 표현합니다. 마지막 출력은 업데이트되고, 추가되고, 삭제되는 노드를 보여주는 효과 리스트입니다.
React Fiber는 각 노드가 자식, 형제, 그리고 반환을 가진 속성을 가지도록 가상 돔 트리를 Fiber 트리로 변환합니다. Fiber 트리는 후위 순회를 특징으로 합니다.
1. 순회는 끝부터 시작합니다.
2. 자식이 있다면, 자식을 먼저 순회합니다; 아니면 완료합니다.
3. 자식 순회를 위해서는:
구조를 만들어 봅시다
const A1 = { type: 'div', key: 'A1' }
const B1 = { type: 'div', key: 'B1', return: A1 }
const B2 = { type: 'div', key: 'B2', return: A1 }
const C1 = { type: 'div', key: 'C1', return: B1 }
const C2 = { type: 'div', key: 'C2', return: B1 }
const C3 = { type: 'div', key: 'C3', return: B2 }
const C4 = { type: 'div', key: 'C4', return: B2 }
A1.child = B1
B1.sibling = B2
B1.child = C1
C1.sibling = C2
B2.child = C3
C3.sibling = C4
module.exports = A1
순회 메소드 코드를 작성합니다.
let rootFiber = require('./element')
const beginWork = (Fiber) => {
console.log(`${Fiber.key} start`)
}
const completeUnitWork = (Fiber) => {
console.log(`${Fiber.key} end`)
}
// 순회 함수
const performUnitOfWork = (Fiber) => {
beginWork(Fiber)
if (Fiber.child) {
return Fiber.child
}
while (Fiber) {
completeUnitWork(Fiber)
if (Fiber.sibling) {
return Fiber.sibling
}
Fiber = Fiber.return
}
}
const workloop = (nextUnitOfWork) => {
// 보류 중인 단위를 실행하고 다음 실행 단위로 넘어갑니다.
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
if (!nextUnitOfWork) {
console.log('The end of reconciliation')
}
}
workloop(rootFiber)
/*
결과:
B1 start
C1 start
C1 end // C1 completed
C2 start
C2 end // C2 completed
B1 end // B1 completed
B2 start
C3 start
C3 end // C3 completed
C4 start
C4 end // C4 completed
B2 end // B2 completed
A1 end // A1 completed
reconciliation end
*/
순회 메소드를 배우고 나서, 효과 리스트를 생성하기 위한 순회를 하는 동안 모든 노드 변화를 수집해야 합니다. 명심: 오직 바뀐 노드만 효과 리스트에 포함됩니다. 루트 노드의 효과 리스트에 모든 변경 사항을 기록하기 위해 각 노드가 업데이트 될 때 상향식으로 효과 리스트를 병합함으로써 작업 결과는 수집됩니다.
const reconcileChildren = (currentFiber, newChildren) => {
let newChildIndex = 0
let prevSibling // 이전 자식 fiber
// 자식 가상 돔 요소 배열을 순회하고 각 가상돔 요소의 자식 fiber를 생성합니다.
while (newChildIndex < newChildren.length) {
let newChild = newChildren[newChildIndex]
let tag
// fiber 타입을 명시할 태그 추가
if (newChild.type === ELEMENT_TEXT) { // 글자 노드
tag = TAG_TEXT
} else if (typeof newChild.type === 'string') { // 타입이 문자열이라면, 돔 노드는 native 노드입니다.
tag = TAG_HOST
}
let newFiber = {
tag,
type: newChild.type,
props: newChild.props,
stateNode: null, // 돔 요소가 생성되지 않음
return: currentFiber, // 부모 fiber
effectTag: INSERT, // 추가, 삭제, 업데이트를 포함하는 효과 식별자
nextEffect: null, // 다음 fiber를 가리킵니다. 효과 리스트는 다음 효과 포인터에 의해 연결됩니다.
}
if (newFiber) {
if (newChildIndex === 0) {
currentFiber.child = newFiber // 자식은 첫째 자식
} else {
prevSibling.sibling = newFiber // 첫째 자식의 형제에서 둘째 자식을 가리킨다.
}
prevSibling = newFiber
}
newChildIndex++
}
}
fiber 노드 아래로 모든 효과를 수집하는 메소드를 선언하고 효과 리스트를 생성합니다. 명심: 각 fiber는 두 속성이 있습니다.
nextEffect 사이에 있는 자식 fiber를 연결하고 Linked list를 만드는 데에 사용됩니다.
// 완료 시 효과가 있는 fiber를 수집하여 효과 리스트를 생성합니다.
const completeUnitOfWork = (currentFiber) => {
// 후위 순회에서는 모든 자식 노드가 순회될 때만 노드 순회가 완료됩니다. 마지막으로 위 그림과 같은 사슬 모양의 구조가 얻어집니다.
let returnFiber = currentFiber.return
if (returnFiber) {
// 부모 노드의 firstEffect가 값이 엄다면, 형재 fiber의 firstEffect를 가리킵니다.
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect
}
// 현재 Fiber의 lastEffect에 값이 있는 경우
if (currentFiber.lastEffect) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect
}
returnFiber.lastEffect = currentFiber.lastEffect
}
const effectTag = currentFiber.effectTag
if (effectTag) { // 부수 효과가 나옴
// 각 fiber는 두 속성이 있습니다.
// firstEffect: 효과를 가진 첫째 자식을 가리킴
// lastEffect: 효과를 가진 마지막 자식을 가리킴
// nextEffect 사이에 있는 자식 fiber를 연결하고 Linked list를 만드는 데에 사용됩니다.
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber
} else {
returnFiber.firstEffect = currentFiber
}
returnFiber.lastEffect = currentFiber
}
}
}
루트 노드에서 모든 fiber 노드를 순회하는 재귀 함수를 선언하고 마지막 일반 효과 리스트를 생성합니다.
// 노드와 자식 노드에 있는 작업 완료
const performUnitOfWork = (currentFiber) => {
beginWork(currentFiber)
if (currentFiber.child) {
return currentFiber.child
}
while (currentFiber) {
completeUnitOfWork(currentFiber) // 노드 그자체로 종료
if (currentFiber.sibling) { // 아무거나라면, 형제 반환
return currentFiber.sibling
}
currentFiber = currentFiber.return // 형제가 없다면 부모로 가서 부모의 형제를 찾는다.
}
}
이전 단계에서 얻은 효과는 커밋 단계에서 모두 함께 실행되며 일시 중지할 수 없습니다. 그렇지 않으면 사용자가 끊김 현상을 경험할 수 있습니다. 이 단계에서 모든 업데이트는 효과 목록에 따라 돔 트리에 커밋됩니다.
아래 예제는 Fiber 효과 리스트를 기반으로 화면을 어떻게 하는 지 보여줍니다.
오직 추가, 삭제, 그리고 노드 업데이트만 나와 있습니다.
const commitWork = currentFiber => {
if (!currentFiber) return
let returnFiber = currentFiber.return
let returnDOM = returnFiber.stateNode // 부모 요소
if (currentFiber.effectTag === INSERT) { // 현재 fiber의 effectTag가 INSERT일때 삽입되는 노드입니다
returnDOM.appendChild(currentFiber.stateNode)
} else if (currentFiber.effectTag === DELETE) { // 현재 fiber의 effectTag가 DELETE일때 삭제되는 노드입니다
returnDOM.removeChild(currentFiber.stateNode)
} else if (currentFiber.effectTag === UPDATE) { // 현재 fiber의 effectTag가 UPDATE일때 업데이트되는 노드입니다
if (currentFiber.type === ELEMENT_TEXT) {
if (currentFiber.alternate.props.text !== currentFiber.props.text) {
currentFiber.stateNode.textContent = currentFiber.props.text
}
}
}
currentFiber.effectTag = null
}
루트 노드의 효과 목록에 따라 모든 업데이트를 완료하는 재귀 함수를 작성합니다.
const commitRoot = () => {
let currentFiber = workInProgressRoot.firstEffect
while (currentFiber) {
commitWork(currentFiber)
currentFiber = currentFiber.nextEffect
}
currentRoot = workInProgressRoot // 성공적으로 현재 루트에 렌더링된 현재 루트 fiber를 할당합니다.
workInProgressRoot = null
}
이제 루프 실행을 만들어 보겠습니다. 각 fiber의 효과 목록을 얻은 후 commitRoot를 호출하여 보기 업데이트를 완료합니다.
const workloop = (deadline) => {
let shouldYield = false // 제어권을 받았는 지
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1 // 작업 실행 이후에 남는 시간이 1ms 이하라면 브라우저가 제어를 해야 함
}
if (!nextUnitOfWork && workInProgressRoot) {
console.log('The end of the render stage ')
commitRoot() // 보류중인 작업 없음. 효과 리스트를 기반으로 화면 업데이트
}
// 다른 작업의 스케쥴을 다시 조정하기 위해 브라우저에게 요청
requestIdleCallback(workloop, { timeout: 1000 })
}
수집된 변경사항 정보를 바탕으로 뷰가 새로고침되었습니다.
이 글은 React Fiber의 일반적인 개요입니다.
React에서 Fiber가 도입된 이유와 Fiber의 디자인 아이디어, 구현 방법을 단계별로 설명했습니다.
그러나 작업 우선 순위를 정의하는 방법, 작업을 일시 중지하고 재개하는 방법 등 많은 세부 사항은 설명되지 않습니다. 관심이 있으시면 React 소스 코드에 대해 자세히 알아보고 연구를 계속하는 편이 좋습니다.
https://github.com/facebook/react
https://www.alibabacloud.com/blog/a-closer-look-at-react-fiber_598138