本教學是基於本教學,但使用了 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 進行比較。這稱為協調過程。
我們需要將舊光纖與新光纖進行比較。我們首先將舊纖維放入初始工作中。
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 }
然後我們將新 Fiber 的建立分離到一個新函數中。
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++ } } }
現在我們已將舊光纖安裝到新光纖上。但我們沒有任何東西可以觸發重新渲染 - 目前,我們透過添加按鈕來手動觸發它。由於我們還沒有狀態,因此我們使用 props 來改變 vDOM。
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)
現在,如果您按一下渲染按鈕,渲染結果將重複一次,因為我們目前的所有邏輯只是將渲染的 vDOM 放入文件中。
如果在提交函數中新增console.log,您可以看到列印出備用光纖。
現在我們需要定義如何處理舊的 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 }
現在,當涉及提交時,我們有一個額外的替代欄位來比較舊光纖和新光纖。
這是原始的提交函數,
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++ } } }
請永遠記住,在 javascript 中,所有值都是引用。如果我們有 Fiber.dom = Fiber.alternate.dom,那麼 Fiber.dom 和 Fiber.alternate.dom 將指向同一個物件。如果我們改變 Fiber.dom,fibre.alternate.dom 也會改變,反之亦然。這就是為什麼在替換時,我們簡單地使用 Fiber.alternate.dom?.replaceWith(fibre.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 }
好的,現在一切正常了 - 有點。
如果您點擊渲染按鈕,清單會更新 - 但舊元素仍然保留。我們需要刪除舊元素。
我們在這裡重申規則 - 對於任何新的 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 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 的工作原理。
實際上,現在一切已經可以工作了——每當我們更改道具時,我們都可以手動觸發重新渲染。然而,這種令人沮喪的體力勞動並不是我們想要的。我們希望反應是自動的。所以,我們將在下一章討論 hooks。
以上是建構一個小型 React Chpdating vDOM的詳細內容。更多資訊請關注PHP中文網其他相關文章!