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函数比较Vnode和Old 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)
}
}
- 将
Vnode与Old Vnode的子节点提取出来,作为newCh与oldCh,它们两头各设两个指针oldStartIdx oldEndIdx newStartIdx newEndIdx - 将四者两两比较:
oldStartVnode与newStartVnode比较,如果相同,指针向后移一位oldStartVnode与newEndVnode比较,如果相同,则真实DOM中的第一个节点会移到最后,指针向中间移一位oldEndVnode与newStartVnode比较,如果相同,则真实DOM中的最后一个节点会移到最前,指针向中间移一位oldEndVnode与newEndVnode比较,如果相同,指针向前移一位
- 如果四种匹配没有一对是成功的,分为两种情况
- 无key,直接创建新的node插入真实DOM
- 有
key,会用newStartVnode的key在oldChild的key中做匹配,若匹配节点与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也就变化了