vue中的diff算法

83 阅读3分钟

vue中的diff算法

一、vue是如何更新节点的

众所周知渲染真实DOM是件十分消耗性能的过程,当修改某个数据时,diff算法能够帮助我们只修改对应的DOM,而不是更新整个DOM树。

vue提出了虚拟DOM(virtual DOM),虚拟DOM就是根据真实DOM的数据,以对象形式模拟的树形结构。

真实DOM

<ul>
  <li>1</li>
  <li>2</li>
</ul>

它对应的虚拟DOM对象(伪代码)

var Vnode = {
  tag: 'ul',
  children: [
    { tag: 'li', children: [ { vnode: { text: '1' }}]  },
    { tag: 'li', children: [ { vnode: { text: '2' }}]  },
  ]
}

diff比较只会在同层比较,将更新后的新Vnode与旧的Old Vnode比较,只将变化的地方更新在真实DOM上,同时将Old Vnode变成Vnode。在vue中这个过程叫做给真实的DOM打补丁

二、diff流程

调用patch函数比较VnodeOld Vnode,如果不一样,直接返回Vnode,替换掉真实DOM。
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
}
如果是相同节点,则调用patchVnode方法进行进一步比较
patchVnode (oldVnode, vnode) {
  	// 找到对应的真实DOM 
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    // 判断Vnode和oldVnode是否指向同一个对象
    if (oldVnode === vnode) return
    // 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点
    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函数比较子节点
            updateChildren(el, oldCh, ch)
    	}else if (ch){
          	// 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
            createEle(vnode) //create el's children dom
    	}else if (oldCh){
            // 如果oldVnode有子节点而Vnode没有,则删除el的子节点
            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)
    }
}
  • VnodeOld Vnode的子节点提取出来,作为newCholdCh,它们两头各设两个指针oldStartIdx oldEndIdx newStartIdx newEndIdx
  • 将四者两两比较:
    • oldStartVnodenewStartVnode比较,如果相同,指针向后移一位
    • oldStartVnodenewEndVnode比较,如果相同,则真实DOM中的第一个节点会移到最后,指针向中间移一位
    • oldEndVnodenewStartVnode比较,如果相同,则真实DOM中的最后一个节点会移到最前,指针向中间移一位
    • oldEndVnodenewEndVnode比较,如果相同,指针向前移一位
  • 如果四种匹配没有一对是成功的,分为两种情况
    • 无key,直接创建新的node插入真实DOM
    • key,会用newStartVnodekeyoldChildkey中做匹配,若匹配节点与newStartVnode相同,就在真实DOM中将该节点移到最前面,否则将newStartVnode生成对应的节点插入到dom中对应的oldStartVnode位置,newStartVnode指针向中间移动,被匹配old中的节点置为null。
  • 这个匹配过程的结束有两个条件:
    • oldStartVnode> oldEndtVnode表示oldCh先遍历完,那么就将多余的vCh根据index添加到dom中去
    • newStartVnode > newEndVnode表示vCh先遍历完,那么就在真实dom中将区间为[oldS, oldE]的多余节点删掉
  • diff 结束

三、vue中key的作用

在上述diff算法中,使用key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,减少DOM操作。

为什么不能用index作为key

当用index作为key的时候,删除节点后面的所有节点都会导致重新渲染,因为index变化了,key也就变化了