vue3的Fragment实现原理
在 Vue 2 中,每个组件必须有一个根元素。而在 Vue 3 中,引入了 Fragment 特性,允许组件具有多个根节点。这为开发人员提供了更多灵活性,简化了组件模板的结构。
Vue3支持多个根节点,通过Fragment来实现,原理比较容易
我们用一个Fragment容器来渲染多个节点
分析
- 首先,在runtime-core模块中的vnode.ts里文件里新加一个类型Fragment
export const Fragment = Symbol.for('v-fgt') as any as {
__isFragment: true
new (): {
$props: VNodeProps
}
}
- 那么我在调runtime-core中的在我们调用render方法时,又会走到patch方法里,
- 如果老的虚拟节点不存在,那么就是创建操作,否则就走更新流程
const render: RootRenderFunction = (vnode, container, namespace) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
namespace,
)
}
container._vnode = vnode
if (!isFlushing) {
isFlushing = true
flushPreFlushCbs()
flushPostFlushCbs()
isFlushing = false
}
}
走到patch 方法里
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
namespace = undefined,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
) => {
if (n1 === n2) {
return
}
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = 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, namespace)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, namespace)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
internals,
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
internals,
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
在patch里我们增加一个Fragment类型的判断 命中Fragment类型后,我们写一个processFragment方法来进行处理
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
break
processFragment方法
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
if (
__DEV__ &&
// #5523 dev root fragment may inherit directives
(isHmrUpdating || patchFlag & PatchFlags.DEV_ROOT_FRAGMENT)
) {
// HMR updated / Dev root fragment (w/ comments), force full diff
patchFlag = 0
optimized = false
dynamicChildren = null
}
// check if this is a slot fragment with :slotted scope ids
if (fragmentSlotScopeIds) {
slotScopeIds = slotScopeIds
? slotScopeIds.concat(fragmentSlotScopeIds)
: fragmentSlotScopeIds
}
if (n1 == null) { //如果老的虚拟节点为null,那么我们就把新虚拟节点里的children挂载到容器中
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
// a fragment can only have array children
// since they are either generated by the compiler, or implicitly created
// from arrays.
mountChildren(
// #10007
// such fragment like `<></>` will be compiled into
// a fragment which doesn't have a children.
// In this case fallback to an empty array
(n2.children || []) as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else {
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
n1.dynamicChildren
) {
// a stable fragment (template root or <template v-for>) doesn't need to
// patch children order, but it may contain dynamicChildren.
patchBlockChildren(
n1.dynamicChildren,
dynamicChildren,
container,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
)
if (__DEV__) {
// necessary for HMR
traverseStaticChildren(n1, n2)
} else if (
// #2080 if the stable fragment has a key, it's a <template v-for> that may
// get moved around. Make sure all root level vnodes inherit el.
// #2134 or if it's a component root, it may also get moved around
// as the component is being moved.
n2.key != null ||
(parentComponent && n2 === parentComponent.subTree)
) {
traverseStaticChildren(n1, n2, true /* shallow */)
}
} else {
// keyed / unkeyed, or manual fragments.
// for keyed & unkeyed, since they are compiler generated from v-for,
// each child is guaranteed to be a block so the fragment will never
// have dynamicChildren.
patchChildren(
n1,
n2,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
) // ,走的是diff算法
}
}
}
如果老的虚拟节点为null,那么我们就把新虚拟节点里的children挂载到容器中 否则我们就对比新老虚拟节点的children,走的是diff算法
Fragment的原理比较简单,其实底层就是新增加了一个类型,用一个函数去处理初始挂载或diff更新。
在 Vue3 的编译过程中,如果组件的模板存在多个节点,Vue3 会将这些节点放入一个片段中,然后作为组件的根节点。
总结
Vue3 之所以能够支持多节点的 Fragment,是因为在内部采用了 Virtual DOM 和片段的机制。Vue3 会将多个节点放入一个片段中,然后作为组件的根节点进行 Virtual DOM 操作。使用片段的方式,可以方便地处理多节点的情况,并提高渲染效率
实际 DOM 更新时遵循单个根元素规则: 虽然 Vue 3 允许组件具有多个根节点,但在实际的 DOM 结构中,每个组件仍然需要遵循单个根元素的规则。因此,在将虚拟 DOM 更新到实际的 DOM 时,Vue 会确保每个组件的多个根节点都被插入到正确的位置,且符合 DOM 规范。
Fragment 的优点
通过这种方式,Vue 3 的 Fragment 实现了对多个根节点的支持,同时保持了与 DOM 规范的兼容性。这使得开发人员能够编写更简洁、灵活的组件模板,提高了组件的可维护性和可读性。