什么是Dom
在介绍虚拟Dom之前,我们先简单了解一下什么是Dom.Dom是Document Object Model的简称。Dom是一种通过对象来表示文档结构的方式。我们可以通过Dom来创建结构,
新增修改删除节点和内容。在浏览器端我们可以使用Javascript和Css来操作Dom。也就是说Dom是一个我们如何获得修改增加删除Html节点的规范。
原始Dom有什么问题?
在Html Dom中,我们一个节点对象都代表一个Html节点。在现代到前端场景中,类似ASP或者一些Web App的应用我们可能需要使用大量到元素来构建一个页面,成千上万的节点就会有成千上万到节点对象(Dom),
你会有一个非常大到Dom树,我们可能会有很多种情况需要修改增加删除节点。
为什么原始Dom会慢?
其实更新Dom并不慢,他就像更新一个js对象,但是了解浏览器渲染过程的应该知道,渲染引擎负责解析HTML来创建Dom,也负责解析css,在HTML上应用css来构建渲染树,而Layout过程
给了每个渲染树中的节点一个纵坐标,表示了这个节点在哪里渲染和显示。在我们修改增加删除节点时候,特别是节点层级比较多的时候或者节点比较多的时候,重新计算css和修改layout
使用了复杂的算法从而影响了性能,因此更新原始Dom不仅仅是更新了Dom,还牵涉了很多过程,每次更新原始Dom都会重复这些过程,这就是为什么Dom慢的原因,
而Virtual Dom就是尽可能到减少这个过程的时间。
什么是Virtual Dom
Virtual Dom是HTML DOM抽象出来的结果,你也可以理解为HTML DOM是HTML DOM简化以后到结果,他允许我们在一个虚拟的Dom世界里避免”真正”到Dom操作。
ReactJS的Virtual Dom简述
最早听到Virtual Dom是在ReactJS中,而Virtual Dom的核心步骤主要是三个
- 构建Virtual Dom。
- 两个Virtual Dom比较差异。
- patch老到Virtual Dom。
而ReactJS中Virtual Dom之所以快是因为他使用了: - 高效的diff算法。
- 批量更新操作。
- 只更新需要更新的子节点。
- 使用观察模式而不是脏检查来监控节点更新。
网上已经有许多的ReactJS的Virtual Dom实现的文章,大家可以找来看看。有时间我也会深究一下。
vue中Virtual Dom的实现
首先在vue中有部分到vdom实现是基于Snabbdom库而实现的。在之前的vue源代码学习02中我们已经知道了Vue根据const { render, staticRenderFns } = compileToFunctions(template, {})
获得了render方法.我们先来看看生成完的render方法。来看一个最简单的例子:
1 | const vm = new Vue({ |
而VNode是这样定义的:
1 | export default class VNode { |
再看一下最主要的createElement方法:
1 | function _createElement ( |
这里的Watcher我们先简单的介绍一下,当代码执行到时候updateComponent会作为获取值的表达式来获得value,当vm对象到任何属性改变的时候都会重新调用这个表达式来重新获取值。
这个方法很关键,我们后面在展开。先看更关键vm._update方法,前面我们已经获得了_render方法返回到VNode。来看看_update方法
1 | Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { |
我们终于看到了patch方法。在vuejs中patch方法中同时也实现了diff算法。这个方法主要做了这几件事:
- 如果没有老元素,则直接创建新元素。
- 如果有老元素,老元素是否为原始Element,是原始Element考虑是否是服务器渲染(SSR)的,服务器渲染则混合(hydrate)。
- 如果有老元素,老元素不是原始Element,比较老元素和新元素并且更新元素。
服务器渲染混合的情况我们先放一边,直接来看patchVNode方法:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
//当节点为clone的静态节点时会被重复使用。
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
//updateAttrs; updateClass; updateDOMListeners; updateDOMProps; updateStyle; update; updateDirectives;
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
//都包含子节点
if (isDef(oldCh) && isDef(ch)) {
//子节点不同,更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
//只有新的元素包含子节点,就新增这些新子节点
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
//如果只有老元素包含子节点,则需要移除这些子节点
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
//更新text
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
结合updateChildren的流程图来看
vue仅仅对同一层的节点尝试匹配这样的算法是一个O(n)的复杂度,n取决于节点的大小,也尝试使用key进行匹配。而修改只修改发生了变化的节点,也就是和老节点存在差异的节点,
这又是一个O(n)的复杂度,这个n取决于存在差异的节点个数。