浅析vue的diff算法原理

2022-06-11  本文已影响0人  青城墨阕

渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?

模板转换成视图的过程

简单点讲,在Vue的底层实现上,Vue将模板编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应到DOM操作上。

什么是虚拟DOM?

Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实的DOM上。

        <ul id='list'>
          <li class='item'>Item 1</li>
          <li class='item'>Item 2</li>
          <li class='item'>Item 3</li>
        </ul>
    var element = {
        tagName: 'ul', // 节点标签名
        props: { // DOM的属性,用一个对象存储键值对
            id: 'list'
        },
        children: [ // 该节点的子节点
          {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
          {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
          {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
        ]
    }

为何需要Virtual DOM?

diff算法

在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较

diff流程图
patch

来看看 patch 是怎么打补丁的(代码只保留核心部分)

function patch (oldVnode, vnode) { 
    // some code 
    if (sameVnode(oldVnode, vnode)) {  
        patchVnode(oldVnode, vnode) 
    } else {  
        const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点  
        let parentEle = api.parentNode(oEl) // 父元素  createEle(vnode) 
        // 根据Vnode生成新元素  
        if (parentEle !== null) {   
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
            api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点   
            oldVnode = null  
        } 
    } // some code  
    return vnode
}

// sameVnode
function sameVnode (a, b) { 
        return ( 
            a.key === b.key && // key值 
            a.tag === b.tag && // 标签名 
            a.isComment === b.isComment && // 是否为注释节点 
            // 是否都定义了data,data包含一些具体信息,例如onclick , 
            style isDef(a.data) === isDef(b.data) &&  sameInputType(a, b) 
            // 当标签是<input>的时候,type必须相同 
        )
}

patch函数做了什么?

  1. 值得比较则执行 patchVnode
  2. 不值得比较则用 Vnode 替换 oldVnode
    2.1 获取当前oldVnode对应的真实元素节点oldEle;
    2.2 获取oldEle父元素parentEle;
    2.3 根据vnode生成新元素newEle;
    2.4 若parentEle不为空,则直接将newEle添加进父元素;
    2.5 溢出oldEle。

patchVnode

当我们确定两个节点值得比较之后我们会对两个节点指定 patchVnode 方法。

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

patchVnode函数做了什么?

  1. 找到对应的真实dom,称为 el;
  2. 判断 Vnode 和 oldVnode 是否指向同一个对象,如果是,那么直接 return;
  3. 如果 oldVnode 有子节点而 Vnode 没有,则删除 el 的子节点;
  4. 如果 oldVnode 没有子节点而 Vnode 有,则将 Vnode 的子节点真实化之后添加到 el;
  5. 如果他们都有文本节点并且不相等,那么将 el 的文本节点设置为 Vnode 的文本节点;
  6. 如果两者都有子节点,则执行 updateChildren 函数比较子节点。

updateChildren
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) 
        
    }
}

代码量太大,可直接参考updateChildren图解进行理解

参考资料:
https://juejin.cn/post/6881907432541552648#heading-1
https://juejin.cn/post/6972407881790390308
https://segmentfault.com/a/1190000020663531

上一篇 下一篇

猜你喜欢

热点阅读