美文网首页
浅析vue的diff算法原理

浅析vue的diff算法原理

作者: 青城墨阕 | 来源:发表于2022-06-11 22:57 被阅读0次

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

模板转换成视图的过程

  • Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树;
  • 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行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?

  • 具备跨平台的优势
    由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

  • 操作原生DOM慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。
    因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。

  • Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)

  • 提升渲染性能
    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

相关文章

  • 浅析vue的diff算法原理

    渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重...

  • 0725-

    1.virtual DOM 和 Diff算法? 2.vue生命周期 3.vue-router原理 4.vue通信 ...

  • sammary

    vue-diff算法 react 性能优化 diff算法 ,局部更新DOMshouldComponentUpdat...

  • Vue2.0的Diff算法

    原文:解析Vue2.0的Diff算法 Vue2.0加入了Virtual Dom,Vue的Diff位于patch.j...

  • VueDiff算法的简单分析和一些个人思考

    Diff算法是Vue视图动态改变的核心算法之一 本文包括对Diff算法的简单概括,和我闲的难受对Diff算法的一些...

  • 框架和类库——vue

    1.熟练使用Vue的API、生命周期、钩子函数 2.MVVM框架设计理念 3.Vue双向绑定实现原理、Diff算法...

  • Vue-diff算法原理

    虚拟DOM 虚拟DOM(Virtual DOM)是对真实DOM的JS抽象表现,能够描述DOM结构和关系,在合适的时...

  • Vue的diff算法核心原理

    写在最前:本文转发掘金[https://juejin.cn/post/6994959998283907102]类似...

  • vue系列---vue-diff

    1.vue-diff 是什么? 提到vue的diff算法就不得不提一个名词 虚拟dom(Virtual DOM) ...

  • 我整理的网上讲解详细的文章

    讲算法的 RSA算法原理(一) RSA算法原理(二) 网络协议 iOS网络协议----HTTP/TCP/IP浅析 ...

网友评论

      本文标题:浅析vue的diff算法原理

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