이 튜토리얼은 이 튜토리얼을 기반으로 하지만 JSX, TypeScript 및 구현하기 더 쉬운 접근 방식을 사용합니다. 내 GitHub 저장소에서 메모와 코드를 확인할 수 있습니다.
이제 반응성에 대해 이야기해 보겠습니다.
새 광케이블과 비교할 수 있도록 기존 광케이블을 보관해야 합니다. 우리는 광섬유에 필드를 추가하여 이를 수행할 수 있습니다. 또한 나중에 유용하게 사용될 전용 필드가 필요합니다.
export interface Fiber { type: string props: VDomAttributes parent: Fiber | null child: Fiber | null sibling: Fiber | null dom: HTMLElement | Text | null alternate: Fiber | null committed: boolean }
여기서 커밋된 상태를 설정합니다.
function commit() { function commitChildren(fiber: Fiber | null) { if(!fiber) { return } if(fiber.dom && fiber.parent?.dom) { fiber.parent.dom.appendChild(fiber.dom) fiber.committed = true } if(fiber.dom && fiber.parent && isFragment(fiber.parent.vDom) && !fiber.committed) { let parent = fiber.parent // find the first parent that is not a fragment while(parent && isFragment(parent.vDom)) { // the root element is guaranteed to not be a fragment has has a non-fragment parent parent = parent.parent! } parent.dom?.appendChild(fiber.dom!) fiber.committed = true } commitChildren(fiber.child) commitChildren(fiber.sibling) } commitChildren(wip) wipParent?.appendChild(wip!.dom!) wip!.committed = true wip = null }
오래된 섬유나무도 살려야 합니다.
let oldFiber: Fiber | null = null function commit() { function commitChildren(fiber: Fiber | null) { if(!fiber) { return } if(fiber.dom && fiber.parent?.dom) { fiber.parent.dom.appendChild(fiber.dom) fiber.committed = true } commitChildren(fiber.child) commitChildren(fiber.sibling) } commitChildren(wip) wipParent?.appendChild(wip!.dom!) wip!.committed = true oldFiber = wip wip = null }
이제 반복 중에 기존 Fiber와 새 Fiber를 비교해야 합니다. 이것을 화해 과정이라고 합니다.
오래된 광섬유와 새 광섬유를 비교해 볼 필요가 있습니다. 초기 작업에서는 먼저 오래된 Fiber를 투입합니다.
export function render(vDom: VDomNode, parent: HTMLElement) { wip = { parent: null, sibling: null, child: null, vDom: vDom, dom: null, committed: false, alternate: oldFiber, } wipParent = parent nextUnitOfWork = wip }
그런 다음 새로운 섬유의 생성을 새로운 기능으로 분리합니다.
function reconcile(fiber: Fiber, isFragment: boolean) { 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: isFragment ? fiber.parent : fiber, dom: null, sibling: null, child: null, vDom: element, committed: false, alternate: null, } if (index === 0) { fiber.child = newFiber } else { prevSibling!.sibling = newFiber } prevSibling = newFiber index++ } } } function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null { if(!nextUnitOfWork) { return null } const fiber = nextUnitOfWork const isFragment = isElement(fiber.vDom) && fiber.vDom.tag === '' && fiber.vDom.kind === 'fragment' if(!fiber.dom && !isFragment) { fiber.dom = createDom(fiber.vDom) } reconcile(fiber, isFragment) if (fiber.child) { return fiber.child } let nextFiber: Fiber | null = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent } return null }
단, 기존 광케이블을 새 광케이블에 장착해야 합니다.
function reconcile(fiber: Fiber, isFragment: boolean) { if (isElement(fiber.vDom)) { const elements = fiber.vDom.children ?? [] let index = 0 let prevSibling = null let currentOldFiber = fiber.alternate?.child ?? null while (index < elements.length) { const element = elements[index] const newFiber: Fiber = { parent: isFragment ? fiber.parent : fiber, dom: null, sibling: null, child: null, vDom: element, committed: false, alternate: currentOldFiber, } if (index === 0) { fiber.child = newFiber } else { prevSibling!.sibling = newFiber } prevSibling = newFiber currentOldFiber = currentOldFiber?.sibling ?? null index++ } } }
이제 기존 Fiber를 새 Fiber에 장착했습니다. 하지만 재렌더링을 트리거할 항목이 없습니다. 지금은 버튼을 추가하여 수동으로 트리거합니다. 아직 상태가 없기 때문에 vDOM을 변경하기 위해 props를 사용합니다.
import { render } from "./runtime"; import { createElement, fragment, VDomAttributes, VDomNode } from "./v-dom"; type FuncComponent = (props: VDomAttributes, children: VDomNode[]) => JSX.Element const App: FuncComponent = (props: VDomAttributes, __: VDomNode[]) => { return <div> <> <h1>H1</h1> <h2>{props["example"]?.toString()}</h2> { props["show"] ? <p>show</p> : <></> } <h1>H1</h1> </> </div> } const app = document.getElementById('app') const renderButton = document.createElement('button') renderButton.textContent = 'Render' let cnt = 0 renderButton.addEventListener('click', () => { const vDom: VDomNode = App({ "example": (new Date()).toString(), "show": cnt % 2 === 0 }, []) as unknown as VDomNode cnt++ render(vDom, app!) }) document.body.appendChild(renderButton)
이제 renderButton을 클릭하면 렌더링된 결과가 한 번 반복됩니다. 현재의 모든 로직은 단순히 렌더링된 vDOM을 문서에 넣는 것뿐입니다.
Commit 기능에 console.log를 추가하면 Alternative Fiber가 출력되는 것을 볼 수 있습니다.
이제 기존 Fiber와 새 Fiber를 처리하는 방법을 정의하고 정보를 기반으로 DOM을 변경해야 합니다. 규칙은 다음과 같습니다.
새로운 섬유 각각에 대해
좀 헷갈리시나요? 음, 코드를 보여드리겠습니다. 먼저 이전 DOM 생성을 삭제합니다. 그런 다음 위의 규칙을 적용하세요.
첫 번째 규칙, 오래된 섬유가 있으면 오래된 섬유와 새로운 섬유의 함량을 비교합니다. 서로 다른 경우 이전 DOM 노드를 새 DOM 노드로 바꾸거나 이전 DOM 노드를 새 DOM 노드에 복사합니다.
export function vDOMEquals(a: VDomNode, b: VDomNode): boolean { if (isString(a) && isString(b)) { return a === b } else if (isElement(a) && isElement(b)) { let ret = a.tag === b.tag && a.key === b.key if (!ret) return false if (a.props && b.props) { const aProps = a.props const bProps = b.props const aKeys = Object.keys(aProps) const bKeys = Object.keys(bProps) if (aKeys.length !== bKeys.length) return false for (let i = 0; i < aKeys.length; i++) { const key = aKeys[i] if (key === 'key') continue if (aProps[key] !== bProps[key]) return false } for (let i = 0; i < bKeys.length; i++) { const key = bKeys[i] if (key === 'key') continue if (aProps[key] !== bProps[key]) return false } return true } else { return a.props === b.props } } else { return false } }
그런 다음 작은 리팩토링을 했습니다.
export interface Fiber { type: string props: VDomAttributes parent: Fiber | null child: Fiber | null sibling: Fiber | null dom: HTMLElement | Text | null alternate: Fiber | null committed: boolean }
이제 커밋에 있어서 기존 Fiber와 새 Fiber를 비교할 수 있는 추가 대안 필드가 생겼습니다.
원래 커밋 기능입니다
function commit() { function commitChildren(fiber: Fiber | null) { if(!fiber) { return } if(fiber.dom && fiber.parent?.dom) { fiber.parent.dom.appendChild(fiber.dom) fiber.committed = true } if(fiber.dom && fiber.parent && isFragment(fiber.parent.vDom) && !fiber.committed) { let parent = fiber.parent // find the first parent that is not a fragment while(parent && isFragment(parent.vDom)) { // the root element is guaranteed to not be a fragment has has a non-fragment parent parent = parent.parent! } parent.dom?.appendChild(fiber.dom!) fiber.committed = true } commitChildren(fiber.child) commitChildren(fiber.sibling) } commitChildren(wip) wipParent?.appendChild(wip!.dom!) wip!.committed = true wip = null }
이름을 조금 바꾸겠습니다. 예전 이름이 틀렸을 뿐이에요(죄송합니다).
let oldFiber: Fiber | null = null function commit() { function commitChildren(fiber: Fiber | null) { if(!fiber) { return } if(fiber.dom && fiber.parent?.dom) { fiber.parent.dom.appendChild(fiber.dom) fiber.committed = true } commitChildren(fiber.child) commitChildren(fiber.sibling) } commitChildren(wip) wipParent?.appendChild(wip!.dom!) wip!.committed = true oldFiber = wip wip = null }
그럼 우리는 어떻게 해야 할까요? 기존 로직은 추가만 하므로 이를 추출합니다.
export function render(vDom: VDomNode, parent: HTMLElement) { wip = { parent: null, sibling: null, child: null, vDom: vDom, dom: null, committed: false, alternate: oldFiber, } wipParent = parent nextUnitOfWork = wip }
더 많은 유연성을 제공하려면 커밋 단계까지 DOM 구성을 연기해야 합니다.
function reconcile(fiber: Fiber, isFragment: boolean) { 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: isFragment ? fiber.parent : fiber, dom: null, sibling: null, child: null, vDom: element, committed: false, alternate: null, } if (index === 0) { fiber.child = newFiber } else { prevSibling!.sibling = newFiber } prevSibling = newFiber index++ } } } function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null { if(!nextUnitOfWork) { return null } const fiber = nextUnitOfWork const isFragment = isElement(fiber.vDom) && fiber.vDom.tag === '' && fiber.vDom.kind === 'fragment' if(!fiber.dom && !isFragment) { fiber.dom = createDom(fiber.vDom) } reconcile(fiber, isFragment) if (fiber.child) { return fiber.child } let nextFiber: Fiber | null = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent } return null }
첫 번째와 두 번째 규칙에 따라 다음 코드로 리팩토링합니다.
function reconcile(fiber: Fiber, isFragment: boolean) { if (isElement(fiber.vDom)) { const elements = fiber.vDom.children ?? [] let index = 0 let prevSibling = null let currentOldFiber = fiber.alternate?.child ?? null while (index < elements.length) { const element = elements[index] const newFiber: Fiber = { parent: isFragment ? fiber.parent : fiber, dom: null, sibling: null, child: null, vDom: element, committed: false, alternate: currentOldFiber, } if (index === 0) { fiber.child = newFiber } else { prevSibling!.sibling = newFiber } prevSibling = newFiber currentOldFiber = currentOldFiber?.sibling ?? null index++ } } }
자바스크립트에서는 모든 값이 참조라는 점을 항상 명심하세요. fibre.dom = fibre.alternate.dom이면 fibre.dom과 fibre.alternate.dom은 동일한 개체를 가리킵니다. Fiber.dom을 변경하면 Fiber.alternate.dom도 변경되고 그 반대도 마찬가지입니다. 그렇기 때문에 교체할 때 단순히 Fiber.alternate.dom?.replaceWith(섬유.dom)를 사용했습니다. 그러면 이전 DOM이 새 DOM으로 대체됩니다. 이전 상위 항목을 복사하면 DOM에 대해 Fiber.alternate.dom이 있지만 DOM도 교체됩니다.
단, 아직 삭제 처리는 하지 않았습니다.
알겠습니다. 이전 코드에는 좀 더 복잡한 jsx를 작성하면서 발견한 몇 가지 버그가 포함되어 있으므로 삭제를 구현하기 전에 수정해 보겠습니다.
이전에 버그가 있었습니다. 목록을 props에 전달할 수 없습니다. 이번 기회에 수정해 보겠습니다.
import { render } from "./runtime"; import { createElement, fragment, VDomAttributes, VDomNode } from "./v-dom"; type FuncComponent = (props: VDomAttributes, children: VDomNode[]) => JSX.Element const App: FuncComponent = (props: VDomAttributes, __: VDomNode[]) => { return <div> <> <h1>H1</h1> <h2>{props["example"]?.toString()}</h2> { props["show"] ? <p>show</p> : <></> } <h1>H1</h1> </> </div> } const app = document.getElementById('app') const renderButton = document.createElement('button') renderButton.textContent = 'Render' let cnt = 0 renderButton.addEventListener('click', () => { const vDom: VDomNode = App({ "example": (new Date()).toString(), "show": cnt % 2 === 0 }, []) as unknown as VDomNode cnt++ render(vDom, app!) }) document.body.appendChild(renderButton)
그럼 유형을 수정하세요. 오류가 하나뿐이니까 직접 해 보세요.
그러나 다음과 같은 코드가 있다면
export function vDOMEquals(a: VDomNode, b: VDomNode): boolean { if (isString(a) && isString(b)) { return a === b } else if (isElement(a) && isElement(b)) { let ret = a.tag === b.tag && a.key === b.key if (!ret) return false if (a.props && b.props) { const aProps = a.props const bProps = b.props const aKeys = Object.keys(aProps) const bKeys = Object.keys(bProps) if (aKeys.length !== bKeys.length) return false for (let i = 0; i < aKeys.length; i++) { const key = aKeys[i] if (key === 'key') continue if (aProps[key] !== bProps[key]) return false } for (let i = 0; i < bKeys.length; i++) { const key = bKeys[i] if (key === 'key') continue if (aProps[key] !== bProps[key]) return false } return true } else { return a.props === b.props } } else { return false } }
우리 일이 또 망가졌어요...
그렇습니다. 위의 경우 하위 항목이 중첩 배열이 될 수 있기 때문에 이들을 평면화해야 합니다.
하지만 그것만으로는 충분하지 않습니다. 으, 우리 createDom은 정수가 아닌 문자열이나 요소만 인식하므로 숫자를 문자열로 묶어야 합니다.
function buildDom(fiber: Fiber, fiberIsFragment: boolean) { if(fiber.dom) return if(fiberIsFragment) return fiber.dom = createDom(fiber.vDom) } function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null { if(!nextUnitOfWork) { return null } const fiber = nextUnitOfWork const fiberIsFragment = isFragment(fiber.vDom) reconcile(fiber) buildDom(fiber, fiberIsFragment); if (fiber.child) { return fiber.child } let nextFiber: Fiber | null = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent } return null }
좋아, 이제 일이 잘 풀리네요.
렌더링 버튼을 누르면 목록이 업데이트되지만 이전 요소는 여전히 남아 있습니다. 이전 요소를 삭제해야 합니다.
여기서 규칙을 다시 설명합니다. 새 파이버에 대해 하위 또는 형제 자매가 없지만 기존 파이버에 하위 또는 형제 자매가 있는 경우 기존 하위 또는 형제 자매를 재귀적으로 제거합니다.
function commit() { function commitChildren(fiber: Fiber | null) { if(!fiber) { return } if(fiber.dom && fiber.parent?.dom) { fiber.parent?.dom?.appendChild(fiber.dom) fiber.committed = true } if(fiber.dom && fiber.parent && isFragment(fiber.parent.vDom) && !fiber.committed) { let parent = fiber.parent // find the first parent that is not a fragment while(parent && isFragment(parent.vDom)) { // the root element is guaranteed to not be a fragment has has a non-fragment parent parent = parent.parent! } parent.dom?.appendChild(fiber.dom!) fiber.committed = true } commitChildren(fiber.child) commitChildren(fiber.sibling) } commitChildren(wip) wipParent?.appendChild(wip!.dom!) wip!.committed = true oldFiber = wip wip = null }
재귀 제거를 수행하지 않으면 삭제가 필요한 항목이 여러 개 있을 때 일부 오래된 요소가 매달려 있게 됩니다.
로 변경할 수 있습니다.
function commit() { function commitToParent(fiber: Fiber | null) { if(!fiber) { return } if(fiber.dom && fiber.parent?.dom) { fiber.parent?.dom?.appendChild(fiber.dom) fiber.committed = true } if(fiber.dom && fiber.parent && isFragment(fiber.parent.vDom) && !fiber.committed) { let parent = fiber.parent // find the first parent that is not a fragment while(parent && isFragment(parent.vDom)) { // the root element is guaranteed to not be a fragment has has a non-fragment parent parent = parent.parent! } parent.dom?.appendChild(fiber.dom!) fiber.committed = true } commitToParent(fiber.child) commitToParent(fiber.sibling) } commitToParent(wip) wipParent?.appendChild(wip!.dom!) wip!.committed = true oldFiber = wip wip = null }
참고용.
이것은 어려운 장이지만 솔직히 말해서 꽤 전통적인 코딩입니다. 그러나 지금까지 React가 아래에서 위로 어떻게 작동하는지 이해했습니다.
사실 이제 모든 것이 이미 작동할 수 있습니다. 소품을 변경할 때마다 수동으로 다시 렌더링을 실행할 수 있습니다. 하지만 이렇게 답답한 수작업은 우리가 원하는 것이 아닙니다. 우리는 반응이 자동이기를 원합니다. 그래서 다음 장에서 Hook에 대해 이야기해보겠습니다.
위 내용은 작은 React Chpdating vDOM 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!