美文网首页
vue的diff算法分析

vue的diff算法分析

作者: sun_hl | 来源:发表于2021-08-12 17:04 被阅读0次

    https://blog.csdn.net/weixin_33923148/article/details/91474284

    实现步骤

    • 用JavaScript对象模拟DOM
    • 把此虚拟DOM转成真实DOM并插入页面中
    • 如果有事件发生修改了虚拟DOM
    • 比较两棵虚拟DOM树的差异,得到差异对象
    • 把差异对象应用到真正的DOM树上
    class crtateElement {
        constructor (el, attr, child) {
            this.el = el
            this.attrs = attr
            this.child = child || []
        }
        render () { 
            let virtualDOM =  document.createElement(this.el)
            // attr是个对象所以要遍历渲染
            for (var attr in this.attrs) {
                virtualDOM.setAttribute(attr, this.attrs[attr])
            }
     
            // 深度遍历child
            this.child.forEach(el => {
                console.log(el instanceof crtateElement)
                //如果子节点是一个元素的话,就调用它的render方法创建子节点的真实DOM,如果是一个字符串的话,创建一个文件节点就可以了
                // 判断一个对象是否是某个对象的实力
                let childElement = (el instanceof crtateElement) ? el.render() : document.createTextNode(el);
                virtualDOM.appendChild(childElement);
            });
            return virtualDOM
        }
    }
    function element (el, attr, child) {
        return new crtateElement(el, attr, child)
    }
     
    module.exports = element
    
    
    //用JavaScript对象结构表示DOM树的结构;然后用这个树构建一个真正的DOM树,插到文档当中。
    let element = require('./element') 
     
    let myobj = {
        "class": 'big_div'
    }
    let ul = element('div',myobj,[
        '我是文字',
        element('div',{'id': 'xiao'},['1']),
        element('div',{'id': 'xiao1'},['2']),
        element('div',{'id': 'xiao2'},['3']),
    ])
    console.log(ul)
    ul = ul.render()
    document.body.appendChild(ul)
    
    

    DOM DIFF

    https://segmentfault.com/a/1190000008782928
    复杂度为O(n)。
    diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方,最后用patch记录的消息去局部更新Dom。

    比较两棵DOM树的差异是Virtual DOM算法最核心的部分.简单的说就是新旧虚拟dom 的比较,如果有差异就以新的为准,然后再插入的真实的dom中,重新渲染。 借网络一张图片说明:

    比较只会在同层级进行, 不会跨层级比较。
    比较后会出现四种情况:
    1、此节点是否被移除 -> 添加新的节点
    2、属性是否被改变 -> 旧属性改为新属性
    3、文本内容被改变-> 旧内容改为新内容
    4、节点要被整个替换 -> 结构完全不相同 移除整个替换

    11.png

    核心源代码:

    // sameVode用来判断新旧节点是否一致,当我们确定两个节点值得比较时,我们会进入到patchNode方法。
    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
    }
    

    patchVnode这个函数做了以下事情:

    • 判断Vnode和oldVnode是否相同,如果是,那么直接return;
    • 如果他们都有文本节点并且不相等,那么将更新为Vnode的文本节点。
    • 如果oldVnode有子节点而Vnode没有,则删除el的子节点
    • 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el。
    • 如果两者都有子节点,则执行updateChildren函数比较子节点,而这个函数也是diff逻辑最多的一步。
    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)
            }
        }
    }
    

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

    从头得知,新旧节点分别都2个指针,分别指向各自的头部与尾部。那么接下来就开始操作了。

    1、当新旧节点的头部值得对比,进入patchNode方法,同时各自的头部指针+1;

    2、当新旧节点的尾部值得对比,进入patchNode方法,同时各自的尾部指针-1;

    3、当oldStartVnode,newEndVnode值得对比,说明oldStartVnode已经跑到了后面,那么就将oldStartVnode.el移到oldEndVnode.el的后边。oldStartIdx+1,newEndIdx-1;

    4、当oldEndVnode,newStartVnode值得对比,说明oldEndVnode已经跑到了前面,那么就将oldEndVnode.el移到oldStartVnode.el的前边。oldEndIdx-1,newStartIdx+1;

    5、当以上4种对比都不成立时,通过newStartVnode.key 看是否能在oldVnode中找到,如果没有则新建节点,如果有则对比新旧节点中相同key的Node,newStartIdx+1。

    当循环结束时,这时候会有两种情况。

    1、oldStartIdx > oldEndIdx,可以认为oldVnode对比完毕,当然也有可能 newVnode也刚好对比完,一样归为此类。此时newStartIdx和newEndIdx之间的vnode是新增的,调用addVnodes,把他们全部插进before的后边。

    2、newStartIdx > newEndIdx,可以认为newVnode先遍历完,oldVnode还有节点。此时oldStartIdx和oldEndIdx之间的vnode在新的子节点里已经不存在了,调用removeVnodes将它们从dom里删除。

    相关文章

      网友评论

          本文标题:vue的diff算法分析

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