이 튜토리얼은 이 튜토리얼을 기반으로 하지만 JSX, TypeScript 및 구현하기 더 쉬운 접근 방식을 사용합니다. 내 GitHub 저장소에서 메모와 코드를 확인할 수 있습니다.
이 부분에서는 vDOM을 실제 DOM으로 렌더링합니다. 추가적으로 React의 핵심구조인 Fiber Tree에 대해서도 소개하겠습니다.
vDOM 렌더링은 간단합니다. 너무 간단합니다. 다음의 웹 네이티브 API를 알아야 합니다.
와, 좀 과하지 않나요? 하지만 vDOM 생성을 실제 DOM에 미러링하기만 하면 됩니다. 간단한 예를 들어보겠습니다.
function render(vDom: VDomNode, parent: HTMLElement) { if (typeof vDom === 'string') { parent.appendChild(document.createTextNode(vDom)) } else if (vDom.kind === 'element') { const element = document.createElement(vDom.tag) for (const [key, value] of Object.entries(vDom.props ?? {})) { if (key === 'key') continue if (key.startsWith('on')) { element.addEventListener(key.slice(2).toLowerCase(), value as EventListener) } else { element.setAttribute(key, value as string) } } for (const child of vDom.children ?? []) { render(child, element) } parent.appendChild(element) } else { for (const child of vDom.children ?? []) { render(child, parent) } } }
on으로 시작하는 속성을 이벤트 리스너로 등록했는데, 이는 React에서 일반적인 관행입니다. 또한 렌더링용이 아닌 조정용으로 사용되는 키 속성을 무시했습니다.
그럼 렌더링이 완료되고 이번 챕터가 끝나는 걸까요...? 아니요
실제 반응에서는 렌더링 프로세스가 좀 더 복잡합니다. 좀 더 구체적으로 말하자면, requestIdleCallback을 사용하여 더 긴급한 작업을 먼저 처리하고 우선 순위를 낮춥니다.
requestIdleCallback은 Safari, MacOS 및 iOS 모두에서 지원되지 않습니다(Apple 엔지니어 여러분, 이유는 무엇입니까? 적어도 2024년에는 작업 중입니다). Mac을 사용하는 경우 Chrome을 사용하거나 간단한 setTimeout으로 바꾸세요. 실제 React에서는 이를 처리하기 위해 Scheduler를 사용하지만 기본 개념은 동일합니다.
그러려면 다음과 같은 웹 네이티브 API를 알아야 합니다.
따라서 렌더링을 청크로 분할하고 requestIdleCallback을 사용하여 이를 처리해야 합니다. 간단한 방법은 한 번에 하나의 노드를 렌더링하는 것입니다. 쉽지만 너무 열성적으로 하지 마십시오. 그렇지 않으면 렌더링하는 동안 수행해야 할 다른 작업도 필요하기 때문에 많은 시간을 낭비하게 됩니다.
그러나 우리가 하려는 작업에 대한 기본 프레임워크로 다음 코드를 사용할 수 있습니다.
function render(vDom: VDomNode, parent: HTMLElement) { if (typeof vDom === 'string') { parent.appendChild(document.createTextNode(vDom)) } else if (vDom.kind === 'element') { const element = document.createElement(vDom.tag) for (const [key, value] of Object.entries(vDom.props ?? {})) { if (key === 'key') continue if (key.startsWith('on')) { element.addEventListener(key.slice(2).toLowerCase(), value as EventListener) } else { element.setAttribute(key, value as string) } } for (const child of vDom.children ?? []) { render(child, element) } parent.appendChild(element) } else { for (const child of vDom.children ?? []) { render(child, parent) } } }
이제 // TODO를 vDOM 렌더링으로 채우고 렌더링할 다음 vDOM 노드를 반환하면 간단한 유휴 시간 렌더링을 수행할 수 있습니다. 하지만 서두르지 마세요. 더 많은 작업이 필요합니다.
다음 장에서는 반응성을 구현할 예정인데 조정이 다소 복잡하므로 일부 콘텐츠를 파이버 트리인 이 부분으로 옮깁니다.
Fiber Tree는 단지 특별한 데이터 구조일 뿐입니다. React가 변경사항을 처리할 때 다음과 같은 프로세스를 수행합니다.
아시다시피 React에는 Fiber Tree가 필수입니다.
파이버 트리는 기존 트리와 조금 다르게 노드 사이에 세 가지 유형의 관계가 있습니다.
예를 들어 다음 DOM의 경우
import { createDom, VDomNode } from "./v-dom" interface Fiber { parent: Fiber | null sibling: Fiber | null child: Fiber | null vDom: VDomNode, dom: HTMLElement | Text | null } let nextUnitOfWork: Fiber | null = null function workLoop(deadline: IdleDeadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null { // TODO throw new Error('Not implemented') }
나무로 표현하면 됩니다.
<div> <p></p> <div> <h1></h1> <h2></h2> </div> </div>
p는 루트 div의 자식이지만 보조 div는 루트 div의 자식이 아니라 p의 형제입니다. h1과 h2는 보조 div의 하위입니다.
렌더링의 경우 순서는 주로 깊이 우선이지만 약간 다르기 때문에 기본적으로 다음 규칙을 따릅니다. 각 노드별로 다음과 같은 단계를 거칩니다.
이제 구현해 보겠습니다. 하지만 먼저 렌더링 프로세스를 시작해야 합니다. 간단합니다. nextUnitOfWork를 파이버 트리의 루트로 설정하기만 하면 됩니다.
function render(vDom: VDomNode, parent: HTMLElement) { if (typeof vDom === 'string') { parent.appendChild(document.createTextNode(vDom)) } else if (vDom.kind === 'element') { const element = document.createElement(vDom.tag) for (const [key, value] of Object.entries(vDom.props ?? {})) { if (key === 'key') continue if (key.startsWith('on')) { element.addEventListener(key.slice(2).toLowerCase(), value as EventListener) } else { element.setAttribute(key, value as string) } } for (const child of vDom.children ?? []) { render(child, element) } parent.appendChild(element) } else { for (const child of vDom.children ?? []) { render(child, parent) } } }
렌더링을 트리거한 후 브라우저는 PerformUnitOfWork를 호출하며 여기서 작업을 수행합니다.
첫 번째는 실제 DOM 요소를 생성해야 한다는 것입니다. 새 DOM 요소를 생성하고 이를 상위 DOM 요소에 추가하면 됩니다.
import { createDom, VDomNode } from "./v-dom" interface Fiber { parent: Fiber | null sibling: Fiber | null child: Fiber | null vDom: VDomNode, dom: HTMLElement | Text | null } let nextUnitOfWork: Fiber | null = null function workLoop(deadline: IdleDeadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null { // TODO throw new Error('Not implemented') }
<div> <p></p> <div> <h1></h1> <h2></h2> </div> </div>
작업의 첫 번째 부분입니다. 이제 현재의 광섬유에서 분기된 광섬유를 구성해야 합니다.
div ├── p └── div ├── h1 └── h2
이제 현재 노드에 대해 파이버 트리가 구축되었습니다. 이제 규칙에 따라 섬유 나무를 처리해 보겠습니다.
export function render(vDom: VDomNode, parent: HTMLElement) { nextUnitOfWork = { parent: null, sibling: null, child: null, vDom: vDom, dom: parent } }
이제 vDOM을 렌더링할 수 있습니다. typescript는 가상 DOM의 유형을 알 수 없기 때문에 어리석은 짓이라는 점에 유의하십시오. 여기서는 추악한 우회가 필요합니다.
function isString(value: VDomNode): value is string { return typeof value === 'string' } function isElement(value: VDomNode): value is VDomElement { return typeof value === 'object' } export function createDom(vDom: VDomNode): HTMLElement | Text | DocumentFragment { if (isString(vDom)) { return document.createTextNode(vDom) } else if (isElement(vDom)) { const element = document.createElement(vDom.tag === '' ? 'div' : vDom.tag) Object.entries(vDom.props ?? {}).forEach(([name, value]) => { if (value === undefined) return if (name === 'key') return if (name.startsWith('on') && value instanceof Function) { element.addEventListener(name.slice(2).toLowerCase(), value as EventListener) } else { element.setAttribute(name, value.toString()) } }) return element } else { throw new Error('Unexpected vDom type') } }
이제 vDOM이 실제 DOM으로 렌더링됩니다. 축하해요! 당신은 훌륭한 일을 해냈습니다. 하지만 아직 끝나지 않았습니다.
현재 구현에는 문제가 있습니다. 노드가 너무 많아 전체 프로세스 속도가 느려지면 사용자는 렌더링이 어떻게 수행되는지 확인할 수 있습니다. 물론 영업비밀이라던가 유출이 되진 않겠지만 좋은 경험은 아닙니다. 돔 생성을 커튼 뒤에 숨기고 한꺼번에 제출하는 것이 좋습니다.
해결책은 간단합니다. 문서에 직접 커밋하는 대신 문서에 추가하지 않고 요소를 만들고 작업이 완료되면 문서에 추가합니다. 이것을 누적 커밋이라고 합니다.
function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null { if(!nextUnitOfWork) { return null } if(!nextUnitOfWork.dom) { nextUnitOfWork.dom = createDom(nextUnitOfWork.vDom) } if(nextUnitOfWork.parent && nextUnitOfWork.parent.dom) { nextUnitOfWork.parent.dom.appendChild(nextUnitOfWork.dom) } // TODO throw new Error('Not implemented') }
이제 PerformUnitOfWork에서 AppendChild를 제거합니다. 즉, 다음 부분입니다.
const fiber = nextUnitOfWork if (isElement(fiber.vDom)) { const elements = fiber.vDom.children ?? [] let index = 0 let prevSibling = null while (index < elements.length) { const element = elements[index] const newFiber: Fiber = { parent: fiber, dom: null, sibling: null, child: null, vDom: element, } if (index === 0) { fiber.child = newFiber } else { prevSibling!.sibling = newFiber } prevSibling = newFiber index++ } }
이제 모든 작업을 마치면 모든 Fiber가 DOM으로 올바르게 구축되었지만 문서에 추가되지는 않습니다. 이러한 이벤트가 전달되면 문서에 DOM을 추가하는 커밋 함수를 호출합니다.
if (fiber.child) { return fiber.child } let nextFiber: Fiber | null = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent } return null
이제 커밋 기능은 간단합니다. 모든 하위 DOM을 wip에 재귀적으로 추가한 다음 wip를 DOM에 커밋하면 됩니다.
import { render } from "./runtime"; import { createElement, fragment, VDomNode } from "./v-dom"; function App() { return <> <h1>a</h1> <h2>b</h2> </> } const app = document.getElementById('app') const vDom: VDomNode = App() as unknown as VDomNode render(vDom, app!)
commitChildren 함수에 시간 초과를 추가하여 이를 테스트할 수 있습니다. 이전에는 렌더링이 단계별로 이루어졌는데 이제는 한꺼번에 이루어집니다.
다음과 같이 중첩된 함수를 사용해 볼 수도 있습니다.
function render(vDom: VDomNode, parent: HTMLElement) { if (typeof vDom === 'string') { parent.appendChild(document.createTextNode(vDom)) } else if (vDom.kind === 'element') { const element = document.createElement(vDom.tag) for (const [key, value] of Object.entries(vDom.props ?? {})) { if (key === 'key') continue if (key.startsWith('on')) { element.addEventListener(key.slice(2).toLowerCase(), value as EventListener) } else { element.setAttribute(key, value as string) } } for (const child of vDom.children ?? []) { render(child, element) } parent.appendChild(element) } else { for (const child of vDom.children ?? []) { render(child, parent) } } }
하지만 JSX를 구문 분석할 때 태그는 레이블 이름일 뿐이므로 작동하지 않습니다. 물론, 기본 요소의 경우 문자열일 뿐이지만 구성 요소의 경우 함수입니다. 그래서 JSX를 vDOM으로 변환하는 과정에서 태그가 함수인지 확인하고 함수라면 호출해야 합니다.
import { createDom, VDomNode } from "./v-dom" interface Fiber { parent: Fiber | null sibling: Fiber | null child: Fiber | null vDom: VDomNode, dom: HTMLElement | Text | null } let nextUnitOfWork: Fiber | null = null function workLoop(deadline: IdleDeadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null { // TODO throw new Error('Not implemented') }
이제 각 구성요소에 소품과 하위 요소가 필요합니다. 실제 React에서는 확인하기 위해 추가 필드를 추가했습니다. 함수를 클래스로 대체하면 추가 필드가 생기고 일반적인 팩토리 패턴인 객체를 생성하는 새 함수를 제공할 수 있지만 여기서는 게으르게 처리합니다.
<div> <p></p> <div> <h1></h1> <h2></h2> </div> </div>
실제 React에서는 함수 컴포넌트 호출이 Fiber 구축 단계로 지연된다는 점에 유의하세요. 그럼에도 불구하고 편의상 그렇게 한 것이며, 이 시리즈의 목적을 크게 훼손하지는 않습니다.
그러나 아직은 부족합니다. 이전에는 조각을 div로 처리했는데 이는 올바르지 않습니다. 하지만 이를 문서 조각으로 바꾸면 작동하지 않습니다. 그 이유는 프래그먼트가 일회성 컨테이너이기 때문에 실제 항목을 꺼낼 수 없고 중첩할 수 없으며 이상한 동작이 발생하는 일회성 컨테이너이기 때문입니다. 더 간단하게 작업할 수 없습니다...). 그러니까 젠장, 이걸 파헤쳐야 해.
그래서 해결책은 프래그먼트용 DOM을 생성하지 않고 DOM을 추가할 올바른 부모를 찾는 것입니다.
우리에게는
div ├── p └── div ├── h1 └── h2
렌더링도 바꿔보세요
export function render(vDom: VDomNode, parent: HTMLElement) { nextUnitOfWork = { parent: null, sibling: null, child: null, vDom: vDom, dom: parent } }
이제 조각이 올바르게 처리되었습니다.
위 내용은 작은 React Chendering vDOM 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!