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

虚拟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) // 当标签是的时候,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的部分


vue知识点-3--diff算法 vnode_第1张图片
058F80DB095860A947105DE79C9605C5.jpg

diff算法基本就是这样子。

你可能感兴趣的:(vue知识点-3--diff算法 vnode)