vue3源码学习3-DOM Diff
第2节中我们运行带副作用的渲染函数setupRenderEffect方法,上一节我们关注的是创建组件,这一节来看更新组件,文件位置:packages/runtime-core/src/renderer.ts
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
if (!instance.isMounted) {
// 渲染组件
} else {
// 更新组件
// next是新节点,vode是旧节点
let { next, bu, u, parent, vnode } = instance
if (next) {
next.el = vnode.el
// 更新组件vnode节点信息
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
// 渲染新的子树 vnode
const nextTree = renderComponentRoot(instance)
// 缓存旧的子树 vnode
const prevTree = instance.subTree
// 更新子树 vnode
instance.subTree = nextTree
// 组件更新核心逻辑,根据新旧子树vnode做patch
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
// 如果在teleport组件中父节点可能已经改变,所以直接找旧树DOM元素的父节点
hostParentNode(prevTree.el!)!,
// anchor may have changed if it's in a fragment
// 参考节点在fragment的情况可能改变,多以直接找旧树DOM的下一个元素
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
// 缓存更新后的DOM节点
next.el = nextTree.el
}
}
组件更新核心逻辑,patch流程:
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 如果存在新旧节点,且新旧节点类型不同,直接销毁旧节点,然后走mount流程
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
// n1设置为null,保证后续都走mount流程
n1 = null
}
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:
// 处理Fragment元素
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理普通DOM元素
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
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理TELEPORT
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理SUSPENSE
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
}
}
处理组件
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
if (n1 == null) {
// 挂载组件
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
// 更新组件
updateComponent(n1, n2, optimized)
}
更新组件:
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
const instance = (n2.component = n1.component)!
// 根据新旧子组件的vnode判断是否需要更新子组件
if (shouldUpdateComponent(n1, n2, optimized)) {
// 新的子组件 vnode 赋值给 instance.next
instance.next = n2
// 子组件可能因为数据变化被添加到更新队列里,移除他们防止子组件重复更新
invalidateJob(instance.update)
// 执行子组件的副作用渲染函数
instance.update()
} else {
// 不需要更新,只复制属性
n2.el = n1.el
instance.vnode = n2
}
接下来我们回到副作用的渲染函数setupRenderEffect中,看下面的这行代码
// setupRenderEffect中的代码
updateComponentPreRender(instance, next, optimized)
// 看看这个方法都做了什么
const updateComponentPreRender = (
instance: ComponentInternalInstance,
nextVNode: VNode,
optimized: boolean
) => {
// 新组件vnode的component属性指向组件实例
nextVNode.component = instance
// 旧组件vnode 的props属性
const prevProps = instance.vnode.props
// 组件实例的vnode属性指向新的组件的vnode
instance.vnode = nextVNode
// 清空next属性,为了下一次重新渲染准备
instance.next = null
// 更新props
updateProps(instance, nextVNode.props, prevProps, optimized)
// 更新插槽
updateSlots(instance, nextVNode.children, optimized)
}
处理普通元素
组件是抽象的,最终的更新还是会针对普通的DOM元素
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
if (n1 == null) {
// 挂载元素
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 更新元素
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
// 看patchElement的定义
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
const el = (n2.el = n1.el!)
const oldProps = n1.props || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
// 更新子节点
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds,
false
)
// 更新 props
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
}
patchProps主要是更新DOM节点的class、style、event以及其他一些DOM属性,可以在packages/runtime-dom/src/patchProp.ts文件中自行查看,重点来看patchChildren方法:
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized = false
) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// children has 3 possibilities: text, array or no children.
// 子节点有3种可能情况:文本、数组、空
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// text children fast path
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) {
// prev children was array
// 新的节点也是数组
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// two arrays, cannot assume anything, do full diff
// 数组到数组的更新,需要完整的diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// no new children, just unmount old
// 没有新的子节点,删除之前的子节点
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// prev children was text OR null
// new children is array OR null
// 之前的子节点是文本节点或空, 新的子节点是数组或空
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 之前的子节点是文本,则清空
hostSetElementText(container, '')
}
// mount new if array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 如果新的子节点是数组,挂载新子节点
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
}
一个元素的子节点vnode有三种情况:文本、vnode数组和空,排列组合有九种情况:
旧子节点 | 新子节点 | 操作 |
---|---|---|
纯文本 | 纯文本 | 替换新文本 |
空 | 删除旧子节点 | |
vnode数组 | 清空文本,添加多个新子节点 | |
空 | 纯文本 | 添加新文本节点 |
空 | 什么都不做 | |
vnode数组 | 添加多个新子节点 | |
vnode数组 | 纯文本 | 删除旧子节点,添加新文本节点 |
空 | 删除旧子节点 | |
vnode数组 | 完成的diff子节点 |
核心diff算法。。。