>웹 프론트엔드 >JS 튜토리얼 >작은 React Chpdating vDOM 구축

작은 React Chpdating vDOM 구축

Linda Hamilton
Linda Hamilton원래의
2024-10-20 18:31:30271검색

Build a Tiny React Chpdating vDOM

이 튜토리얼은 이 튜토리얼을 기반으로 하지만 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을 변경해야 합니다. 규칙은 다음과 같습니다.

새로운 섬유 각각에 대해

  • 오래된 Fiber가 있는 경우 기존 Fiber의 내용을 새 Fiber와 비교하고, 서로 다른 경우 이전 DOM 노드를 새 DOM 노드로 교체하거나 이전 DOM 노드를 새로운 DOM 노드. 두 개의 vDOM이 동일하다는 것은 해당 태그와 모든 속성이 동일하다는 것을 의미합니다. 그들의 아이들은 다를 수 있습니다.
  • 오래된 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.