1.什么是虚拟dom?
(1)什么是虚拟DOM?
vdom可以看作是一个使用javascript
模拟了DOM结构
的树形结构,这个树结构包含整个DOM结构的信息:
<ul id="list">
<li class="item">item1</li>
<li class="item">item2</li>
</ul>
上面的DOM结构,不论是标签名称还是标签的属性或标签的子集,都会对应在下面的树结构(其实就是一个对象)里
{
tag:'ul',
attrs:{
id:'list'
},
children:[
{
tag:'li',
attrs:{className:'item'},
children:['item1']
},
{
tag:'li',
attrs:{className:'item'},
children:['item2']
}
]
}
Snabbdom is virtual DOM library,Vue2.0使用的就是snabbdom
2.虚拟DOM+diff的性能
2.1虚拟DOM+diff为什么快?
(1)真实DOM的创建需要完成默认样式,挂载相应的属性,注册相应的Event Listener ...效率是很低的。如果元素比较多的时候,还涉及到嵌套,那么元素的属性和方法等等就会很多,效率更低。 diff算法对DOM进行原地复用,减少DOM创建性能耗费
(2)虚拟DOM很轻量,对虚拟DOM操作快
(3)页面的排版与重绘也是一个相当耗费性能的过程。通过对虚拟DOM进行diff,逐步找到更新前后vdom的差异,然后将差异反应到DOM树上(也就是patch)减少过多DOM节点排版与重绘损耗
。特别要提一下Vue的patch是即时的,并不是打包所有修改最后一起操作DOM(React则是将更新放入队列后集中处理),朋友们会问这样做性能很差吧?实际上现代浏览器对这样的DOM操作做了优化,并太大差别。
使用虚拟DOM的损耗计算:
总损耗 = 虚拟DOM增删改 + (与Diff算法效率有关)真实DOM差异增删改 + (较少的节点)排版与重绘
直接使用真实DOM的损耗计算:
总损耗 = 真实DOM完全增删改 + (可能较多的节点)排版与重绘
2.2虚拟DOM+diff的缺点?
引入虚拟DOM实际上有优点也缺点。
(1)尺寸
更多的功能意味着更多的代码。
(2)内存
虚拟DOM需要在内存中的维护一份DOM的副本。在DOM更新速度和使用内存空间之间取得平衡。
(3)不是适合所有情况
如果虚拟DOM大量更改,这是合适的。但是少量的,频繁的更新的话,虚拟DOM将会花费更多的时间处理计算的工作。所以,如果一个DOM节点相对较少页面,用虚拟DOM,它实际上有可能会更慢。
3.Vue2.x的虚拟DOM diff原理
源码
下面是精简版的分析
3.1 patch函数
diff的过程就是调用patch函数,就像打补丁一样修改真实dom。
function patch (oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl)
createEle(vnode)
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
api.removeChild(parentEle, oldVnode.el)
oldVnode = null
}
}
return vnode
}
在patch函数中,当oldVnode与vnode在sameVnode的时候才会进行patchVnode,也就是新旧VNode节点判定为同一节点的时候才会进行patchVnode这个过程,否则就是创建新的DOM,移除旧的DOM。
3.2 patchVnode函数
function patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
节点的比较有5种情况
(1)if (oldVnode === vnode) 两个VNode节点相同则直接返回。
(2)if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本节点的比较,需要修改,则会调用Node.textContent = vnode.text。
(3)if( oldCh && ch && oldCh !== ch ), 两个节点都有子节点,而且它们不一样,这样我们会调用updateChildren函数比较子节点,这是diff的核心
,后边会讲到。
(4)else if (ch),只有新的节点有子节点,调用createEle(vnode),vnode.el已经引用了老的dom节点,createEle函数会在老dom节点上添加子节点。
(5)else if (oldCh),新节点没有子节点,老节点有子节点,直接删除老节点。
3.3 updateChildren函数
function updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { //对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else {
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
参考
【深入Vue2.x的虚拟DOM diff原理】
【 解析vue2.0的diff算法】
【全面理解虚拟DOM,实现虚拟DOM】
`