从头创建您自己的vuei .js——第3部分(构建VDOM)
如果你喜欢这篇文章,你可能也会喜欢我的推特。如果你很好奇,可以看看我的Twitter简介。????
这是“从头创建您自己的vuei .js”系列文章的第三部分,在这里我将教您如何创建响应式框架(比如vuei .js)的基础知识。要阅读这篇博客文章,我建议您阅读本系列的第一部分和第二部分。
这篇文章一开始可能很长,但可能不像它看起来那么专业。它描述了代码的每一步,这就是为什么它看起来很复杂的原因。但容忍我,所有这一切将在最后????完美的意义
在本系列的第2部分中,我们了解了虚拟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的意思是,将其附加到任何给定的容器。这个节点可以是原始容器(在我们的示例中是#app-div),也可以是另一个虚拟节点(例如,在
这将是一个递归函数,因为我们必须遍历所有节点的子节点并将其挂载到各自的容器中。
我们的挂载函数看起来像这样:
function mount(vnode, container) { ... }
我们需要创建一个DOM元素
const el = (vnode.el = document.createElement(vnode.tag))
2)我们需要将属性(道具)设置为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
})
}
在这段代码的第二部分中可以看到,使用相同的挂载函数挂载子线程。这将递归地继续,直到只剩下“文本节点”。然后递归停止。
作为挂载函数的最后一部分,我们需要将创建的DOM元素添加到相应的容器中:
container.appendChild(el)
在卸载函数中,我们从实际DOM中的父节点中删除给定的虚拟节点。该函数仅将虚拟节点作为参数。
function unmount(vnode) {
vnode.el.parentNode.removeChild(vnode.el)
}
这意味着使用两个虚拟节点,比较它们,并找出它们之间的区别。
到目前为止,这是我们将为虚拟DOM编写的最广泛的函数,不过请耐心听我说。
const el = (n2.el = n1.el)
如果节点具有不同的标记,我们可以假设内容完全不同,只需完全替换节点即可。我们通过挂载新节点并卸载旧节点来实现这一点。
if (n1.tag !== n2.tag) {
// Replace node
mount(n2, el.parentNode)
unmount(n1)
} else {
// Nodes have different tags
}
如果节点具有相同的标签;但是,它可以表示两种不同的意思:
新节点有字符串子节点
新节点有一组子节点
在本例中,我们将继续使用“children”(实际上只是一个字符串)替换元素的textContent。
...
// Nodes have different tags
if (typeof n2.children === 'string') {
el.textContent = n2.children
}
...
在这种情况下,我们必须检查孩子们之间的差异。有三种情况:
子结点的长度是一样的
旧节点比新节点有更多的子节点。在这种情况下,我们需要从DOM中删除“exceed”子节点
新节点比旧节点有更多的子节点。在本例中,我们需要向DOM添加额外的子元素。
所以首先,我们需要确定子节点的公共长度,或者换句话说,每个节点的子节点的最小值:
const c1 = n1.children
const c2 = n2.children
const commonLength = Math.min(c1.length, c2.length)
对于从点4)开始的每一种情况,我们需要修补节点共有的子节点:
for (let i = 0; i < commonLength; i++) {
patch(c1[i], c2[i])
}
在长度相等的情况下,这已经是它了。没有什么可做的了。
如果新节点的子节点比旧节点少,则需要从DOM中删除这些子节点。我们已经为此编写了unmount函数,所以现在我们需要遍历额外的子节点并卸载它们:
if (c1.length > c2.length) {
c1.slice(c2.length).forEach(child => {
unmount(child)
})
}
如果新节点比旧节点有更多的子节点,我们需要将它们添加到DOM中。我们已经为此编写了mount函数。现在我们需要遍历额外的子节点并挂载它们:
else if (c2.length > c1.length) {
c2.slice(c1.length).forEach(child => {
mount(child, el)
})
}
就是这样。我们发现了节点之间的每个差异,并相应地修正了DOM。这个解决方案没有实现的是属性的修补。这会使博客文章更长,而且会错过要点。
我们的虚拟DOM引擎已经准备好了。为了演示它,我们可以创建一些节点并渲染它们。让我们假设我们想要以下HTML结构:
Hello World ????
Thanks for reading the marc.dev blog ????
const node1 = h('div', { class: 'container' }, [
h('div', null, 'X'),
h('span', null, 'hello'),
h('span', null, 'world'),
])
我们想挂载新创建的DOM。在哪里?到文件最顶部的#app-div:
mount(node1, document.getElementById('app'))
结果应该是这样的:
现在,我们可以创建第二个节点,并对其进行一些更改。让我们添加一些节点,这样结果将是这样的:
Hello Dev ????
Thanks for reading the marc.dev blog
下面是创建该节点的代码:
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;',
},
[],
),
])
如您所见,我们添加了一些节点,还更改了一个节点。
我们希望用第二个节点替换第一个节点,因此不使用mount。我们要做的是找出两者之间的区别,进行更改,然后渲染。所以我们修补它:
setTimeout(() => {
patch(node1, node2)
}, 3000)
我在这里添加了一个超时,以便您可以看到代码DOM的变化。如果没有,就只能看到呈现的新VDOM。
就是这样!我们有一个非常基本的DOM引擎版本,它让我们:
创建虚拟节点
将虚拟节点挂载到DOM
从DOM中删除虚拟节点
找出两个虚拟节点之间的差异,并相应地更新DOM
你可以在我为你准备的Github要点中找到我们在这篇文章中做的代码。如果你只是想玩玩它,我还创建了一个Codepen,你可以这样做。