From 582f16accff95e9ff0218444e314d8a7bcb50c23 Mon Sep 17 00:00:00 2001 From: baozhangjie Date: Tue, 1 Jul 2025 14:35:24 +0800 Subject: [PATCH] feat(runtime-core): add unwrapFragment to flatten nested Fragment nodes in vnode arrays --- .../__tests__/unwrapFragment.spec.ts | 48 +++++++++++++++++++ packages/runtime-core/src/index.ts | 2 + packages/runtime-core/src/unwrapFragment.ts | 17 +++++++ packages/runtime-core/src/vnode.ts | 4 ++ 4 files changed, 71 insertions(+) create mode 100644 packages/runtime-core/__tests__/unwrapFragment.spec.ts create mode 100644 packages/runtime-core/src/unwrapFragment.ts diff --git a/packages/runtime-core/__tests__/unwrapFragment.spec.ts b/packages/runtime-core/__tests__/unwrapFragment.spec.ts new file mode 100644 index 000000000..942bc679b --- /dev/null +++ b/packages/runtime-core/__tests__/unwrapFragment.spec.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest' +import { Fragment, type VNode, h, unwrapFragment } from '../src/index' + +describe('unwrapFragment', () => { + it('returns empty array if input is undefined or empty', () => { + expect(unwrapFragment(undefined)).toEqual([]) + expect(unwrapFragment([])).toEqual([]) + }) + + it('returns same array if no Fragment present', () => { + const vnode1 = h('div') + const vnode2 = h('span') + const input = [vnode1, vnode2] + const result = unwrapFragment(input) + expect(result).toEqual(input) + }) + + it('unwraps single level Fragment', () => { + const children = [h('div', 'a'), h('div', 'b')] + const fragmentVNode: VNode = h(Fragment, null, children) + const input = [fragmentVNode] + const result = unwrapFragment(input) + expect(result).toHaveLength(2) + expect(result).toEqual(children) + }) + + it('unwraps nested Fragments recursively', () => { + const innerChildren = [h('span', 'x'), h('span', 'y')] + const innerFragment = h(Fragment, null, innerChildren) + const outerChildren = [innerFragment, h('div', 'z')] + const outerFragment = h(Fragment, null, outerChildren) + const input = [outerFragment] + const result = unwrapFragment(input) + // Should flatten all fragments recursively + expect(result).toHaveLength(3) + expect(result).toEqual([...innerChildren, outerChildren[1]]) + }) + + it('unwraps mixed array with Fragment and non-Fragment vnode', () => { + const children = [h('li', 'item1'), h('li', 'item2')] + const fragmentVNode = h(Fragment, null, children) + const nonFragmentVNode = h('p', 'paragraph') + const input = [fragmentVNode, nonFragmentVNode] + const result = unwrapFragment(input) + expect(result).toHaveLength(3) + expect(result).toEqual([...children, nonFragmentVNode]) + }) +}) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 9910f8210..343001563 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -125,6 +125,8 @@ export { withDirectives } from './directives' // SSR context export { useSSRContext, ssrContextKey } from './helpers/useSsrContext' +export { unwrapFragment } from './unwrapFragment' + // Custom Renderer API --------------------------------------------------------- export { createRenderer, createHydrationRenderer } from './renderer' diff --git a/packages/runtime-core/src/unwrapFragment.ts b/packages/runtime-core/src/unwrapFragment.ts new file mode 100644 index 000000000..a7cebb521 --- /dev/null +++ b/packages/runtime-core/src/unwrapFragment.ts @@ -0,0 +1,17 @@ +import { type VNode, isFragmentVNode } from './vnode' + +/** + * 展开 vnode 数组中所有 Fragment 节点,将它们的子节点平铺出来。 + */ +export const unwrapFragment = (vnodes: VNode[] | undefined): VNode[] => { + if (!vnodes) return [] + const result: VNode[] = [] + for (const vnode of vnodes) { + if (isFragmentVNode(vnode)) { + result.push(...unwrapFragment(vnode.children as VNode[])) + } else { + result.push(vnode) + } + } + return result +} diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index a8c5340cd..57fe12faf 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -387,6 +387,10 @@ export function isVNode(value: any): value is VNode { return value ? value.__v_isVNode === true : false } +export function isFragmentVNode(vnode: VNode): vnode is VNode { + return !!vnode && vnode.type === Fragment +} + export function isSameVNodeType(n1: VNode, n2: VNode): boolean { if (__DEV__ && n2.shapeFlag & ShapeFlags.COMPONENT && n1.component) { const dirtyInstances = hmrDirtyComponents.get(n2.type as ConcreteComponent)