上篇文章我们介绍了组件的渲染流程,本篇文章我们来介绍响应式数据变化后组件的更新渲染流程。最后有不看文章的分析总结图。
为了方便介绍流程,我们这里举一个例子:
App
组件中有一个Hello
组件,并且赋值msg
这个prop值给Hello
组件;msg
为Vue 3时,App
组件中有li
标签数组显示vue3.feature
,即显示Vue 3
的新特性,当msg
为Vue 2时则不显示;App
组件中有一个按钮切换msg
的值。
App 组件显示:
- {{ item }}
Hello 组件显示:{{ msg }}
componentUpdateFn
开启组件重新渲染我们上篇文章提到过组件挂载的时候会创建一个副作用渲染函数componentUpdateFn
,这个函数在响应式数据变化后则会被调用。
数据变化后为什么就会引发副作用渲染函数的调用?这是
Vue 3.0
响应式系统的相关内容,后续介绍。目前知道是这个逻辑就行。
const componentUpdateFn = () => {
// 1.
if (!instance.isMounted) {
instance.isMounted = true
} else {
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
// 2.
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
// 3
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
// 4
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el!)!,
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
}
}
patch
更新组件的逻辑const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 1.
if (n1 === n2) {
return
}
// 2.
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 3.
const { type, ref, shapeFlag } = n2
switch (type) {
// 省略 ...
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
// 省略 ...
}
}
updateComponent
App组件对象的子树VNode
的第一个子节点VNode
是Hello组件对象的VNode
,其prop值变化了,所以Hello组件对象需要更新渲染,接下来我们就来看看Hello子组件的更新逻辑processComponent
。
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
const instance = (n2.component = n1.component)!
// 1.
if (shouldUpdateComponent(n1, n2, optimized)) {
// 2.
instance.next = n2
// 3.
invalidateJob(instance.update)
// 4.
instance.update()
} else {
// 2.
n2.component = n1.component
n2.el = n1.el
instance.vnode = n2
}
}
next
值?VNode
, 所有需要赋值给子组件对象让其知道如何更新渲染。componentUpdateFn
的和组件自身触发的区别let { next, bu, u, parent, vnode } = instance
let originNext = next
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
区别就在于父组件对象触发的子组件的
VNode
有next
值,此时需要执行updateComponentPreRender
,从而在渲染前完成props
,slot
等属性的赋值;
updateComponentPreRender
方法?updateComponentPreRender
方法,所以自身触发的情景下只需要更新一些属性值就行,要么通过updateComponentPreRender
,要么直接给设置vnode
属性值。patchElement
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// 1.
patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
// 2.
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds,
false
)
}
这个方法特别的长,功能是通过
patchProps
更新props
,style
,class
,event
等;通过patchChildren
更新子节点。
接下来我们就来重点介绍下子节点的更新逻辑。
patchChildren
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized = false
) => {
// 1.
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
if (patchFlag > 0) {
// 2.
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// unkeyed
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
}
}
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 3.
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 4.
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// 5.
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
}
普通元素节点的子节点有三种情况:
子节点类型 例子 数组子节点
- 1
- 1
- 1
文本子节点
文本空子节点
patchChildren
针对这三种情况进行分别处理, 9种情况:
行-旧节点,列-新节点 数组子节点 文本子节点 空子节点 数组子节点 diff比对 卸载数组节点,设置文本 卸载数组节点 文本子节点 将文本节点替换为数组节点 文本替换 去掉文本节点 空子节点 挂载数组子节点 设置文本
patchUnkeyedChildren
const patchUnkeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.min(oldLength, newLength)
let i
for (i = 0; i < commonLength; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
patch(
c1[i],
nextChild,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
if (oldLength > newLength) {
// remove old
unmountChildren(
c1,
parentComponent,
parentSuspense,
true,
false,
commonLength
)
} else {
// mount new
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
commonLength
)
}
}
这个方法逻辑简单:先将两个数组从前往后逐个
patch
,当某个数组对比完成后,如果新的子节点数组还有元素就将剩下的节点进行mountChildren
挂载,如果是旧节点有剩余的则unmountChildren
卸载。
这个方法简单,但是效率比较低,。我们接下来分析高效的比对方法。
patchKeyedChildren
这个逻辑很长,我们分拆来分析:
旧节点 (a b) c
新节点 (a b) d e
先从两个数组的头部开始比对,如果节点是相同的VNode
类型,执行patch
更新节点,否则同步结束。
上面例子中第三个节点的时 同步头部节点这一逻辑结束。
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index
// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
break
}
i++
}
// a (b c)
// d e (b c)
先从两个数组的尾部开始比对,如果节点是相同的VNode
类型,执行patch
更新节点,否则同步尾部结束。
上面例子中倒数第三个节点的时 同步尾部部节点这一逻辑结束。
// 2. sync from end
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
break
}
e1--
e2--
}
(a b)
(a b) c
if (i > e1) { // 旧子节点到了尾部
if (i <= e2) { // 新子节点剩余节点
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
// 逐个挂载
while (i <= e2) {
patch(
null,
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
i++
}
}
}
(a b) c (d e)
(a b) (d e)
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// [i … e1 + 1]: a b [c d j] f g
// [i … e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
// 5.1 build key:index map for newChildren
const keyToNewIndexMap: Map = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (nextChild.key != null) {
if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
warn(
`Duplicate keys found during update:`,
JSON.stringify(nextChild.key),
`Make sure keys are unique.`
)
}
keyToNewIndexMap.set(nextChild.key, i)
}
}
结果:
{
{"e" => 2},
{"d" => 3},
{"c" => 4},
{"h" => 5}
}
patch
更新,并且移除不在新子序列中的节点,并且确定序列是否有排列顺序的变化。
- 建一个
newIndexToOldIndexMap
数组,数组长度是未知新子序列的长度,每个元素的初始值为0,当最后处理完还是0,那说明这个节点是新添加的节点;- 正序遍历旧子序列查找旧子序列节点在新子序列中的索引,如果找不到说明新子序列中没有该节点,这个节点需要卸载;如果找到了,就将其在旧子序列中的索引更新到newIndexToOldIndexMap`中,索引加了1;
- 利用
maxNewIndexSoFar
来计算新子节点的顺序是否有更换,如果有更换将moved
设置为true;- 如果新子节点序列已经遍历完成,旧子节点还有元素,直接卸载节点即可。
let j
let patched = 0
const toBePatched = e2 - s2 + 1
let moved = false
// used to track whether any node has moved
let maxNewIndexSoFar = 0
// works as Map
// Note that oldIndex is offset by +1
// and oldIndex = 0 is a special value indicating the new node has
// no corresponding old node.
// used for determining longest stable subsequence
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
if (patched >= toBePatched) {
// all new children have been patched so this can only be a removal
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
if (prevChild.key != null) {
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
// key-less node, try to locate a key-less node of the same type
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
newIndex = j
break
}
}
}
if (newIndex === undefined) {
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
patched++
}
}
结果:
newIndexToOldIndexMap: [0, 4, 3, 0] // d在旧子节点的索引是3,c在旧子节点的所以为2,e,h都是新增的节点
moved: true
- 如果
moved
为true, 则求解最大递增子序列increasingNewIndexSequence
,最大递增子序列能够让移动的次数最小化;
本例子中得到的的值为[0, 2]
,表示newIndexToOldIndexMap
对应的0, 3
。- 倒序遍历新子节点,如果newIndexToOldIndexMap对应的索引的值为0,说明新增的节点,进行挂载;
- 倒序遍历新子节点,如果碰到了不是
increasingNewIndexSequence
中的对应索引下元素的值值则需要移动,否则不进行操作;
我们用上面的例子解释下:
循环次数 | 新子节点索引 | 新子节点 | increasingNewIndexSequence的索引 | increasingNewIndexSequence[索引] | newIndexToOldIndexMap[循环次数] | 进行的操作 |
---|---|---|---|---|---|---|
1 | 5 | h | 1 | 3 | 0 | 直接挂载h |
2 | 4 | c | 1 | 3 | 3 | c不进行操作,将increasingNewIndexSequence的索引-1,变为0 |
3 | 3 | d | 0 | 0 | 4 | 取到元素d,移动到c前面 |
4 | 2 | e | 0 | 0 | 0 | 直接挂载e |