美文网首页
vue知识点-3--diff算法 vnode

vue知识点-3--diff算法 vnode

作者: 前端劝退师_ty | 来源:发表于2020-01-02 19:07 被阅读0次

    虚拟dom是通过状态生成一个虚拟节点树,然后使用虚拟节点数进行渲染,在渲染之前,会使用新生成的虚拟节点数和上一个上传的虚拟节点数进行对比,只渲染不同的部分。
    之前说过vue的变化侦测,当属性变化时vue会通知到对应的组件,然后组件内部通过虚拟dom进行对比渲染。

    编写vue文件模板-->render渲染函数-->生成vnode-->渲染视图

    VNode是一个类,可以生成不同的vnode实例,vonde只是一个对象,不要想象的太神秘。
    新旧两个vnode对比的过程就是diff算法,内部函数是patch(补丁)

    源码来源 https://www.cnblogs.com/wind-lanyan/p/9061684.html 写的很不错这篇文章

    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就是对比新旧两个vnode是否相同 如果相同进行下一次对比,否则直接替换oldVnode,
    diff是逐层对比,也是同层对比,判断两个vnode是否相同主要是看他们的key,tag,isComment,vode上的data。
    如果父级不相同也就不会再进入子集对比了哦!
    insertBefore 传入三个值,父节点,移动的节点(真正的内容),要移动到的地方(会被替换掉)

    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必须相同
      )
    }
    

    patchVnode
    判断Vnode和oldVnode是否指向同一个对象,如果是,那么直接return;
    如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
    如果oldVnode有子节点而Vnode没有,则删除el的子节点
    如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
    如果两者都有子节点,则执行updateChildren函数比较子节点
    上面这几条看代码基本能读懂意思

    updateChildren 就是具体的规则来对比新老节点的复用问题,复用就是指原本有的子节点被他找到了,他发现新老对比只是移动了位置就只直接移动内容位置,或是位置都没有变(位置没变还是会调用回patchVnode)。
    updateChildren内部的具体代码还是要调用回来 patchVnode
    patchVnode 中的createEle、removeChildren、setTextContent 就已经是具体操作dom的地方了,仔细思考层层递归,到了最底层多半就是这些情况了,要不新增、要不删除、要不替换文字。

    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 就是对比子级用的。
    oldCh和vCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和vCh至少有一个已经遍历完了,就会结束比较。 原博客的介绍,感觉有些太概括了。

    再仔细的说一下

    updateChildren 对比子层级的时候是从两边向中间循环。
    所以开头的这些变量oldStartIdx、newStartIdx、oldEndIdx、oldStartVnode、newEndIdx、newStartVnode....是用来记录位置的,
    新vnode左侧第一个id 右侧第一个id 老vnode右侧第一个...
    四种对比方式是 新前-旧前 新后-旧后 新后-旧前 新前-旧后
    是这么对比的,如果判断为相等就去更改内容或是移动位置。
    如果四种方式都没比较成功就用key循环对比。
    最后根据前面的id 判断哪个先循环完毕了,
    如果旧vnode循环完毕了新的vnode还要节点就说明剩下的都是新增的
    如果新vnode循环完毕就vnode还要节点就说明旧的上剩余的节点都是需要删除的。

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

    核心源码解读就是上面这些。

    再说下自己的理解。其实对比新旧vnode是否相同就是直接通过sameVnode传递进新老节点对比即可,但是为什么updateChildren内要写这么多代码,四种对比方式
    新前-旧前 新后-旧后 新后-旧前 新前-旧后,其实用最笨的方法直接循环对比就行,但是这样对比要双重循环,新老双向循环这样就很耗费性能,这四种方式就可以看成在碰运气,往四个方向比较一下,比较上了就找到了,比较不上再去循环用key比较。但是你不要感觉这样没有道理,真实场景思考一下,实际的界面上位置基本不会大变这四种情况碰上的概率还是很高的。

    在思考一个问题。为什么vue推荐我们循环时要加上key,可以优化性能复用节点。

    当 Vue 更新已使用 v-for 渲染的元素列表时,默认会采用“就地填充”策略。如果数据项的顺序发生了变化,不是移动 DOM 元素来匹配列表项的顺序,Vue 会将每个元素填充到恰当的位置,并且确保最终反映为,在该特定索引处放置应该呈现的内容
    

    官网上是这么写的
    再来解读下 updateChildren 关于key的部分


    058F80DB095860A947105DE79C9605C5.jpg

    diff算法基本就是这样子。

    相关文章

      网友评论

          本文标题:vue知识点-3--diff算法 vnode

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