源码笔记(九):update 阶段(下)
接上文,得到 vnode 后执行 vm._update。因为 vm._vnode 存在即已经渲染过,则走更新方法:
// updates
vm.$el = vm.__patch__(prevVnode, vnode);vm.__patch__ 即 patch 方法,进入 patch 流程。
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、 tag、是否有 data 的存在(不关心内部具体的值)、是否是注释节点、是否是相同的 input type,异步则判断是否有 相同的占位符,asyncFactory 等。若相同则视为同一节点进行 patch。
patch
patch 的主要功能是将新旧 vnode 进行同级比较,然后更新真实 dom 节点和组件实例。
patch 源码
return function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) {
invokeDestroyHook(oldVnode);
}
return;
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
if (isRealElement) {
// 服务端渲染相关处理
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
// 服务端渲染相关处理
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true);
return oldVnode;
} else if (true) {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.',
);
}
}
oldVnode = emptyNodeAt(oldVnode);
} // replacing existing element
var oldElm = oldVnode.elm;
var parentElm = nodeOps.parentNode(oldElm); // create new node
createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));
if (isDef(vnode.parent)) {
var ancestor = vnode.parent;
var patchable = isPatchable(vnode);
while (ancestor) {
for (var i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
ancestor.elm = vnode.elm;
if (patchable) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, ancestor);
}
var insert = ancestor.data.hook.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
insert.fns[i$2]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.parent;
}
}
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm;
};patch 逻辑
vnode不存在oldVnode存在,则触发invodeDestoryHook进行销毁旧的节点(组件销毁用到)
vnode存在oldVnode不存在,则调用createElm创建新的节点oldVnode存在oldVnode不是真实节点且和vnode是相同节点(调用sameVnode比较),则调用patchVnode进行该vnode的补丁操作oldVnode是真实节点,略过SSR相关,则先把真实节点转为空的vnode。再调用createElm创建新的DOM节点并插入到真实的父节点中。如果vnode是组件实例,则递归更新各级父占位符节点(组件节点)的属性oldVnode的真实父dom节点存在,则调用removeVnodes将旧的节点从父节点中移除oldVnode的真实父dom节点不存在,则触发invodeDestoryHook进行销毁旧的节点
- 触发
invokeInsertHook并 返回vnode.elm真实dom节点
patchVnode
比较新旧 vnode 节点,根据不同的状态对 dom 做更新操作(添加,移动,删除)(属性更新,文本更新,子节点更新)并依次调用 prepatch, update, postpatch 等钩子。在 patch 和 updateChildren 里调用。
在编译阶段生成的一些静态子树,在这个过程 oldVnode 中由于不会改变而直接跳过比对,动态子树在比较过程中比较核心的部分就是当新旧 vnode 同时存在 children,通过 updateChildren 方法对子节点做更新。
patchVnode 为更新操作核心。
patchVnode 源码
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
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;
}
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)) {
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)) {
if (oldCh !== ch) {
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
}
} else if (isDef(ch)) {
if (true) {
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)) {
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) {
i(oldVnode, vnode);
}
}
}patchVnode 逻辑
vnode和oldVnode完全一致,直接返回- 异步占位则执行
hydrate方法或者定义isAsyncPlaceholder为true,直接返回 oldVnode和vnode都是静态节点且相同,则oldVnode.componentInstance赋给vnode.componentInstance,直接返回vnode有data属性,有prepatch则执行其data.hook.prepatch钩子更新子组件,并执行更新其属性(cbs.update);有update则执行其data.hook.update钩子vnode不是文本节点vnode和oldVnode的children都存在且不完全相等,则调用updateChildren更新子节点- 只有
vnode存在子节点,检查子节点key值后,如果oldVnode文本节点存在,则置为空。然后调用addVnodes添加这些子节点 - 只有
oldVnode存在子节点,则调用removeVnodes移除这些子节点 oldVnode和vnode都不存在子节点,但是oldVnode为文本节点或注释节点,则把elm的文本内容置为空
vnode是文本节点或注释节点且vnode.text和oldVnode.text不相等,则更新elm的文本内容为vnode.textvnode为组件节点, 则执行其data.hook.postpatch钩子
updateChildren
diff 算法核心,仅在同级的 vnode 间做 diff,递归地进行同级 vnode 的 diff,最终实现整个 DOM 树的更新。
updateChildren 源码
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;
var canMove = !removeOnly;
if (true) {
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);
}
}updateChildren 逻辑
执行 while 循环:
- 如果
oldStartVnode不存在,则将oldStartVnode设置为下一个节点 - 如果
oldEndVnode不存在,则将oldEndVnode设置为上一个节点 - 如果
oldStartVnode和newStartVnode是同一节点,则调用patchVnode并将oldStartVnode和newStartVnode设置为下一个节点 - 如果
oldEndVnode和newEndVnode是同一节点,则调用patchVnode并将oldEndVnode和newEndVnode设置为上一个节点 - 如果
oldStartVnode和newEndVnode是同一节点,则调用patchVnode,然后调用insertBefore将oldStartVnode.elm移动到oldEndVnode.elm之后,并将oldStartVnode设置为下一个节点,newEndVnode设置为上一个节点 - 如果
oldEndVnode和newStartVnode是同一节点,则调用patchVnode,然后调用insertBefore将oldEndVnode.elm移动到oldStartVnode.elm之前,并将oldEndVnode设置为上一个节点,newStartVnode设置为下一个节点 - 以上都未命中
- 如果
newStartVnode有key值,则尝试在oldChildren中查找与newStartVnode具有相同key的节点的索引位置 - 如果
newStartVnode没有key值,则尝试在oldChildren中查找与newStartVnode相同节点的索引位置 - 如果不存在索引,则调用
createElm创建一个新节点插入到oldStartVnode.elm之前 - 如果存在索引
- 如果索引对应的节点与
newStartVnode是相同节点,则调用patchVnode并将找到的节点置为undefined, 然后调用insertBefore将该节点的elm移动到oldStartVnode.elm之前 - 如果不是,则调用
createElm创建一个新的节点插入到oldStartVnode.elm之前
- 如果索引对应的节点与
- 将
newStartVnode设置为下一个节点
- 如果
当 oldChildren 和 newChildren 节点在 while 过程中如果任意一个的开始索引和结束索引重合,则表明遍历结束。
遍历结束后:
- 如果
oldStartIdx大于oldEndIdx,说明newChildren长度大于oldChildren,则需要调用addVnodes添加newStartIdx到newEndIdx之间的节点 - 如果
newStartIdx大于newEndIdx,说明oldChildren长度大于newChildren,则需要调用removeVnodes移除oldStartIdx到oldEndIdx之间的节点
updateChildren 例子
以旧 Vnode 节点为 A,B,C,D,F,新 Vnode 节点为 E,D,A,C,B,以()代表 startVnode,{}代表 endVnode,则 updateChildren 过程为:
| 第一次循环 | 第二次循环 | 第三次循环 | 第四次循环 | 第五次循环 | 跳出循环 | |
|---|---|---|---|---|---|---|
| 旧 Vnode | (A),B,C,D, | (A),B,C,D, | (A),B,C,undef, | A,(B),C,undef, | A,B,(C),undef, | A,B,C,(undef), |
| 新 Vnode | (E),D,A,C, | E,(D),A,C, | E,D,(A),C, | E,D,A,(C), | E,D,A,{(C)},B | E,D,A,{C},(B) |
| 得到的真实 Dom | E,A,B,C,D,F | E,D,A,B,C,F | E,D,A,B,C,F | E,D,A,C,F,B | E,D,A,C,F,B | E,D,A,C,B |
updateChildren 注意点
- 设置 4 指针并向中间靠拢的目的,是因为在前端需求场景中,大概率操作是在列表开头/结尾增加/删除了一个元素,减少循环,提高效率
- 设置
key可以直接查找匹配的节点是否相同,提高效率 - 首尾互相比较和
key比较都未查到相同则暴力循环查相同节点 - 整个操作都是在通过比较
vnode来操作真实dom,vnode在大多数情况下都不变
为什么不用 index 作为 key
由源码可知,当用 index 作为 key 时,则新旧 vnode 的 key 都一样,则直接命中 updateChildren 逻辑 的第 3 条进行繁琐的 patchVnode,而不会命中后面真正相同节点进行复用操作的逻辑;
除了性能损耗外,如果该 vnode 里有子节点且其 props 未变化(依赖子组件状态或临时 DOM 状态),则将不会更新子组件,则节点不更新从而引起错误。
子组件更新
对于本例,在 updateChildren 里,根据新旧 vnode 的差异,递归找到差异节点进行 patch,不再详述。
除此之外,对于第一个组件节点 App,在 patchVnode 里先执行 prepatch 钩子时,执行 updateChildComponent 更新子组件实例。updateChildComponent 里会对组件实例进行一系列的更新,包括 vm.$vnode 的更新、slot 的更新、listeners 的更新、props 的更新等等。如果有 props 且值有更新(本 demo 为 num 由 28 更新到 29):
props[key] = validateProp(key, propOptions, propsData, vm);则会重新给 num 赋值并触发对应的订阅列表,将 app 组件的 渲染 watcher 推入 queue 队列,在当前 watcher 执行完成后在执行该 watcher 的更新。
当前 Vue 即根组件 watcher 执行完成即视图更新后,回到 flushSchedulerQueue 里执行刚推入 queue 的第三个 watcher(App 组件的 渲染 watcher)开始进行子组件的更新。
子组件视图更新结束后,回到 flushSchedulerQueue 执行 callUpdatedHooks 钩子,以倒序方式执行 queue 队列里各 渲染 watcher 对应的组件的 updated 钩子,本例中为 App one updated->vue updated。
到此,vue 更新结束,整体生命周期为:vue beforeUpdate -> App one beforeUpdate -> App one updated -> vue updated。
本章小结
- 本章主要介绍了在得到新
vnode后,如何与旧vnode进行最小化差异更新真实dom。 - 更新
dom涉及到 3 个方法:patch,patchVnode,updateChildren。其中updateChildren为diff算法核心。 - 对于子组件更新执行
updateChildComponent方法,若props有变化则会重新渲染子组件。根据slot情况可能涉及到强制更新组件。