持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情
前言
Vue 在通过 VNode 更新节点的时候,会对比新旧两个节点的VNode,然后通过对比结果找出差异的属性或节点进行按需更细,他会尽量 减少这个过程的开销,这个过程就是patch
/**
n1 与 n2 是待比较的两个节点
n1 为旧节点
n2 为新节点
container 是新节点的容器
anchor 是一个锚点,用来标识当我们对新旧节点做增删或移动等操作时,以哪个节点为参照物。
optimized 参数是是否开启优化模式的标识
*/
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
/** 忽略 */
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
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
)
}
}
}
processCommentNode 处理注释节点
当类型为 Comment 时,说明是注释节点。
const processCommentNode: ProcessTextOrCommentFn = (
n1,
n2,
container,
anchor
) => {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateComment((n2.children as string) || '')),
container,
anchor
)
} else {
n2.el = n1.el
}
}
- 如果没有旧节点,则先创建一个注释节点,然后插入到 container 中
- 因为 vue3 里面是不支持注释节点的动态替换的,所以当两个新旧节点都有数据的时候,是会直接把旧的内容直接赋予给新的节点。
StaticNode 静态节点
当类型为 Static 时,就是静态节点,如果没有旧节点,则直接通过mountStaticNode挂载新节点。
mountStaticNode = (
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
isSVG: boolean
) => {
//静态节点仅在与编译器dom/运行时dom一起使用时才存在
//这保证了hostInsertStaticContent的存在。
;[n2.el, n2.anchor] = hostInsertStaticContent!( // 创建静态节点,并且插入内容
n2.children as string,
container,
anchor,
isSVG
)
}
否则进行 patch 更新,新旧节点的子节点不一致时,移除旧的的静态节点,插入新的静态节点。一致时,直接把旧节点复制给新节点
processFragment 片段
fragment 是 vue3 一个新组件,Vue2.x 时组件必须要一个根组件,在Vue3 中可以不用根标签,因为vue3会自动将多个节点用fragment包裹。
当旧节点n1不存在的时候,直接进行mountChildren操作,将内部的内容遍历挂载。 如果旧节点和新节点都有内容的情况下,就会把新旧节点的内容进行对比更新,由于可以接受v-for之类的动态内容,所以还将其进行不同的处理。
processElement 节点
processElement 这个方法本身的逻辑很简单,如果存在旧节点,则继续通过 patch 比较新旧两个节点,否则直接挂载新节点。关键在于
patch 两个节点的逻辑 patchElement。这个方法比较长,我们只摘出关键的 diff 算法部分
if (patchFlag > 0) {
if (patchFlag & PatchFlags.FULL_PROPS) {
// 如果元素的 props 中含有动态的 key,则需要全量比较
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG)
}
}
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
if (patchFlag & PatchFlags.PROPS) {
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
if (
next !== prev ||
(hostForcePatchProp && hostForcePatchProp(el, key))
) {
hostPatchProp(
el,
key,
prev,
next,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
} else if (!optimized && dynamicChildren == null) {
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
}
这边出现一个属性 patchFlag,他的作用是优化了 patch 的过程。
当两个节点是同一类型时,比如都是 div,虽然类型相同,但是可能新节点的属性也发生了变化,所以此时我们还需要对节点的属性进行遍历,才能有效的判断是否需要更新。
<div id="bar" :class="foo">Hello World</div>
对于这么一个元素,id 和 文本都是静态的,不会发生改变,只有 class 是动态的,如果我们还是一样对新旧两个节点的 id、内容、class 都进行遍历对比,
会造成不必要的开销,因为只有class是动态的。而现在拥有了 patchFlag 在生成 AST 树后,经过转换器遍历各个节点时,就会根据节点的特点打上对应的 patchFlag。
而在 patch 的过程中,仅仅会处理 class 这一个 props,而并不是全量比较。这样的话能减少以及遍历 props 的次数,从而实现性能提升。
了解完 patchFlag 的作用,我们在回头看下patchElement的逻辑。
- 当 patchFlag 为 FULL_PROPS 时,说明此时的元素的属性props中,可能包含了动态的 key ,需要进行全量的 props diff。
- 当 patchFlag 为 CLASS 时,说明只有 class 是动态的,只对比新旧节点的 class ,当他们不一致时,会对 class 进行 patch,如果 class 属性完全一致时,不需要进行任何操作。这个 Flag 标记会在元素有动态的 class 绑定时加入。
- 当 patchFlag 为 STYLE 时,会对 style 进行更新,这是每次 patch 都会进行的,这个 Flag 会在有动态 style 绑定时被加入。
- 当 patchFlag 为 PROPS 时,需要注意这个 Flag 会在元素拥有动态的属性或者 attrs 绑定时添加,不同于 class 和 style,这些动态的prop 或 attrs 的 key 会被保存下来以便于更快速的迭代。
- 当 patchFlag 为 TEXT 时,如果新旧节点中的子节点是文本发生变化,则进行更新。
小结
这节我们了解完了,patch 的流程,虽然后面还有 processComponent ,但是逻辑都是一样的,所以就不进行赘述。