源码笔记(九):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.text
vnode
为组件节点, 则执行其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
情况可能涉及到强制更新组件。