从头开始创建自己的Vue.js-第3部分(构建VDOM)

425 阅读7分钟

作者: Marc Backes 翻译:张全玉

N8TthV.jpg

从头开始创建自己的Vue.js-第3部分(构建VDOM

这是从头开始创建Vue.js的系列文章的第三部分,我将在这里教您如何创建诸如Vue.js之类的反应式框架的基础。 要关注此博客文章,我建议您阅读本系列的第一部分和第二部分。

这篇文章起初可能很长,但可能不像它看起来那样技术性。 它描述了代码的每个步骤,这就是为什么它看起来非常复杂的原因。 但是,请忍受,所有这些最终都将完全合情合理😊。

路线图🚘

1.介绍 2.虚拟DOM基础 3.实施虚拟DOM和渲染(本文) 4.构建反应式 5.总结

构建虚拟DOM

骨架

在本系列的第二部分中,我们了解了虚拟DOM的工作原理。 您可以从该要点的最后一点复制了VDOM骨架。 我们使用该代码进行后续操作。 您还将在此找到VDOM引擎的最终版本。 我还创建了一个Codepen,您可以在其中使用它。

创建一个虚拟节点

因此,要创建虚拟节点,我们需要标记,属性和子代。 因此,我们的函数如下所示:

function h(tag, props, children){ ... }

(在Vue中,用于创建虚拟节点的函数命名为h,因此我们将在此处调用它。)

在此函数中,我们需要具有以下结构的JavaScript对象。

{    tag: 'div',    props: {        class: 'container'    },    children: ...}

为此,我们需要将标签,属性和子节点参数包装在一个对象中并返回:

function h(tag, props, children) {    return {        tag,        props,        children,    }}

虚拟节点创建已经完成了。

将虚拟节点挂载到DOM

我将虚拟节点挂载到DOM的意思是将其附加到任何给定的容器。 该节点可以是原始容器(在我们的示例中为#app-div),也可以是将其挂载在其上的另一个虚拟节点(例如,在<div>中安装一个<span>)。

这将是一个递归函数,因为我们将必须遍历所有节点的子节点并将其挂载到相应的容器中。

我们的挂载函数将如下所示:

function mount(vnode, container) { ... }

(1)我们需要创建一个DOM元素

const el = (vnode.el = document.createElement(vnode.tag))

(2)我们需要将属性(props)设置为DOM元素的属性: 我们通过迭代它们来做到这一点,例如:

for (const key in vnode.props) {    el.setAttribute(key, vnode.props[key])}

(3)我们需要将子级挂载在这个元素上 请记住,有两种类型的孩子:

  • 一个简单的文字

  • 虚拟节点数组 我们都处理:

// Children is a string/textif (typeof vnode.children === 'string') {    el.textContent = vnode.children}​// Chilren are virtual nodeselse {    vnode.children.forEach(child => {        mount(child, el) // Recursively mount the children    })}

如您在该代码的第二部分中所见,正在使用相同的挂载函数挂载子级。 递归地继续进行,直到只剩下“文本节点”为止。 然后递归停止。

作为此挂载函数的最后一部分,我们需要将创建的DOM元素添加到相应的容器中:

container.appendChild(el)

从DOM卸载虚拟节点

在卸载函数中,我们从真实DOM中的父节点中移除了给定的虚拟节点。 该函数仅将虚拟节点作为参数。

function unmount(vnode) {    vnode.el.parentNode.removeChild(vnode.el)}

修补虚拟节点

这意味着获取两个虚拟节点,对其进行比较,然后找出它们之间的区别。

这是迄今为止我们将为虚拟DOM编写的最广泛的功能,但是请耐心等待。

(1)分配我们将要使用的DOM元素

const el = (n2.el = n1.el)

(2)检查节点是否具有不同的标记 如果节点具有不同的标签,我们可以假定内容完全不同,而我们将完全替换节点。 我们通过安装新节点并卸载旧节点来做到这一点。

if (n1.tag !== n2.tag) {    // Replace node    mount(n2, el.parentNode)    unmount(n1)} else {    // Nodes have different tags}

如果节点具有相同标记;这可能意味着两件事:

  • 新节点具有字符串子代

  • 新节点具有数组子代

(3)节点具有字符串子代的情况

在这种情况下,我们继续将元素的textContent替换为“ children”(实际上只是一个字符串)。

...    // Nodes have different tags    if (typeof n2.children === 'string') {        el.textContent = n2.children    }...

(4)如果节点具有子节点数组 在这种情况下,我们必须检查子节点之间的差异。 有以下三种情况:

子节点的长度是一样的 旧节点比新节点具有更多的子节点。 在这种情况下,我们需要从DOM中删除“超出”子级 新节点比旧节点具有更多的子节点。 在这种情况下,我们需要向DOM添加其他子代。 因此,首先,我们需要确定子节点的公共长度,或者换句话说,每个节点具有的最小子节点数:

const c1 = n1.childrenconst c2 = n2.childrenconst commonLength = Math.min(c1.length, c2.length)

(5)修补公共的子节点

对于第(4)点的每种情况,我们需要修补节点共有的子节点:

for (let i = 0; i < commonLength; i++) {    patch(c1[i], c2[i])}

在长度相等的情况下,已经足够了。 没什么可做的。

(6)从DOM中删除不需要的孩子 如果新节点的子节点数少于旧节点,则需要从DOM中删除这些子节点。 我们已经为此编写了unmount函数,因此现在我们需要遍历额外的子项并将其卸载:

if (c1.length > c2.length) {    c1.slice(c2.length).forEach(child => {        unmount(child)    })}

(7)将其他子代添加到DOM 如果新节点的子节点数多于旧节点,则需要将其添加到DOM。 我们也已经为此写了mount函数。 现在,我们需要遍历其他子项并挂载它们:

else if (c2.length > c1.length) {    c2.slice(c1.length).forEach(child => {        mount(child, el)    })}

我们发现了节点之间的每个差异,并相应地更正了DOM。 但是,此解决方案未实现的是属性修补。 这将使博客文章更长,并且遗漏了重点。

在真实DOM中渲染虚拟树

我们的虚拟DOM引擎现已准备就绪。 为了演示它,我们可以创建一些节点并进行渲染。 假设我们需要以下HTML结构:

<div class="container">    <h1>Hello World 🌍</h1>    <p>Thanks for reading the marc.dev blog 😊</p></div>

(1)用h函数创建虚拟节点

const node1 = h('div', { class: 'container' }, [    h('div', null, 'X'),    h('span', null, 'hello'),    h('span', null, 'world'),])

(2)将节点挂载到DOM

我们要挂载新创建的DOM,在文件顶部的#app-div

mount(node1, document.getElementById('app'))

结果应如下所示:


(3)创建第二个虚拟节点

现在,我们可以创建第二个节点,并对其进行一些更改。让我们添加一些节点,以便结果如下:

<div class="container">    <h1>Hello Dev 💻</h1>    <p><span>Thanks for reading the </span><a href="https://marc.dev">marc.dev</a><span> blog</span></p>    <img src="https://media.giphy.com/media/26gsjCZpPolPr3sBy/giphy.gif" style="width: 350px; border-radius: 0.5rem;" /></div>

这是用于创建该节点的代码:

const node2 = h('div', { class: 'container' }, [    h('h1', null, 'Hello Dev 💻'),    h('p', null, [        h('span', null, 'Thanks for reading the '),        h('a', { href: 'https://marc.dev' }, 'marc.dev'),        h('span', null, ' blog'),    ]),    h(        'img',        {            src: 'https://media.giphy.com/media/26gsjCZpPolPr3sBy/giphy.gif',            style: 'width: 350px; border-radius: 0.5rem;',        },        [],    ),])

如您所见,我们添加了一些节点,还更改了一个节点。

(4)渲染第二个节点 我们要用第二个节点替换第一个节点,因此我们不使用mount。 我们要做的是找出两者之间的差异,进行更改,然后进行渲染。 因此,我们对其进行了修补:

setTimeout(() => {    patch(node1, node2)}, 3000)

我在此处添加了超时,因此您可以看到代码DOM发生了变化。 如果没有,您将只能看到呈现的新VDOM。

总结

我们有一个非常基本的DOM引擎版本,可以使我们:

  • 创建虚拟节点

  • 将虚拟节点挂载到DOM

  • 从DOM中删除虚拟节点

  • 查找两个虚拟节点之间的差异并相应地更新DOM