gogogo
管理员
管理员
  • UID25
  • 粉丝0
  • 关注0
  • 发帖数1377
阅读:3600回复:0

虚拟 DOM 实现原理

楼主#
更多 发布于:2020-10-16 11:40
Vue2.x的虚拟DOM diff原理

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操作做了优化,并太大差别。



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)
        }
游客


返回顶部