# 1.虚拟 dom 到底是什么?
> 虚拟 dom 是对真实 dom 的对象描述,这个对象可以称为 vnode。那么 vnode
> 是怎么产生的又有什么属性呢?
> Vue 通过调用 createElement 函数来返回 vnode,createElement 函数大致如下
> 简易流程图如下
> vnode 的定义则是在 types 中的 vnode.d.ts 文件,如下
- tag: 当前节点的标签名
- data: 当前节点的数据对象
- 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 指令
> 而其中最为重要的 data 的定义如下
# 虚拟 dom diff 算法
### diff 算法包括一下几个步骤:
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM
树,插到文档当中
- 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较(diff),记录两棵树差异
- 把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM
树上(patch),视图就更新了
而且,diff算法是通过**同层的树节点**进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法
> 函数 patch 的核心部分如下
### patch 函数做了以下事情:
- 使用samevnode 比较新旧vnode,只有相同,才会使用patchVnode函数进行深入节点比较
- 否则,取得 oldvnode.el 的父节点,parentEle 是真实 dom,createEle(vnode)会为 vnode 创建它的真实 dom,令 vnode.el =真实 dom,
parentEle 将新的 dom 插入,移除旧的dom。也就是说使用新节点直接将老节点替换掉。
### patchVnode 函数做了以下事情:
- 找到对应的真实dom,称为 el
- 判断 Vnode 和oldVnode 是否指向同一个对象,如果是,那么直接 return
- 如果他们都有文本节点并且不相等,那么将 el 的文本节点设置为 Vnode的文本节点。
- 如果oldVnode 有子节点而 Vnode 没有,则删除el 的子节点
- 如果oldVnode 没有子节点而 Vnode 有,则将 Vnode 的子节点真实化之后添加到 el
- 如果两者都有子节点,则执行updateChildren 函数比较子节点,这一步很重要
- 其他几个点都很好理解,我们详细来讲一下updateChildren
> 上面代码很长,但是逻辑很清晰
> 代码里分别有四个坐标,分别为 oldStart+oldEnd,newStart+newEnd。然后处理如下
1. 处理四个指标分别为null 的情况,如果是 end
则使用前一个节点,如果是start,则使用后一个节点
2. 处理头部的同类型节点,即 oldStart 和 newStart 指向同类节点的情况
3. 处理尾部的同类型节点,即 oldEnd 和 newEnd 指向同类节点的情况
4. 处理头尾/尾头的同类型节点,即 oldStart 和 newEnd,以及 oldEnd 和 newStart
指向同类节点的情况
5 上面四种情况之外的情况
5.1 尝试在oldStart+oldEnd 之间 中寻找跟 newStartVnode 具有相同 key
的节点,如果找不到相同 key 的节点,说明 newStartVnode
是一个新节点,就创建一个,并插入 oldStartVnode.elm 之前然后把 newStartVnode
设置为下一个节点
5.2 如果找到了跟 newStartVnode 相同 key 的节点,那么通过其他属性的比较来判断这 2
个节点是否是同一个节点,如果是,就调用 patchVnode 进行patch,并把
newStartVnode.elm 插入到 oldStartVnode.elm 之前,把newStartVnode
设置为下一个节点
最后,可能会出现两种情况,一个是,oldStartIdx \> oldEndIdx,这说明旧节点列表
oldch 先一步被循环完毕,此时新节点列表 newch
剩下的没有被循环到的也就是newStart+newEnd 之间的节点会被插入到 newEndIdx+1
的节点之前。另一种是新
节点列表 newch 先循环完毕,那么将旧节点列表 oldch 中未被处理的也就是oldstart
和oldend 之间的节点删除(注意,删除时由于第 5 第 2)
步中将被匹配到的旧节点设置为了
null,以此来标记此节点已经被处理过,因此它不会被删除)。