本教程基于本教程,但使用了 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中文网其他相关文章!

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

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

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

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

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

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

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

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


热AI工具

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

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

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

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

热门文章

热工具

EditPlus 中文破解版
体积小,语法高亮,不支持代码提示功能

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

Dreamweaver Mac版
视觉化网页开发工具

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