一、概述
我们都知道频繁操作Dom是一个非常耗费性能的事情,这是因为浏览器的Dom规范设计的非常复杂,
我们可以看到,一个简单的div元素,就有这么多复杂的属性,足以见得如果频繁操作Dom,将有多么耗费性能。在这种情况下,Virtual Dom应运而生,解决了频繁操作Dom的性能开销问题。它是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。
二、VNode
1、VNode属性
Virtual Dom把真实一个个节点转换成虚拟节点(VNode),用来表示Dom中的树形结构,一个VNode包含以下属性:
上图是Vue中对于VNode的定义,我们挑其中的一些主要属性:
-
tag
: 当前节点的标签名 -
data
: 当前节点的数据对象,具体包含哪些字段可以参考vue源码types/vnode.d.ts
中对VNodeData
的定义 -
children
: 数组类型,包含了当前节点的子节点 -
text
: 当前节点的文本,一般文本节点或注释节点会有该属性 -
elm
: 当前虚拟节点对应的真实的dom节点 -
ns
: 节点的namespace -
context
: 编译作用域 -
functionalContext
: 函数化组件的作用域 -
key
: 节点的key属性,用于作为节点的标识,有利于patch的优化 -
componentOptions
: 创建组件实例时会用到的选项信息 -
child
: 当前节点对应的组件实例 -
parent
: 组件的占位节点 -
raw
: raw html -
isStatic
: 静态节点的标识 -
isRootInsert
: 是否作为根节点插入,被<transition>
包裹的节点,该属性的值为false
-
isComment
: 当前节点是否是注释节点 -
isCloned
: 当前节点是否为克隆节点 -
isOnce
: 当前节点是否有v-once
指令
二、VNode 分类
VNode
可以理解为vue框架的虚拟dom的基类,通过new
实例化的VNode
大致可以分为几类
EmptyVNode
: 没有内容的注释节点TextVNode
: 文本节点ElementVNode
: 普通元素节点ComponentVNode
: 组件节点CloneVNode
: 克隆节点,可以是以上任意类型的节点,唯一的区别在于isCloned
属性为true
三、createElement()
Vue中使用createElement()
方法来生成VNode。代码由于过长就不在此贴出了,源代码在 下。另外从网上偷了张图来描述下这个方法执行的过程:
三、patch
patch是另一个Virtual Dom的核心,它用来比对前后两次改动的Dom的异同,并将改动的部分更新至真实Dom,比对的策略应用了Diff算法。
Vue中把这块分为三部分:patch、patchVNode、updateChildren,我们先来看patch部分:
patch
的策略是:
-
如果
vnode
不存在但是oldVnode
存在,说明意图是要销毁老节点,那么就调用invokeDestroyHook(oldVnode)
来进行销毁 -
如果
oldVnode
不存在但是vnode
存在,说明意图是要创建新节点,那么就调用createElm
来创建新节点 -
当
vnode
和oldVnode
都存在时-
如果
oldVnode
和vnode
是同一个节点,就调用patchVnode
来进行patch
(diff算法) -
当
vnode
和oldVnode
不是同一个节点时,如果oldVnode
是真实dom节点或hydrating
设置为true
,需要用hydrate
函数将虚拟dom和真是dom进行映射,然后将oldVnode
设置为对应的虚拟dom,找到oldVnode.elm
的父节点,根据vnode创建一个真实dom节点并插入到该父节点中oldVnode.elm
的位置
-
patchVnode
-
如果
oldVnode
跟vnode
完全一致,那么不需要做任何事情 -
如果
oldVnode
跟vnode
都是静态节点,且具有相同的key
,当vnode
是克隆节点或是v-once
指令控制的节点时,只需要把oldVnode.elm
和oldVnode.child
都复制到vnode
上,也不用再有其他操作 -
如果
vnode
不是文本节点或注释节点,调用updateChildren
执行更新子节点的操作 -
如果
vnode
是文本节点或注释节点,但是vnode.text != oldVnode.text
时,只需要更新vnode.elm
的文本内容就可以
updateChildren
- 如果
oldVnode
和vnode
都有子节点,且2方的子节点不完全一致,就执行更新子节点的操作,算法如下- 分别获取
oldVnode
和vnode
的firstChild
、lastChild
,赋值给oldStartVnode
、oldEndVnode
、newStartVnode
、newEndVnode
- 如果
oldStartVnode
和newStartVnode
是同一节点,调用patchVnode
进行patch
,然后将oldStartVnode
和newStartVnode
都设置为下一个子节点,重复上述流程 - 如果
oldEndVnode
和newEndVnode
是同一节点,调用patchVnode
进行patch
,然后将oldEndVnode
和newEndVnode
都设置为上一个子节点,重复上述流程 - 如果
oldStartVnode
和newEndVnode
是同一节点,调用patchVnode
进行patch
,如果removeOnly
是false
,那么可以把oldStartVnode.elm
移动到oldEndVnode.elm
之后,然后把oldStartVnode
设置为下一个节点,newEndVnode
设置为上一个节点,重复上述流程 - 如果
newStartVnode
和oldEndVnode
是同一节点,调用patchVnode
进行patch
,如果removeOnly
是false
,那么可以把oldEndVnode.elm
移动到oldStartVnode.elm
之前,然后把newStartVnode
设置为下一个节点,oldEndVnode
设置为上一个节点,重复上述流程 - 如果以上都不匹配,就尝试在
oldChildren
中寻找跟newStartVnode
具有相同key
的节点,如果找不到相同key
的节点,说明newStartVnode
是一个新节点,就创建一个,然后把newStartVnode
设置为下一个节点 - 如果上一步找到了跟
newStartVnode
相同key
的节点,那么通过其他属性的比较来判断这2个节点是否是同一个节点,如果是,就调用patchVnode
进行patch
,如果removeOnly
是false
,就把newStartVnode.elm
插入到oldStartVnode.elm
之前,把newStartVnode
设置为下一个节点,重复上述流程 - 如果在
oldChildren
中没有寻找到newStartVnode
的同一节点,那就创建一个新节点,把newStartVnode
设置为下一个节点,重复上述流程 - 如果
oldStartVnode
跟oldEndVnode
重合了,并且newStartVnode
跟newEndVnode
也重合了,这个循环就结束了
- 分别获取
- 如果只有
oldVnode
有子节点,那就把这些节点都删除 - 如果只有
vnode
有子节点,那就创建这些子节点 - 如果
oldVnode
和vnode
都没有子节点,但是oldVnode
是文本节点或注释节点,就把vnode.elm
的文本设置为空字符串