作者: Marc Backes 翻译:张全玉
从头开始创建自己的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