虚拟DOM(Virtual Dom),也就是我们常说的虚拟节点,是用JS对象来模拟真实DOM中的节点,该对象包含了真实DOM的结构及其属性,用于对比虚拟DOM和真实DOM的差异,从而进行局部渲染来达到优化性能的目的,避免频繁对真实DOM操作造成布局重排、回流重绘等性能开销。
1、diff算法:
渲染真实DOM的开销是很大的,轻微的操作都可能导致页面重新排版,非常耗性能。 相对于DOM对象,js对象处理起来更快,而且更简单。 通过diff算法对比新旧vdom之间的差异,可以批量的、最小化的执行 dom操作,从而提高性能。
2、diff时间复杂度分析:
常规:O(n^3) 遍历树1; 遍历树2; 排序; 1000个节点,十亿的量级。
vue diff:O(n) 只比较同一层级 ;tag不相同,直接删掉重建; 通过key来标识区分相同节点。
3、vue diff key优化特点
{{a}}
旧节点:A、B、C、D
新节点:A、E、B、C、D
节点没设置key:如下,将会对4个节点做更新操作
节点设置了key:如下,只对1个节点做更新操作
diff算法核心原理:
1、sameVnode:判断两个节点是否为同一个节点
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
判断条件:
1.1 两个节点key是否相同(两个key都没有,即 undefined === undefined)
1.2 两个节点tag标签名是否一样
1.3 两个节点是否都为注释节点
1.4 两个节点的data isDef是否都相等(isDef:data !== undefined && data !== null)
1.5 两个节点的input类型是否相同
1.6 节点a是否为异步占位
1.7 两个节点的异步函数是否相等
1.8节点b异步函数的error是否为空(isUndef:data === undefined && data === null)
2、patchVnode:更新节点
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 新旧node相同,return
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode);
}
var elm = vnode.elm = oldVnode.elm;
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
} else {
vnode.isAsyncPlaceholder = true;
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return
}
var i;
var data = vnode.data;
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}
var oldCh = oldVnode.children;
var ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
// 调用update回调以及update钩子
for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
}
if (isUndef(vnode.text)) {
// 新节点text文本属性不存在
if (isDef(oldCh) && isDef(ch)) {
// 新旧节点都有子节点,调用updateChildren重排子节点
if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
} else if (isDef(ch)) {
// 只有新节点有子节点,调用addVnodes添加子节点
{
checkDuplicateKeys(ch);
}
if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 只有旧节点有子节点,调用removeVnodes添加子节点
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 只有旧节点有文本,调用setTextContent清空文本
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 新旧节为文本节点并且文本不一致,调用setTextContent清空文本
nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
}
}
更新条件:
2.1 新旧节点都有子节点,调用updateChildren重排子节点
2.2 只有新节点有子节点,调用addVnodes添加子节点
2.3 只有旧节点有子节点,调用removeVnodes移除子节点
2.4 如果是文本节点,调用setTextContent更新节点文本内容
3、updateChildren:更新子节点
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
var oldStartIdx = 0; // 旧子节点start下标(游标)
var newStartIdx = 0; // 新子节点start下标(游标)
var oldEndIdx = oldCh.length - 1; // 旧子节点end下标(游标)
var newEndIdx = newCh.length - 1; // 新子节点end下标(游标)
var oldStartVnode = oldCh[0]; // 旧start节点(旧头)
var newStartVnode = newCh[0]; // 新start节点(新头)
var oldEndVnode = oldCh[oldEndIdx]; // 旧end节点(旧尾)
var newEndVnode = newCh[newEndIdx]; // 新end节点(新尾)
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
// removeOnly is a special flag used only by
// to ensure removed elements stay in correct relative positions
// during leaving transitions
var canMove = !removeOnly;
{
checkDuplicateKeys(newCh);
}
// oldStartIdx大于oldEndIdx,或者newStartIdx大于newEndIdx时候跳出循环
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 旧头不存在,oldStartIdx++
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// 旧尾不存在,oldEndIdx--
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 旧头、新头相同,更新节点,oldStartVnode++、newStartVnode++
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 旧尾、新尾相同,更新节点,oldEndIdx--、newEndIdx--
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 旧头、新尾相同,更新节点,并且将旧头移到最后,oldStartIdx++、newEndIdx--
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 旧尾、新头相同,更新节点,并且将旧尾移到最前面,newStartIdx++、oldEndIdx--
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) { // New element
// 遍历发现旧子节点中没有和新节点相同的节点,创建新节点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
// 遍历节点和新头相同,更新节点,移动节点替换
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// 节点不同,新建节点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx > oldEndIdx) {
// 旧子节点先遍历完,新子节点还有剩,调用addVnodes添加节点
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
// 新子节点先遍历完,旧子节点还有剩,调用removeVnodes添加节点
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
}
3.1 新旧子节点未遍历完毕
3.1.1 旧头不存在,将旧头游标往后移一位
3.1.2 旧尾不存在,将旧尾游标往前移一位
3.1.3 旧头、新头相同,更新节点,并将头部游标往后移一位
3.1.4 旧尾、新尾相同,更新节点,并将尾部游标往前移一位
3.1.5 旧头、新尾相同,更新节点,并且将旧头移到尾部,旧头游标往后移一位,新尾游标往前移一位
3.1.6 旧尾、新头相同,更新节点,并且将旧尾移到头部,新头游标往后移一位,旧尾游标往前移一位
3.1.7 拿新头遍历旧子节点,找不到则新建一个节点;找到判断节点是否相同,相同则更新节点,移动老节点,不同则新建一个节点
3.2 新旧子节点遍历完毕
3.2.1 旧子节点先遍历完毕,说明有新增节点,批量增加
3.2.2 新子节点先遍历完毕,说明有节点删除,批量移除