最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue - key/diff

    正文概述 掘金(周小亮)   2021-06-03   383

    参考原文-掘金-详解 vue 的 diff 算法

    key 的特殊属性,主要用于 Vue 的虚拟 DOM 算法。在新旧 vnode 的对比中,如果不使用 keyVue 会最大限度的减少动态元素并尽可能的就地复用。这会导致一些渲染错误。而且当我们想要触发一些 transition 过渡动画的时候,会出现不生效的情况。因为 vue 判断该元素并没有改变。

    而使用 key 的时候,它会基于 key 的变化,重新计算排序元素序列。并且会移除 key 不存在的元素。

    其原理在于 Vuediff 算法。而我们的 key 起作用在其 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;
    }
    
    // 作者:windlany
    // 链接:https://juejin.im/post/6844903607913938951
    

    Vue - key/diff

    同层比较

    Vue - key/diff

    如果两个节点是一样的,就执行 patchVnode() 方法进一步比较。

    如果两个节点不一样,直接用新的 Vnode 替换旧的。如果两个父节点不一样,但是其子节点都是一样的,也不会进行子节点比较。这就是同层比较。

    patchNode()

    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)
        	}
        }
    }
    
    // 作者:windlany
    // 链接:https://juejin.im/post/6844903607913938951
    

    以上就是根据不同情况进行不同处理了。

    • 如果新旧节点指向同一个对象,直接 return 什么都不做。
    • 如果都有文本节点,并且不一样,则用新的替换旧的。
    • 如果 oldVnode 有子节点,而新的 Vnode 没有,则删除该子节点。
    • 反过来,如果 Vnode 有子节点,而 oldVnode 没有,则将该子节点添加。
    • 如果都有子节点,则进行 updateChildren() 比较。

    diff 算法就在 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]
                // oldS 与 S 比较
            }else if (sameVnode(oldStartVnode, newStartVnode)) {
                patchVnode(oldStartVnode, newStartVnode)
                oldStartVnode = oldCh[++oldStartIdx]
                newStartVnode = newCh[++newStartIdx]
                // oldE 与 E 比较
            }else if (sameVnode(oldEndVnode, newEndVnode)) {
                patchVnode(oldEndVnode, newEndVnode)
                oldEndVnode = oldCh[--oldEndIdx]
                newEndVnode = newCh[--newEndIdx]
                // oldS 与 E 比较
            }else if (sameVnode(oldStartVnode, newEndVnode)) {
                patchVnode(oldStartVnode, newEndVnode)
                api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
                oldStartVnode = oldCh[++oldStartIdx]
                newEndVnode = newCh[--newEndIdx]
                // oldE 与 S 比较
            }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 的子节点 VnodeChildren(下文称Vch)oldNode 的子节点 oldNodeChildren(下文称oldCh) 提取出来。
    • VcholdCh 各有两个头尾的变量 startIdxendIdx 。 他们的两个变量相互比较。一共有 4 种比较方式。如果 4 种都没有匹配,再看是否设置了 key。如果设置了,就会用 key 进行比较。在比较的过程中,变量会往中间靠,一旦 startIdx > endIdx 表明 oldChVch 至少有一个已经遍历完了,就会结束比较。

    接下来上图:

    以下是 VnodeoldVnode

    Vue - key/diff

    将其取出来,并分别赋予头尾变量。

    Vue - key/diff

    oldS 将会与 SEsameNode 比较。oldE 将会与 SEsameNode 比较。

    • 一旦有一对匹配上了,那么真实的 DOM 会移动到与之对应的节点。这两个指针会像中间移动。
    • 如果 4 组都没有匹配上,分两种情况。
      • 如果新旧子节点都存在 key , 那么会根据 oldChkey 生成一张 hash表 。用 Skey 与之做对比。匹配成功就去判断这 S 和 该节点是否 sameNode 。如果是,就在真实 DOM 中将成功的节点移到最前面。否则将 S 对应生成的节点插入到 DOM 中对应的 oldS 位置。 S 指针向中间移动,被匹配 old 中的节点置为 null
      • 如果没有 key, 则直接将 S 生成新的节点插入真实 DOM

    也就是,没有 key 只会做 4 中匹配,就算指针中间有可复用的节点,也不能被复用。

    接下来,看一下上图做匹配的过程:

    Vue - key/diff

    DOM 中的节点 a 放到第一个。已经是第一个了就不管了。

    Vue - key/diff

    DOM 中的节点 b 放到最后一个。

    Vue - key/diff

    本来是要将 c 移动到 S 对应的位置。可是真实 DOM 中节点c 已经是在第二个位置了。所以什么都不做。

    将剩余的节点 d 按照自己的 index 插入到 DOM 中去。

    匹配结束有两种情况。

    • oldS > oldE 说明 oldCh 先遍历完,则需要将多余的 Vch 根据 index 添加到 DOM 中。
    • S > E 说明 Vch 先遍历完。则需要删除 oldCh 中多余的节点。

    起源地下载网 » Vue - key/diff

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元