上文可知,render 函数会生成深层次的虚拟 dom, 若是页面第一次渲染,直接插入到真实dom 中,如果不是第一次,那么就要进行新老 dom树对比。
首先思考三个问题:
对比原则是什么?
同一层之间进行对比,不能跨层级, 比如:
// 旧的:
<div>
<div>
<p></p>
</div>
</div>
// 新的:
<div>
<div></div>
<p></p>
</div>
复制代码
新老dom区别就是p标签的位置,看起老可以直接把p标签拿出来,直接复用就可以了,但是真正对比的时候却不是这样的,而是删除旧的里面的p标签,插入新的p标签,原因就是diff算法的对比逻辑是在同一层级展开的。
怎么判断节点是否相同可以复用?
源码里通过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)
)
)
)
}
复制代码
这里暂且理解为,如果存在循环且有key值得话,比较key和tagName是否相同,如果是普通节点不存在key值得话,比较tagName是否相同。
vnode格式是怎样的呢?
// body下的 <div id="v" class="classA"><div> 对应的 oldVnode 就是
{
el: div //对真实的节点的引用,本例中就是document.querySelector('#id.classA')
tagName: 'DIV', //节点的标签
sel: 'div#v.classA' //节点的选择器
data: null, // 一个存储节点属性的对象,对应节点的el[prop]属性,例如onclick , style
children: [], //存储子节点的数组,每个子节点也是vnode结构
text: null, //如果是文本节点,对应文本节点的textContent,否则为null
}
复制代码
接下来继续上文,分析patch过程:
patch过程
vue源码中是这么做的:
我们可以看到,patch方法中对比了 oldVnode 和vnode是否相似,相似就进行进一步对比,执行patchVnode方法,不相似进行了一大堆操作。。。
我们将patch代码简化为:
function patch(oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl) // 获取oldVnodede 父节点
createEle(vnode) // 创建vnode,这里是dom操作
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将vnode插入到oldVnode的后面
api.removeChild(parentEle, oldVnode.el) // 删除oldVnode
oldVnode = null
}
}
return vnode
}
复制代码
总结一下:
- patch方法中对比了 oldVnode 和vnode是否相似,相似就进行进一步对比,执行patchVnode方法
- 不相似,获取oldVnodede 父节点,创建vnode,这里是dom操作
- parentEle父节点不为空,将vnode插入到父节点下面的oldVnode的后面
- 删除oldVnode
patchVnode 节点对比过程
function patchVnode (oldVnode, vnode) {
var oldCh = oldVnode.children;
var ch = vnode.children;
console.log(90, vnode);
if (isDef(data) && isPatchable(vnode)) {
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)) {
if (isDef(oldCh) && isDef(ch)) { // 新老节点都有children,则比较children, updateChildren(diff算法)
if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
} else if (isDef(ch)) { // 新节点有子节点,老节点没有子节点,老节点就新增
{
checkDuplicateKeys(ch);
}
if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) { // 新节点没有子节点,老节点有子节点,老节点就删除
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) { //新、老节点都没children,并且老节点有text,则删除老节点text
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text); // 新节点有文本,新老节点文本不一样,替换文本
}
}
复制代码
节点对比:
- 新节点有文本,新老节点文本不一样,替换文本
- 否则,新老节点都有children,则比较children, updateChildren(diff算法)
- 否则,新节点有子节点,老节点没有子节点,老节点就新增
- 否则,新节点没有子节点,老节点有子节点,老节点就删除
- 否则,新、老节点都没children,并且老节点有text,则删除老节点text
diff算法
diff算法用于对比新老节点的children
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
var canMove = !removeOnly;
{
checkDuplicateKeys(newCh);
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
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
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 {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
}
复制代码
总结一下过程:
- 前前(old, new): 前前节点相同,则进行节点对比patchVnode, 前前index + 1
- 后后(old, new): 后后节点相同,则进行节点对比patchVnode, 后后index - 1
- 前后(old, new): 前后相同,则将旧节点开始节点移动到旧节点结束节点的后面,前面旧节点index + 1, 后面老节点index - 1
- 后前(old, new): 后前相同,将旧节点结束节点移动到旧节点开始节点的前面,后面旧节点index -1, 前面新节点index + 1
-
当以上情况都不符合时,生成一个key与旧vnode对应的哈希表,最后生成的对象就是以children的key为属性,递增的数字为属性值的对象
- 如果有key,则在哈希表里面找到与新的开始节点相同的旧的里面对应key相同的旧的index,没有key则遍历oldVnode找出相似的节点对应的index
- 如果没找到相似的,则 createElm 创建一个新的节点插入到 parentElm 中;
- 如果找到相似的 oldVnode,则 patchNode 后,再在真实 dom 中将该 oldVnode 移动到oldStartVnode.elm前
-
oldStartIdx > oldEndIdx,说明 oldNode 到头了,但是 newNode 中还有多余的,那么就把newNode中多余的全部都addVnodes
- 如果 newStartIdx > newEndIdx,说明了 newNode 到头了,但是 oldVnode 中还有多余的,因此 removeVnodes