vue3.0虚拟dom,vue虚拟dom和真实dom
所谓虚拟DOM,就是为了解决浏览器性能的问题。比如一次操作有10次更新,本文主要介绍Vue源代码分析的虚拟DOM的相关信息,有需要的朋友可以参考一下。
为什么需要虚拟dom?
虚拟DOM旨在解决浏览器性能问题。比如一次操作有10个动作更新DOM,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到一个本地JS对象中,最后将JS对象一次性附着到DOM树中,然后再进行后续操作,避免大量不必要的计算。简单来说,虚拟DOM可以理解为一个简单的JS对象,它至少包含三个属性:标签名、属性和子属性。
-element node:element node更接近我们平时看到的真实的DOM节点。它具有描述节点标记名词的标记属性、描述节点属性(如类和属性)的数据属性,以及描述所包含的子节点信息的子属性。因为元素节点包含的信息相对复杂,所以不像前三个节点那样直接把源代码写死。
VNode的作用:用js的计算性能换取操作真实DOM所消耗的性能,
VNODE在Vue的整个虚拟DOM过程中起到了什么作用?其实VNode的功能还是挺大的。在渲染视图之前,我们将编写的模板编译到VNode中并缓存它。当数据变更页面需要重新渲染时,我们将数据变更后生成的VNode与上次缓存的VNode进行对比,找出差异。那么对应于不同VNode的真实DOM节点就是需要重新渲染的节点。最后将根据差异创建的DOM节点插入到视图中,最终完成视图更新。也就是说,对应于真实DOM的虚拟DOM节点是在数据改变之前和之后生成的。
为什么要有虚拟DOM:
——也就是用JS的计算性能来换取操作real DOM所消耗的性能。Vue通过VNode类实例化不同类型的虚拟DOM节点,学习不同类型节点生成的属性的差异。所谓不同类型的节点本质上是一样的,都属于VNode类的实例,只是实例化时传入的参数不同。
有了数据变化前后的VNode,就可以跟进DOM-Diff找出差异,最后只更新有差异的视图,达到尽量少操作真实DOM,节省性能的目的。
-找出具有不同更新的DOM节点达到了至少用真实DOM更新视图的目的。新旧VNode对比,找出差异的过程就是所谓的DOM-Diff过程,而DOM-Diff算法是整个虚拟DOM的核心。
Patch
在Vue中,DOM-Diff进程被称为patch进程,patch就是补丁的意思。一个思路:旧的VNode(odlNode)是数据变化前的虚拟DOM节点,而新的NVode是数据变化后要渲染的视图对应的虚拟DOM节点,所以我们要以生成的新VNode为基准。与旧VNode相比,如果新VNode中有节点而旧VNode中没有,则将其添加到旧VNode中;如果新虚拟节点中有节点,但旧虚拟节点中有节点,则从旧虚拟节点中删除它们。如果存在旧的和新的VNode节点,则新的VNode将占优势,并且旧的VNode将被更新,使得旧的VNode和新的Vnode是相同的。
整个补丁:正在创建节点:新的VNode有,旧的没有。它是在旧的旧节点中创建的
删除节点:如果新的VNode不存在,但旧的VNode存在,则将其从旧的VNode中删除。
更新节点:新旧都有,所以以新VNode为准,旧VNode更新。
更新子节点
/*
比较两个子节点数组肯定是通过循环,外循环newChildren,内循环oldCHildren数组,每个循环外层
对于newChildren数组中的每个子节点,转到内部oldChildren数组,查看它是否有相同的子节点。
*/
for(设I=0;我是新出生的。长度;i ) {
const newChild=newChildren[i]
for(设j=0;j oldChildren.lengthj ) {
const oldChild=oldChildren[i]
if (newChild===oldChild) {
//.
}
}
}
那么在上述过程中就会出现四种情况。
创建一个子节点。如果newChildren中的一个子节点在oldChildren中找不到相同的子节点,说明newChildren中的这个子节点以前不存在,这次需要添加。然后创建一个子节点。
删除子节点。如果循环完newChildren中的每个子节点后,oldChildren中仍有未处理的子节点,则意味着需要放弃未处理的子节点,因此删除这些节点。
移动子节点,如果newChildren中的一个子节点在oldChildren中找到了相同的子节点,但是位置不同,说明这次需要调整这个子节点的位置。然后根据newChildren中的子节点1的位置,调整这个节点在oldChildren中的位置,使其与newChildren中的位置相同。
更新节点:如果newChildren中的一个子节点在oldCHildren中找到了相同的子节点,并且位置相同,则更新oldChildren中的节点,使其与newChildren中的节点相同。
我们反复强调,更新节点要基于新的VNode,然后操作旧的VNode,使旧的Vnode和新的Vnode一样。
更新的时候分为三个部分:
如果VNode和oldVNode都是静态节点,
我们说过,静态节点与数据中的任何变化无关,所以如果它们都是静态节点,它们将被跳过而不进行处理。
如果VNode是文本节点
如果VNode是我的文本节点,就说明这个节点只包含纯文本,所以我们只需要看看oldVNode是否也是文本节点。如果是的话,我们可以比较这两个文本是否不同。如果我们不需要将oldVNode中的文本更改为与VNode中的文本相同,如果oldVNode不是文本节点,那么无论是什么,我们都可以直接调用setTextNode方法将其更改为与VNode具有相同文本内容的文本节点。
如果VNode是一个元素节点,它被细分为以下两种情况
此节点包含子节点,因此它取决于旧节点是否包含子节点。如果旧节点包含子节点,则需要递归比较和更新子节点。
如果旧节点不包含子节点,则旧节点可能是空节点或文本节点。
如果旧节点为空,则在新节点中创建子节点的副本,并将其插入到旧节点中。
如果旧节点是文本节点,则清空文本,然后在新节点中创建子节点的副本,并将其插入旧节点。
此节点不包含子节点。如果该节点不包含子节点,并且不是文本节点,则意味着该节点是空节点。那很简单。不管以前老节点里有什么,清空就好。
//更新节点
函数patchVnode(oldVnode,Vnode,insertedVnodeQueue,removeOnly) {
vnode和oldVnode完全一样吗?如果是,退出程序。
if (oldVnode===vnode) {
返回
}
const elm=vnode . elm=old vnode . elm
vnode和oldVnode是否是静态节点,如果是,退出程序。
if(is true(vnode . is static)is true(vnode . is static)vnode . key===old vnode . key(is true(vnode . is cloned) is true(vnode . is once))){
返回
}
const oldCh=oldVnode.children
const ch=vnode.children
//vnode具有文本属性,否则为
if (isUndef(vnode.text)) {
if (isDef(oldCh) isDef(ch)) {
//如果两者都存在,则判断子节点是否相同,如果不同则更新子节点。
如果(oldCh!==ch) updateChildren(elm,oldCh,ch,insertedVnodeQueue,removeOnly)
}
//如果只存在vnode的子节点
else if (isDef(ch)) {
/**
*确定oldVnode是否有文本。
*如果没有,将Vnode的子节点添加到真实的DOM中。
*如果是,清空DOM中的文本,然后将vnode的子节点添加到真正的DOM中。
* */
if(isDef(old vnode . text))nodeops . settext context(elm,)
addVnodes(elm,null,ch,0,ch.length - 1,insertedVnodeQueue)
}
//如果只存在oldnode的子节点
else if (isDef(oldCh)) {
//清空DOM中的所有子节点
remove nodes(elm,oldCh,0,oldCh.length - 1)
}
//如果vnode和oldnode都没有子节点,但oldnode中有文本
else if (isDef(oldVnode.text)) {
nodeOps.setTextContext(elm,)
}
//以上两个判断可以用一句话概括,就是如果vnode中既没有text也没有子节点,那么对应的oldnode中什么是空的。
} else if (oldVnode.text!==vnode.text) {
nodeOps.setTextContext(elm,vnode.text)
}
}
以上,我们了解了Vue的补丁,也就是DOM-DIFF算法,知道打补丁过程中基本要做三件事,即创建节点、删除节点和更新节点。创建和删除节点很简单,而更新节点更复杂,因为它必须处理各种可能的情况。在更新过程中,九个Vnode点可能都包含子节点,并且会有一些额外的逻辑用于子节点的比较更新。所以本文将学习如何在Vue中比较子节点。
更新子节点
当新的VNode和旧的Vnode都是元素节点并且包含子节点时,那么这个偶数节点的Vnode实例上的chidlren属性就是所包含的子节点数组。比较两个子节点的传递循环,即外部循环newChildren数组、内部循环oldChildren数组和每个循环,即外部newChildren数组中的一个子节点。然后,转到内部oldchichildren数组,查看是否有与其相同的子节点。
. 创建子节点
创建子节点的位置应该在所有未处理的节点之前,而不是在所有已处理的节点之后。因为如果在处理过的节点后面插入子节点,如果后面要插入新的节点,那么新的子节点就会被弄乱。
. 移动子节点
在所有未处理的节点之前,是我们要移动的目的地的位置。
优化更新子节点:
我们前面介绍过,当new VNode和oldVNode都是元素节点并且包含子节点时,vue子节点是newChildren数组的第一个外部循环,然后是oldChildren数组的内部循环。每次循环newChildren数组的外循环中的一个子节点时,我们都要去oldChildren数组的内循环看看是否有与之相同的子节点。最后我们根据不同的情况做出不同的操作。还有一些优化,比如当有大量子节点时,循环算法的时间复杂度会变得非常大,不利于性能提升。
方法:
首先,在newChildren数组中所有未处理的子节点的第一个子节点和oldChildren数组中所有未处理的子节点的第一个子节点之间建立一对。如果相同,直接进行更新节点的操作;
如果不同,则将newChildren数组中所有未处理的子节点的最后一个节点与oldChildren数组中所有未处理的子节点的最后一个子节点进行比较,如果相同,则直接进入更新节点的操作;
如果它们不同,则将newChildren数组中所有未处理的子节点的最后一个子节点与oldChildren数组中所有未处理的子节点的第一个子节点进行比较。如果相同,直接进入更新节点的操作,然后将oldChildren数组中的节点移动到更新后newChildren数组中节点的相同位置;如果不同,
然后将newChildren数组中所有未处理的子节点的第一个子节点与oldChildren数组中所有未处理的子节点的最后一个子节点进行比较,如果相同,则直接进入更新节点的操作,然后将oldChildren数组中的节点移动到与更新后的newChildren数组中的节点相同的位置;
如果最后四种情况还是不一样,就找上一个循环中的节点。
为了避免双环数据量大和时间复杂度增加带来的性能问题,Vue选择从子节点数组中的四个特殊位置进行相互比较,即新旧之前、新旧之后、新旧之前、新旧之前。
在之前的文章中,介绍了Vue中的虚拟DOM及其patch(DOM-Diff)过程。虚拟DOM存在的必要条件是现有的VNode,那么VNode是从哪里来的呢?编译用户写的模板,就会生成VNode。
模板编译:
什么是模板编译:把用户模板标签里写的类似原生HTML的内容编译出来,找出原生HTML的内容,再找出非原生HTML。经过一系列的逻辑处理,生成渲染函数,这就是所谓的模板编译过程。render函数将模板内容生成到VNode中。
整个渲染过程,所谓渲染过程,是指整个渲染过程,即用户编写的模板,类似于原生HTML,通过一系列的过程最终体现在视图中。这个过程上面已经提到了。
抽象语法树AST:
用户的模板标签里写的模板是一堆字符串给Vue,那么如何解析这一堆字符串,从中提取元素的标签、属性、变量插值等有效信息呢?这需要一种叫做抽象语法树的东西的帮助。
抽象语法树,简称语法树,是源代码语法结构的抽象表示。它以树的形式表示编程语言的语法结构,树中的每个节点表示源代码中的一个结构。语法之所以抽象,是因为这里的语法并不能代表真实语法中出现的每一个解析妹子。例如,嵌套的括号隐含在树的结构中,但是没有以节点的形式出现。
具体流程:
在将一堆字符串模板解析成抽象语法树AST之后,我们可以对它执行各种操作。经过处理后,AST生成render函数,其具体的三个过程可以分为以下三个阶段
模板解析阶段:通过正则表达式将一堆模板字符串解析成抽象语法树AST。
优化阶段:编译AST,找出静态节点并标记。
代码生成阶段:将AST转换为渲染函数
有了模板编译,就有了虚拟DOM,就有了后续的视图更新。
总结
关于Vue源代码分析的虚拟DOM的这篇文章就到这里了。更多关于Vue虚拟DOM的信息,请搜索我们之前的文章或者继续浏览下面的相关文章。希望你以后能支持我们!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。