ホームページ > 記事 > ウェブフロントエンド > 小さな 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 ノードを新しい 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 サイトの他の関連記事を参照してください。