从零开始创建你自己的 Vue.js - 第 3 部分(构建 VDOM)
路线图🚘
构建虚拟DOM
概括
从零开始创建你自己的 Vue.js - 第 3 部分(构建 VDOM)
如果你喜欢这篇文章,很可能也会喜欢我发的推文。如果你感兴趣,可以看看我的推特主页。🚀
这是“从零开始创建你自己的 Vue.js”系列文章的第三部分,我将教你如何创建像 Vue.js 这样的响应式框架的基础知识。为了更好地理解本文,我建议你先阅读本系列的第一部分和第二部分。
这篇文章开头可能有点长,但其实并没有看起来那么技术性。它详细描述了代码的每一步,所以看起来比较复杂。不过请耐心看完,最后一切都会变得清晰明了😊
路线图🚘
- 介绍
- 虚拟 DOM 基础知识
- 实现虚拟 DOM 和渲染(本文)
- 构建反应能力
- 把所有东西整合起来
构建虚拟DOM
骨架
在本系列的第二部分,我们学习了虚拟 DOM 的基础知识。你可以从这篇 gist的最后一点复制 VDOM 框架。我们将使用该代码进行后续讲解。你还可以在那里找到 VDOM 引擎的最终版本。我还创建了一个Codepen 示例,你可以在那里进行体验。
创建虚拟节点
因此,要创建一个虚拟节点,我们需要标签、属性和子节点。所以,我们的函数看起来大致如下:
function h(tag, props, children){ ... }
(在 Vue 中,创建虚拟节点的函数名为 `createVirtualNodes` h,所以我们在这里也这样称呼它。)
在这个函数中,我们需要一个具有以下结构的 JavaScript 对象。
{
tag: 'div',
props: {
class: 'container'
},
children: ...
}
为了实现这一点,我们需要将标签、属性和子节点参数包装在一个对象中并返回该对象:
function h(tag, props, children) {
return {
tag,
props,
children,
}
}
虚拟节点创建就完成了。
将虚拟节点挂载到 DOM
我所说的将虚拟节点挂载到 DOM 上,是指将其添加到任何给定的容器中。这个节点可以是原始容器(在我们的例子中是#app-div),也可以是另一个虚拟节点,它将被挂载到该虚拟节点上(例如,将一个 挂载到一个<span>内部<div>)。
这将是一个递归函数,因为我们需要遍历所有节点的子节点,并将它们挂载到相应的容器中。
我们的mount函数看起来会是这样:
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/text
if (typeof vnode.children === 'string') {
el.textContent = vnode.children
}
// Chilren are virtual nodes
else {
vnode.children.forEach(child => {
mount(child, el) // Recursively mount the children
})
}
如您在代码的第二部分所见,子节点使用相同的mount函数进行挂载。这个过程会递归地持续进行,直到只剩下“文本节点”为止。然后递归停止。
作为挂载函数的最后一部分,我们需要将创建的 DOM 元素添加到相应的容器中:
container.appendChild(el)
从 DOM 中卸载虚拟节点。
该unmount函数会将指定的虚拟节点从其在真实 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元素的“子元素”(实际上只是一个字符串)替换为“子元素”即可。
...
// Nodes have different tags
if (typeof n2.children === 'string') {
el.textContent = n2.children
}
...
4) 如果节点有一个子节点数组
在这种情况下,我们需要检查孩子们之间的差异。有三种情况:
- 孩子们的身高相同
- 旧节点比新节点拥有更多的子节点。在这种情况下,我们需要从 DOM 中移除超出限制的子节点。
- 新节点比旧节点拥有更多子节点。在这种情况下,我们需要向 DOM 添加更多子节点。
首先,我们需要确定子节点的共同长度,或者换句话说,确定每个节点子节点数的最小值:
const c1 = n1.children
const c2 = n2.children
const commonLength = Math.min(c1.length, c2.length)
5)修补普通儿童
对于每个点4),我们需要找到patch节点共有的子节点:
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相应的函数。现在我们需要遍历这些新增的子节点并将它们挂载到 DOM 中:
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.`。我们想做的是找出这两个节点之间的差异,进行修改,然后渲染它。所以我们patch这样做:
setTimeout(() => {
patch(node1, node2)
}, 3000)
我在这里添加了一个超时,这样你就能看到代码 DOM 的变化。否则,你只会看到渲染出来的新虚拟 DOM。
概括
就是这样!我们有一个非常基础的 DOM 引擎版本,它可以让我们:
- 创建虚拟节点
- 将虚拟节点挂载到 DOM。
- 从DOM中移除虚拟节点
- 找出两个虚拟节点之间的差异,并相应地更新 DOM。
你可以在我为你准备的 GitHub Gist上找到本文中使用的代码。如果你只是想自己尝试一下,我还创建了一个Codepen 示例,方便你进行操作。
原封面照片由Joshua Earle拍摄于 Unplash,由Marc Backes编辑。
文章来源:https://dev.to/themarcba/create-your-own-vue-js-from-scratch-part-2-building-the-vdom-3bp2
