搜索
首页web前端js教程构建一个小型 React Chpdating vDOM

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 进行比较。这称为协调过程。

和解

我们需要将旧光纤与新光纤进行比较。我们首先将旧纤维放入初始工作中。

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 



<p>但是,我们需要将旧光纤安装到新光纤上。<br>
</p>

<pre class="brush:php;toolbar:false">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 



<p>现在我们已将旧光纤安装到新光纤上。但我们没有任何东西可以触发重新渲染 - 目前,我们通过添加按钮来手动触发它。由于我们还没有状态,因此我们使用 props 来改变 vDOM。<br>
</p>

<pre class="brush:php;toolbar:false">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 id="H">H1</h1>
            <h2 id="props-example-toString">{props["example"]?.toString()}</h2>
            {
                props["show"] ? <p>show</p> : >
            }
            <h1 id="H">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。规则如下。

对于每条新纤维,

  • 如果有旧的 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 



<p>然后我做了一些小的重构,<br>
</p><pre class="brush:php;toolbar: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 



<p>按照第一条和第二条规则,我们将它们重构为以下代码,<br>
</p>

<pre class="brush:php;toolbar:false">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 



<p>请始终记住,在 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 也将被替换。</p>

<p>但是,我们还没有处理删除操作。</p>

<h3>
  
  
  一些不幸的事
</h3>

<p>好吧,之前的代码包含一些我在编写更复杂的 jsx 时发现的错误,因此,在实现删除之前,让我们修复它们。</p>

<p>之前有一个错误 - 我们无法将列表传递给 props,让我们利用这个机会修复它。<br>
</p>

<pre class="brush:php;toolbar:false">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 id="H">H1</h1>
            <h2 id="props-example-toString">{props["example"]?.toString()}</h2>
            {
                props["show"] ? <p>show</p> : >
            }
            <h1 id="H">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 



<p>我们的东西又坏了...</p>

<p>好的,这是因为在上面的情况下子元素可以是嵌套数组,我们需要将它们展平。</p>

<p>但这还不够,呃,我们的 createDom 只能识别字符串或元素,不能识别整数,所以,我们需要将数字串起来。<br>
</p>

<pre class="brush:php;toolbar:false">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中文网其他相关文章!

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
JavaScript的角色:使网络交互和动态JavaScript的角色:使网络交互和动态Apr 24, 2025 am 12:12 AM

JavaScript是现代网站的核心,因为它增强了网页的交互性和动态性。1)它允许在不刷新页面的情况下改变内容,2)通过DOMAPI操作网页,3)支持复杂的交互效果如动画和拖放,4)优化性能和最佳实践提高用户体验。

C和JavaScript:连接解释C和JavaScript:连接解释Apr 23, 2025 am 12:07 AM

C 和JavaScript通过WebAssembly实现互操作性。1)C 代码编译成WebAssembly模块,引入到JavaScript环境中,增强计算能力。2)在游戏开发中,C 处理物理引擎和图形渲染,JavaScript负责游戏逻辑和用户界面。

从网站到应用程序:JavaScript的不同应用从网站到应用程序:JavaScript的不同应用Apr 22, 2025 am 12:02 AM

JavaScript在网站、移动应用、桌面应用和服务器端编程中均有广泛应用。1)在网站开发中,JavaScript与HTML、CSS一起操作DOM,实现动态效果,并支持如jQuery、React等框架。2)通过ReactNative和Ionic,JavaScript用于开发跨平台移动应用。3)Electron框架使JavaScript能构建桌面应用。4)Node.js让JavaScript在服务器端运行,支持高并发请求。

Python vs. JavaScript:比较用例和应用程序Python vs. JavaScript:比较用例和应用程序Apr 21, 2025 am 12:01 AM

Python更适合数据科学和自动化,JavaScript更适合前端和全栈开发。1.Python在数据科学和机器学习中表现出色,使用NumPy、Pandas等库进行数据处理和建模。2.Python在自动化和脚本编写方面简洁高效。3.JavaScript在前端开发中不可或缺,用于构建动态网页和单页面应用。4.JavaScript通过Node.js在后端开发中发挥作用,支持全栈开发。

C/C在JavaScript口译员和编译器中的作用C/C在JavaScript口译员和编译器中的作用Apr 20, 2025 am 12:01 AM

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。 1)C 用于解析JavaScript源码并生成抽象语法树。 2)C 负责生成和执行字节码。 3)C 实现JIT编译器,在运行时优化和编译热点代码,显着提高JavaScript的执行效率。

JavaScript在行动中:现实世界中的示例和项目JavaScript在行动中:现实世界中的示例和项目Apr 19, 2025 am 12:13 AM

JavaScript在现实世界中的应用包括前端和后端开发。1)通过构建TODO列表应用展示前端应用,涉及DOM操作和事件处理。2)通过Node.js和Express构建RESTfulAPI展示后端应用。

JavaScript和Web:核心功能和用例JavaScript和Web:核心功能和用例Apr 18, 2025 am 12:19 AM

JavaScript在Web开发中的主要用途包括客户端交互、表单验证和异步通信。1)通过DOM操作实现动态内容更新和用户交互;2)在用户提交数据前进行客户端验证,提高用户体验;3)通过AJAX技术实现与服务器的无刷新通信。

了解JavaScript引擎:实施详细信息了解JavaScript引擎:实施详细信息Apr 17, 2025 am 12:05 AM

理解JavaScript引擎内部工作原理对开发者重要,因为它能帮助编写更高效的代码并理解性能瓶颈和优化策略。1)引擎的工作流程包括解析、编译和执行三个阶段;2)执行过程中,引擎会进行动态优化,如内联缓存和隐藏类;3)最佳实践包括避免全局变量、优化循环、使用const和let,以及避免过度使用闭包。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

Dreamweaver Mac版

Dreamweaver Mac版

视觉化网页开发工具

MinGW - 适用于 Windows 的极简 GNU

MinGW - 适用于 Windows 的极简 GNU

这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。