虚拟 DOM
虚拟DOM是什么?
虚拟 DOM 就是用一个 JS 对象来描述一个 DOM 节点。
<div class="a" id="b">我是内容</div>
{
tag: 'div',
attrs: {
class: 'a',
id: 'b'
},
text: '我是内容',
children: []
}
Vue为什么采用虚拟DOM,而不直接操作真实DOM呢?
Vue是数据驱动视图,数据发生变化视图就要更新视图,但是操作 DOM 代价过于昂贵,可以用 js 的计算来换取操作 DOM 所消耗的性能,通过对比数据变化前后的状态,计算出视图中哪些地方需要更新,只要更新需要更新的地方。这样就可以尽少的操作 DOM 了。
通过利用 JS 模拟一个 DOM 节点,当数据发生变化时,对比变化前后的虚拟 DOM,通过 diff 算法计算出需要更新的地方,然后去更新视图。
VNode类
class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
}
VNode 中包含了一个真实的 DOM 节点所需要的属性,通过 VNode 类,来实例化出各种类型的真实 DOM 节点。
const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isComment = vnode.isComment
cloned.fnContext = vnode.fnContext
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.asyncMeta = vnode.asyncMeta
cloned.isCloned = true
return cloned
}
创建好虚拟节点之后,就到了比较阶段了.通过比较新旧两份 VNode,找出差异,然后更新差异的 DOM 节点。
patch
DOM-Diff 的过程就是 patch 过程。数据变化之后对应的虚拟 DOM(newVNode)。以 newVNode 为基准,对比 oldVNode. 从而让新旧 VNode 相同。 这就是 patch 过程干的事情。
- 创建节点
如果 newVNode 中存在,oldVNode 中不存在,就去创建节点加入到 oldVNode.
function createElm () {
if (isDef(tag)) {
} else if (isTrue(vnode.isComment)) {
} else {
}
}
如果有 tag 标签,就是元素节点。调用 createElement 创建元素节点。如果有子节点就遍历创建所有子节点,将创建好的子节点插入到当前元素节点里面,最后插入到 DOM 中。
如果有 isComment 就代表是注释标签,调用 createComment 创建注释节点。在插入到 DOM.
如果都不是,就是文本节点,调用 createTextNode 创建文本节点。
- 删除节点
如果 newVNode 中不存在, oldVNode 中存在,就去从 oldVNode 中删除这个节点。
function removeNode (el) {
const parent = nodeOps.parentNode(el)
if (isDef(parent)) {
nodeOps.removeChild(parent, el)
}
}
就是获取到要删除元素的父节点,然后调用 removeChild 方法。
- 更新节点
如果 newVNode 和 oldVNode 都有,就以新的 VNode 为准,更新旧的 oldVNode。
function patchVnode () {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
}
- 总结
patchVnode 方法,是复用同一个 dom 元素,如果新旧两个 VNode 对象都有子元素。应该调用 updateChildren 方法。
更新子节点 updateChildren
VNode 是元素节点,并且该节点包含子节点,oldVNode 也包含子节点,递归对比更新子节点
会找到对应的 oldVNode 和 newVNode 的第一个和最后一个索引作为指针,比较的过程就是移动指针。
然后找到对应的 newVNode 和 oldVNode 的第一个和最后一个 VNode 节点。
while 循环在新旧节点结束后停止,否则会不断的执行循环流程。
-
如果 oldStartVnode 为定义,就移动 oldCh 数组遍历的起始指针到后一位。 -
如果 oldEndVnode 为定义,则 oldCh 数组向前移动一位。 -
newChildren 数组里的所有未处理子节点的第一个子节点和 oldChildren 数组里的所有未处理子节点的第一个比较,如果相同,就直接进入 直接进入更新节点的操作,也无需进行节点移动操作。 -
newChildren 数组里所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对, 如果相同,那就直接进入更新节点的操作,无需进行节点移动操作. -
把newChildren数组里所有未处理子节点的最后一个子节点和oldChildren数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作 更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置; -
把newChildren数组里所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作 更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置; -
如果上述六种情况都不满足. 会走到默认 else。 通过遍历 oldCh 数组,找出其中 key 的对象,并以 key 为键,索引为 value, 生成新的对象 oldKeyToIdx.
然后查看 newStartVnode 是否有 key 值,并查找 oldKeyToIdx 是否有相同的 key。
如果 newStartVnode 没有 key 或 oldKeyToIdx 没有有相同的 key。 就新增节点插入到 oldChildren 厘米为处理节点之前。
如果找到了,并且两个节点相同,说明可以复用,调用patchVnode方法复用dom元素并递归比较子元素,重置 oldCh 中相对的元素为 undefined. 然后插入到 oldStartVnode.elm 签名。newCh 的起始索引后移一位。
如果节点不同,就调用 createElm 新增元素,newCh 索引后移一位。
|