1.虚拟DOM介绍
虚拟DOM是MVVM框架的灵魂,他相当于存在于真实DOM对象和数据之间的缓存容器,虚拟DOM的概念之所以提出,主要原因在于现今浏览器直接操作DOM对象实现视图更新的开销仍然是巨大的。
随着Web技术的飞速发展,浏览器陆续支持更多新的特色能力和移动端操作能力,但是更新视图依然需要使用DOM操作的API来实现,随着计算机硬件的更新换代,现今Web项目所运行的计算机硬件性能足够运行各种大型的网页应用,但在此等优越的条件下,通过WebAPI直接操作DOM对象来更新视图的代价仍然是非常庞大的。
1.1 操作真实DOM的开销在哪里
众所周知,在HTML文件中通过JavaScript操作DOM对象是同步机制的API,所以他遵循顺序结构和代码的有序运行。也就是说当网页中运行N行代码时,如果其中有M行代码获取了DOM对象并操作指定DOM元素的内容或样式,在运行到任何一行DOM操作代码时,由于同步运行的机制,浏览器无法得知后续还有多少次DOM操作,所以在执行下一行代码前浏览器必须将当前行的DOM操作直接更新。
举个例子,当网页有4个输入框组件存在时,开发者若想要将4个输入框的内容分别修改成1、2、3、4。其需要执行的操作,代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<input id="a" type="text">
<input id="b" type="text">
<input id="c" type="text">
<input id="d" type="text">
<script type="text/javascript">
// 获取4个输入框对象
let ipt1 = document.querySelector('#a')
let ipt2 = document.querySelector('#b')
let ipt3 = document.querySelector('#c')
let ipt4 = document.querySelector('#d')
// 修改4个输入框的内容
ipt1.value = 1
ipt2.value = 2
ipt3.value = 3
ipt4.value = 4
</script>
</body>
</html>
运行代码后会发现,网页中4个输入框的内容直接变成1、2、3、4。看似没问题的案例实际运行的步骤并不是所见的结果。由于JavaScript同步代码顺序运行,DOM操作也是同步触发的,首先在获取4个输入框对象时就会触发4次对DOM树的遍历,来碰撞相匹配的元素对象,再次是当每个DOM对象通过value属性赋值的时候,会直接触发一次视图更新,所以实际上网页是通过4次遍历和4次DOM更新来实现最终展示结果的。编写此代码的实际目的只不过是想要最终渲染的结果,所以这显然是浪费性能的。接下来,通过实际更改了解DOM更新的步骤,将原案例插入部分新内容,代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<input id="a" type="text">
<input id="b" type="text">
<input id="c" type="text">
<input id="d" type="text">
<script type="text/javascript">
// 展示当前4个输入框中的value的值
function showDOM(){
console.log(ipt1.value)
console.log(ipt2.value)
console.log(ipt3.value)
console.log(ipt4.value)
}
// 获取4个输入框对象
let ipt1 = document.querySelector('#a')
let ipt2 = document.querySelector('#b')
let ipt3 = document.querySelector('#c')
let ipt4 = document.querySelector('#d')
// 修改4个输入框的内容
ipt1.value = 1
showDOM()
ipt2.value = 2
showDOM()
ipt3.value = 3
showDOM()
ipt4.value = 4
showDOM()
</script>
</body>
</html>
更改代码后,运行代码时控制台会输出如下内容,如图。
阅读控制台会发现,当对第一个输入框设置value时,剩下3个输入框此时的value属性还没有实际值,这意味着只有在最后一个输入框的value设置完毕后,网页中的4个输入框才能完整的展示1、2、3、4。这就意味着实际DOM需要更新4次才能最终展示结果,只不过肉眼无法看见4次更新,就算不考虑4次更新的情况下获取4个输入框对象仍然需要从HTML页面的根结点开始遍历4次DOM树才能获取4个输入框的引用对象。
1.2 什么是虚拟DOM
众所周知,浏览器在通过HTML解析器解析HTML标记语言时,会将嵌套结构的标记对象映射成一棵极大的DOM树,树的每一个节点都包含当前实际视图中指定对象的结构信息、样式信息以及事件信息等大量的属性和方法。虚拟DOM与真实DOM一样也是一颗树,不同的是虚拟DOM不直接代表真实的视图结构。可以认为虚拟DOM是真实DOM树的简化版,节点数量与真实DOM完全一致,每个节点挂载的属性很少。虚拟DOM是纯粹的JavaScript对象,所以他保存在JavaScript的直接堆区中,所以访问虚拟DOM对象的指定节点速度会比真实DOM更快。由于虚拟DOM是纯粹的JavaScript对象,所以更新该对象也不需要GUI线程处理视图更新,基于这个特点,可以完美的规避DOM操作API执行一次就触发一次视图更新的问题。
描述了真实DOM树和虚拟DOM树的本质区别后,通过DOM操作流程来对比一下真实DOM和虚拟DOM更新的机制。首先图形化了解一下真实DOM实际更新视图的流程,如图。
根据图中的描述得知,DOM对象的属性一旦变更就会触发一次视图更新,所以实际两行操作是两次处理,在DOM树中找到指定的input标记也需要至少从body开始进行递归遍历。接下来了解一下集合了虚拟DOM树后视图的更新流程,如图。
根据两张图可以直观的识别到虚拟DOM操作视图变更的优势,他可以在获取DOM对象和视图更新两个层面同时降低开销,带来的弊端是需要在JavaScript的堆区创造一个和真实视图完全一样结构的DOM树,不过虚拟DOM树的节点和真实DOM树的节点体积相比要小得多,接下来以一个简单的虚拟DOM对象和真实DOM对象在控制台的对比图为例查看两者的区别,如图。
2.创建一颗虚拟DOM树
前面的章节已经介绍了什么是虚拟DOM数据,若想要实现MVVM架构的数据和视图映射关系,首先需要在代码环境中实现一个方法,该方法可以根据描述的标签节点和属性自动构建一颗JavaScript的虚拟DOM树,虚拟DOM树本身的每一个节点还应具备该元素的真实DOM对象,在非渲染步骤执行前真实DOM对象不参与网页渲染,所以虚拟DOM叶子结点上就算存在他对应的真实DOM对象,也不会影响渲染性能,同时其真实DOM对象属性保存的仅仅是该对象的引用地址,所以并不会增加虚拟DOM树的体积。
接下来在一个空的HTML文件中创建对象V,并且在V对象中创建函数h,代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript">
//虚拟DOM总对象
let V = {
//创建虚拟DOM节点的函数
/**
* @param {Object} tagName 标签名
* @param {Object} options 标签属性
* @param {Object} children 标签子元素
*/
h(tagName,options,children){
//...具体实现
}
}
</script>
</body>
</html>
接下来完成h函数的具体实现,代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript">
let V = {
/**
* @param {Object} tagName 标签名
* @param {Object} options 标签属性
* @param {Object} children 标签子元素
*/
h(tagName,options,children){
// 创建虚拟DOM节点
let VNode = {}
// 判断动态参数如果只有一个时
if(arguments.length == 1){
// 如果参数为非对象代表h函数创建了纯Text节点
if(!(arguments[0] instanceof Object)){
// 初始化纯文本节点和其真实DOM对象
VNode = {
type:'text',
content:tagName+'',
$el:document.createTextNode(tagName+'')
}
}else{
// 若非纯文本节点统一认为是虚拟DOM对象
VNode = arguments[0]
}
}else{
// 当正常设置参数时初始化该虚拟节点的真实DOM对象
let element = document.createElement(tagName)
// 初始化行内样式
this.initStyles(element,options)
// 初始化元素默认属性
this.initAttrs(element,options)
// 初始化事件系统
this.initEvents(element,options)
// 初始化id和class
this.initIdClass(element,options)
let c
// 处理虚拟DOM子节点
if(children instanceof Array){
// 将每个子节点采用h函数重新初始化
c = children.map(item => {
return this.h(item)
})
}else{
//当子节点为非数组时
if(children){
c = [this.h(children)]
}
}
// 将虚拟DOM节点信息和其真实DOM对象进行处理
VNode = {
type:tagName,
...options,
$el:element,
children:c
}
}
// 返回创建的节点
return VNode
},
// 初始化行内样式
initStyles(element,options){
for(let key in options.style){
element.style[key] = options.style[key]
}
},
// 初始化标签自带属性
initAttrs(element,options){
for(let key in options.attrs){
element[key] = options.attrs[key]
}
},
// 初始化标签的事件系统
initEvents(element,options){
for(let key in options.on){
element.addEventListener(key,options.on[key])
}
},
// 初始化id和class
initIdClass(element,options){
if(options.id){
element.id = options.id
}
if(options.className){
element.className = options.className
}
}
}
</script>
</body>
</html>
定义完该虚拟树后便可以直接通过V.h()来动态创建虚拟DOM节点以及虚拟DOM树,可以通过JavaScript函数模拟React的方式创建一套虚拟DOM节点,代码如下:
var count = 0
var type = 'text'
function Index(){
return V.h('div',{
id:'d',
className:'p-d',
attrs:{
'data-name':'hahah'
},
on:{
click(){
console.log('点击div的点击事件')
}
}
},[
//将count动态追加到虚拟DOM树中
count,
V.h('input',{
attrs:{
value:count,
type:type //将type动态绑定到输入框的类型中
},
on:{
input(e){
console.log('输入框的输入事件')
}
}
}),
V.h('select',{
on:{
input(e){
console.log('下拉列表的选择事件')
}
}
},[
V.h('option',{
attrs:{
value:'radio'
}
},'单选按钮'),
V.h('option',{
attrs:{
value:'checkbox'
}
},'多选按钮'),
V.h('option',{
attrs:{
value:'password'
}
},'密码框'),
V.h('option',{
attrs:{
value:'text'
}
},'输入框')
]),
V.h('button',{
id:'btn',
attrs:{
type:'submit'
},
on:{
click(){
console.log('按钮的点击事件')
}
}
},'你好')
])
}
let vdom = Index()
console.log(vdom)
根据Index函数的返回结构创造的虚拟DOM树,在控制台中输出的结果,如图。
此时网页中无法看到虚拟DOM所代表的真实视图,接下来创建createElement方法来将初始的虚拟DOM对象转化成真实DOM树并渲染到网页中。在V对象中追加如下内容,代码如下:
let V = {
//创建真实DOM树的函数
/**
* @param {Object} el 选择器
* @param {Object} vdom 虚拟DOM树
*/
createElement(el,vdom){
// 获取真实DOM树的渲染容器
let elem = document.querySelector(el)
// 创建递归渲染函数
function appendChild(elem,children){
// 递归将元素节点插入到根节点中
children.forEach(item => {
// 将当前节点的子元素设置为虚拟DOM节点对应的DOM对象
elem.appendChild(item.$el)
// 如果该节点有后代元素便执行递归
if(item.children){
appendChild(item.$el,item.children)
}
})
}
appendChild(elem,[vdom])
}
}
通过V.createElement可以将创建好的虚拟DOM树直接生成真实DOM结果映射到视图中,代码如下:
var count = 0
var type = 'text'
function Index(){
return V.h('div',{
id:'d',
className:'p-d',
attrs:{
'data-name':'hahah'
},
on:{
click(){
console.log('点击div的点击事件')
}
}
},[
//将count动态追加到虚拟DOM树中
count,
V.h('input',{
attrs:{
value:count,
type:type //将type动态绑定到输入框的类型中
},
on:{
input(e){
console.log('输入框的输入事件')
}
}
}),
V.h('select',{
on:{
input(e){
console.log('下拉列表的选择事件')
}
}
},[
V.h('option',{
attrs:{
value:'radio'
}
},'单选按钮'),
V.h('option',{
attrs:{
value:'checkbox'
}
},'多选按钮'),
V.h('option',{
attrs:{
value:'password'
}
},'密码框'),
V.h('option',{
attrs:{
value:'text'
}
},'输入框')
]),
V.h('button',{
id:'btn',
attrs:{
type:'submit'
},
on:{
click(){
console.log('按钮的点击事件')
}
}
},'你好')
])
}
let vdom = Index()
//将虚拟DOM树创建为真实DOM对象
V.createElement('#app',vdom)
创建后的真实视图会出现渲染的视图结构并且在每个虚拟DOM节点中定义的on事件都可以正常工作,如图。
3.实现DIFF和PATCH
前面的案例已经可以通过JavaScript创建虚拟DOM树以及其对应的真实DOM渲染了,在此基础上再进一步就可以完成Vue和React等MVVM的底层架构搭建,其核心点在于DIFF和PATCH。
DIFF代表差异算法,由于虚拟DOM框架的核心是将数据与虚拟DOM关联,通过以数据为核心驱动页面渲染,每次更新视图需要先将之前的N次修改映射到虚拟DOM树以及其对应的真实DOM节点上,再将修改后的虚拟DOM树和修改前的虚拟DOM树做差异比较算法记录差异点。
PATCH代表补丁,即在DIFF的过程中遇到两次虚拟DOM树的差异点时则对差异点进行补丁处理并更新实际对应的真实视图。
通常在虚拟DOM的DIFF中采用同层比较算法,用以从根节点递归的比较变化前和变化后的虚拟DOM树的每个节点数据,如果两者比较的结果相同则进入深层递归继续比较其标签上的属性和其后代元素。如果新旧两颗树相同层级的相同位置节点为不同元素则直接使用新节点替换旧节点。同层比较算法的具体描述,如图。
接下来通过代码编程的方式在原始虚拟DOM对象中实现DIFF+PATCH,最终实现通过纯JavaScript数据操作更新视图。在V对象中追加如下函数,代码如下:
let V = {
/**
* @param {Object} OldVNode 旧的虚拟DOM节点
* @param {Object} VNode 新的虚拟DOM节点
*/
patch(OldVNode,VNode){
// 判断同层节点是否相同
if(this.sameVNode(OldVNode,VNode)){
// 相同节点进入属性PATCH
this.patchAttrs(OldVNode,VNode)
// 若为文本节点则进入文本PATCH
this.patchText(OldVNode,VNode)
// 对节后代深层递归重复PATCH
if(OldVNode.children){
OldVNode.children.forEach((item,index) => {
this.patch(item,VNode.children[index])
})
}
}else{
// 若前后节点不同执行替换
this.replaceVNode(OldVNode,VNode)
}
},
// 属性PATCH
patchAttrs(OldVNode,VNode){
// 若元素相同则将新虚拟DOM对象的真实DOM节点引用为当前节点
VNode.$el = OldVNode.$el
// 遍历虚拟DOM的属性将变更部分更新到真实DOM中
for(let key in VNode.attrs){
if(VNode.$el[key]!=VNode.attrs[key]){
VNode.$el[key] = VNode.attrs[key]
}
}
},
// 文本PATCH
patchText(OldVNode,VNode){
if(OldVNode.type == 'text'){
// 元素相同时文本节点直接挂载原始DOM对象的引用
VNode.$el = OldVNode.$el
// 若文本内容不同则更新
if(VNode.$el.textContent!=VNode.content){
VNode.$el.textContent = VNode.content
}
}
},
// 替换旧节点的DOM对象
replaceVNode(OldVNode,VNode){
OldVNode.$el.parent.insertBefore(OldVNode.$el,VNode.$el)
OldVNode.$el.parent.removeChild(OldVNode.$el)
},
// 判断是否为相同节点
sameVNode(OldVNode,VNode){
if(OldVNode.key == VNode.key){
return true
}else if(OldVNode.type == VNode.type){
return true
}else{
return false
}
}
}
为更加贴近以数据为核心驱动页面渲染,使用Proxy对象实现属性赋值的监听,在V对象中继续追加函数,代码如下:
let V = {
// 定义响应式数据定义对象
reactive(data){
// 通过代理对象返回可响应式的数据对象
return new Proxy(data,{
// 通过防抖算法实现多次更改数据只触发一次更新
set:debounce((target,property,value) =>{
console.log('视图更新1次')
// 获取更改前后的虚拟DOM对象
let newVDOM = Index()
let oldVDOM = vdom
// 通过patch执行DIFF的差异更新
this.patch(oldVDOM,newVDOM)
//将全局的vdom替换为本次更新好的vdom
vdom = newVDOM
// console.log(target)
},0),
get(target,property){
return target[property]
}
})
}
}
//防抖函数
function debounce(fn,timer){
let timeout
return function(target,property,value){
target[property] = value
if(timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
fn(target,property,value)
},timer)
}
}
完成响应式数据对象reactive的定义后进行完整代码的实现,完整代码包括虚拟DOM的创建和视图更新的处理机制,本案例不管实现了DIFF和PATCH,还利用了防抖的逻辑模拟了异步更新视图队列的效果,实现多次变更虚拟DOM树,最终只会触发一次PATCH从而规避同步操作DOM导致的变更一次DOM属性视图更新一次的开销问题,从而实现同时修改多个响应式数据变量只有一次真实DOM处理,代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript">
let V = {
/**
* @param {Object} tagName 标签名
* @param {Object} options 标签属性
* @param {Object} children 标签子元素
*/
h(tagName,options,children){
// 创建虚拟DOM节点
let VNode = {}
// 判断动态参数如果只有一个时
if(arguments.length == 1){
// 如果参数为非对象代表h函数创建了纯Text节点
if(!(arguments[0] instanceof Object)){
// 初始化纯文本节点和其真实DOM对象
VNode = {
type:'text',
content:tagName+'',
$el:document.createTextNode(tagName+'')
}
}else{
// 若非纯文本节点统一认为是虚拟DOM对象
VNode = arguments[0]
}
}else{
// 当正常设置参数时初始化该虚拟节点的真实DOM对象
let element = document.createElement(tagName)
// 初始化行内样式
this.initStyles(element,options)
// 初始化元素默认属性
this.initAttrs(element,options)
// 初始化事件系统
this.initEvents(element,options)
// 初始化id和class
this.initIdClass(element,options)
let c
// 处理虚拟DOM子节点
if(children instanceof Array){
// 将每个子节点采用h函数重新初始化
c = children.map(item => {
return this.h(item)
})
}else{
//当子节点为非数组时
if(children){
c = [this.h(children)]
}
}
// 将虚拟DOM节点信息和其真实DOM对象进行处理
VNode = {
type:tagName,
...options,
$el:element,
children:c
}
}
// 返回创建的节点
return VNode
},
// 初始化行内样式
initStyles(element,options){
for(let key in options.style){
element.style[key] = options.style[key]
}
},
// 初始化标签自带属性
initAttrs(element,options){
for(let key in options.attrs){
element[key] = options.attrs[key]
}
},
// 初始化标签的事件系统
initEvents(element,options){
for(let key in options.on){
element.addEventListener(key,options.on[key])
}
},
// 初始化id和class
initIdClass(element,options){
if(options.id){
element.id = options.id
}
if(options.className){
element.className = options.className
}
},
/**
* @param {Object} OldVNode 旧的虚拟DOM节点
* @param {Object} VNode 新的虚拟DOM节点
*/
patch(OldVNode,VNode){
// 判断同层节点是否相同
if(this.sameVNode(OldVNode,VNode)){
// 相同节点进入属性PATCH
this.patchAttrs(OldVNode,VNode)
// 若为文本节点则进入文本PATCH
this.patchText(OldVNode,VNode)
// 对节后代深层递归重复PATCH
if(OldVNode.children){
OldVNode.children.forEach((item,index) => {
this.patch(item,VNode.children[index])
})
}
}else{
// 若前后节点不同执行替换
this.replaceVNode(OldVNode,VNode)
}
},
// 属性PATCH
patchAttrs(OldVNode,VNode){
// 若元素相同则将新虚拟DOM对象的真实DOM节点引用为当前节点
VNode.$el = OldVNode.$el
// 遍历虚拟DOM的属性将变更部分更新到真实DOM中
for(let key in VNode.attrs){
if(VNode.$el[key]!=VNode.attrs[key]){
VNode.$el[key] = VNode.attrs[key]
}
}
},
// 文本PATCH
patchText(OldVNode,VNode){
if(OldVNode.type == 'text'){
// 元素相同时文本节点直接挂载原始DOM对象的引用
VNode.$el = OldVNode.$el
// 若文本内容不同则更新
if(VNode.$el.textContent!=VNode.content){
VNode.$el.textContent = VNode.content
}
}
},
// 替换旧节点的DOM对象
replaceVNode(OldVNode,VNode){
OldVNode.$el.parent.insertBefore(OldVNode.$el,VNode.$el)
OldVNode.$el.parent.removeChild(OldVNode.$el)
},
// 判断是否为相同节点
sameVNode(OldVNode,VNode){
if(OldVNode.key == VNode.key){
return true
}else if(OldVNode.type == VNode.type){
return true
}else{
return false
}
},
/**
* @param {Object} el 选择器
* @param {Object} vdom 虚拟DOM树
*/
createElement(el,vdom){
// 获取真实DOM树的渲染容器
let elem = document.querySelector(el)
// 创建递归渲染函数
function appendChild(elem,children){
// 递归将元素节点插入到根节点中
children.forEach(item => {
// 将当前节点的子元素设置为虚拟DOM节点对应的DOM对象
elem.appendChild(item.$el)
// 如果该节点有后代元素便执行递归
if(item.children){
appendChild(item.$el,item.children)
}
})
}
appendChild(elem,[vdom])
},
// 定义响应式数据定义对象
reactive(data){
// 通过代理对象返回可响应式的数据对象
return new Proxy(data,{
// 通过防抖算法实现多次更改数据只触发一次更新
set:debounce((target,property,value) =>{
console.log('视图更新1次')
// 获取更改前后的虚拟DOM对象
let newVDOM = Index()
let oldVDOM = vdom
// 通过patch执行DIFF的差异更新
this.patch(oldVDOM,newVDOM)
//将全局的vdom替换为本次更新好的vdom
vdom = newVDOM
// console.log(target)
},0),
get(target,property){
return target[property]
}
})
}
}
function debounce(fn,timer){
let timeout
return function(target,property,value){
target[property] = value
if(timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
fn(target,property,value)
},timer)
}
}
// 定义可响应的数据对象
let data = V.reactive({
count:0,
count1:10,
type:'text'
})
function Index(){
return V.h('div',{
id:'d',
className:'p-d',
attrs:{
'data-name':'hahah'
},
on:{
click(){
console.log('点击div的点击事件')
}
}
},[
//将count动态追加到虚拟DOM树中
'count的结果:'+data.count,
V.h('br',{}),
//将count1动态追加到虚拟DOM树中
'count1的结果:'+data.count1,
V.h('br',{}),
V.h('input',{
attrs:{
value:data.count,
type:data.type //将type动态绑定到输入框的类型中
},
on:{
input(e){
console.log('输入框的输入事件')
// 通过data属性同时更新count和count1的值
//用以测试多次更改一次渲染
data.count = e.target.value
data.count1 = e.target.value+'aaa'
}
}
}),
V.h('select',{
on:{
input(e){
console.log('下拉列表的选择事件')
// 动态变更下拉菜单实现元素的展示样式替换
data.type = e.target.value
console.log(data.type)
}
}
},[
V.h('option',{
attrs:{
value:'radio'
}
},'单选按钮'),
V.h('option',{
attrs:{
value:'checkbox'
}
},'多选按钮'),
V.h('option',{
attrs:{
value:'password'
}
},'密码框'),
V.h('option',{
attrs:{
value:'text'
}
},'输入框')
]),
V.h('button',{
id:'btn',
attrs:{
type:'submit'
},
on:{
click(){
console.log('按钮的点击事件')
// 只更改一个count实现单独更新
data.count++
}
}
},'你好')
])
}
let vdom = Index()
console.log(vdom)
V.createElement('#app',vdom)
</script>
</body>
</html>
完整案例运行时可以直接在初始界面中操作第一个输入框的内容,第一个输入框在变更时会同时修改data中的count和count1两个属性,所以会同时出发两次reactive的set函数,此时查看控制台会发现只执行一次patch,视图仅仅更新一次,如图。
当切换count和count1时可以同时观察web查看器的Elements选项,会发现只有变更部分的DOM节点会执行更新,其他不涉及变更的DOM节点不会被重新渲染,如图。
到此为止虚拟DOM的创建以及DIFF和PATCH的初步实现便介绍完毕。
尾声
虚拟DOM的思路与实现对于业务开发方面虽然没有实质性的帮助,虚拟DOM本身也并不是Vue或React等任何框架框架的专属,而是MVVM架构框架的渲染和数据驱动原理的一部分,本文从基础到实现仅仅作为虚拟DOM的简单介绍,并不没有实现完善的虚拟DOM和组件化更新机制,通过简单的实现以方便读者理解。读者们可以通过阅读本文在对虚拟DOM的理解上得到帮助。有不懂的或想学习的内容可以联系作者催更哈,最后感谢阅读。