美文网首页Vue
Vue2.x的虚拟DOM diff原理

Vue2.x的虚拟DOM diff原理

作者: 卓三阳 | 来源:发表于2018-10-21 23:47 被阅读48次
    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】

    `

    相关文章

      网友评论

        本文标题:Vue2.x的虚拟DOM diff原理

        本文链接:https://www.haomeiwen.com/subject/gufoiftx.html