小さな React Chpdating vDOM を構築する

Linda Hamilton
リリース: 2024-10-20 18:31:30
オリジナル
231 人が閲覧しました

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
}
ログイン後にコピー
ログイン後にコピー

ここで、反復中に古いファイバーと新しいファイバーを比較する必要があります。これは調整プロセスと呼ばれます。

和解

古いファイバーと新しいファイバーを比較する必要があります。最初の作業では、まず古いファイバーを入れます。

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++
        }
    }
}
ログイン後にコピー
ログイン後にコピー

これで、古いファイバーが新しいファイバーに取り付けられました。しかし、再レンダリングをトリガーするものは何もありません。今のところ、ボタンを追加して手動で再レンダリングをトリガーします。まだ状態を持っていないため、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 をクリックすると、レンダリングされた結果が 1 回繰り返されます。これは、現在のロジックはすべて、レンダリングされた vDOM をドキュメントに配置するだけであるためです。

コミット関数に console.log を追加すると、代替ファイバーが出力されることがわかります。

次に、古いファイバーと新しいファイバーを処理する方法を定義し、その情報に基づいて DOM を変更する必要があります。ルールは以下の通りです。

新しいファイバーごとに、

  • 古いファイバーがあった場合は、古いファイバーの内容と新しいファイバーを比較し、異なる場合は、古い DOM ノードを新しい DOM ノードに置き換えます。そうでない場合は、古い DOM ノードをコピーします。新しい DOM ノード。 2 つの vDOM が等しいということは、それらのタグとすべてのプロパティが等しいことを意味することに注意してください。彼らの子供たちは異なる可能性があります。
  • 古いファイバーがない場合は、新しい 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
}
ログイン後にコピー
ログイン後にコピー

コミットに関しては、古いファイバーと新しいファイバーを比較するための追加の代替フィールドが用意されています。

これはオリジナルのコミット関数です。

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
}
ログイン後にコピー
ログイン後にコピー

最初と 2 番目のルールに従って、それらを次のコードにリファクタリングします。

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++
        }
    }
}
ログイン後にコピー
ログイン後にコピー

JavaScript ではすべての値が参照であることに常に留意してください。 Fiber.dom = Fiber.alternate.dom の場合、 Fiber.dom と Fiber.alternate.dom は同じオブジェクトを指します。 Fiber.dom を変更すると、fiber.alternate.dom も変更され、その逆も同様です。そのため、置き換える際には、単に Fiber.alternate.dom?.replaceWith(fiber.dom) を使用しました。これにより、古い DOM が新しい DOM に置き換えられます。以前の親がコピーされると、その DOM の Fiber.alternate.dom が保持されますが、その DOM も置き換えられます。

ただし、削除はまだ対応していませんでした。

いくつかの事故

わかりました。前のコードには、より複雑な JSX を作成しているときに見つけたいくつかのバグが含まれています。そのため、削除を実装する前に、それらを修正しましょう。

以前はバグがありました - プロパティにリストを渡すことができません。この機会にそれを修正しましょう。

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)
ログイン後にコピー
ログイン後にコピー

それから、タイプのものを修正するだけです。私にとってエラーは 1 つだけなので、ご自身で修正してください。

ただし、次のコードがある場合、

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 が下から上までどのように動作するかを理解しました。

実際には、すでに動作するようになりました。プロパティを変更するたびに手動で再レンダリングをトリガーできます。しかし、そのようなイライラする手作業は私たちが望んでいることではありません。反応性を自動化したいと考えています。したがって、フックについては次の章で説明します。

以上が小さな React Chpdating vDOM を構築するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート