From: daiwei Date: Tue, 29 Jul 2025 06:25:18 +0000 (+0800) Subject: chore: Merge branch 'edison/feat/fowardedSlots' into edison/testVapor X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=6505a8f155b83432bb47697c16e89c57d67e557b;p=thirdparty%2Fvuejs%2Fcore.git chore: Merge branch 'edison/feat/fowardedSlots' into edison/testVapor --- 6505a8f155b83432bb47697c16e89c57d67e557b diff --cc packages/runtime-vapor/src/apiCreateFor.ts index 5bde0790c8,0f032e685e..643a73b3a2 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@@ -11,22 -11,22 +11,22 @@@ import toReadonly, watch, } from '@vue/reactivity' -import { isArray, isObject, isString } from '@vue/shared' +import { FOR_ANCHOR_LABEL, isArray, isObject, isString } from '@vue/shared' import { createComment, createTextNode } from './dom/node' - import { type Block, insert, remove as removeBlock } from './block' -import { - type Block, - ForFragment, - VaporFragment, - insert, - remove, - remove as removeBlock, -} from './block' ++import { type Block, insert, remove, remove as removeBlock } from './block' import { warn } from '@vue/runtime-dom' import { currentInstance, isVaporComponent } from './component' import type { DynamicSlot } from './componentSlots' import { renderEffect } from './renderEffect' import { VaporVForFlags } from '../../shared/src/vaporFlags' -import { isHydrating, locateHydrationNode } from './dom/hydration' +import { + currentHydrationNode, + isHydrating, + locateHydrationNode, + locateVaporFragmentAnchor, + updateNextChildToHydrate, +} from './dom/hydration' - import { VaporFragment } from './fragment' ++import { ForFragment, VaporFragment } from './fragment' import { insertionAnchor, insertionParent, @@@ -95,21 -94,9 +95,21 @@@ export const createFor = let parent: ParentNode | undefined | null // useSelector only let currentKey: any - // TODO handle this in hydration - const parentAnchor = __DEV__ ? createComment('for') : createTextNode() + let parentAnchor: Node + if (isHydrating) { + parentAnchor = locateVaporFragmentAnchor( + currentHydrationNode!, + FOR_ANCHOR_LABEL, + )! + if (__DEV__ && !parentAnchor) { + // this should not happen + throw new Error(`v-for fragment anchor node was not found.`) + } + } else { + parentAnchor = __DEV__ ? createComment('for') : createTextNode() + } + - const frag = new VaporFragment(oldBlocks) + const frag = new ForFragment(oldBlocks) const instance = currentInstance! const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE) const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT) diff --cc packages/runtime-vapor/src/componentSlots.ts index 2831dd5fc6,78d823b367..3a965c2c06 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@@ -16,8 -9,7 +16,8 @@@ import insertionParent, resetInsertionState, } from './insertionState' -import { isHydrating, locateHydrationNode } from './dom/hydration' +import { isHydrating } from './dom/hydration' - import { DynamicFragment, type VaporFragment, isFragment } from './fragment' ++import { DynamicFragment } from './fragment' export type RawSlots = Record & { $?: DynamicSlotSource[] @@@ -204,24 -160,3 +177,17 @@@ export function createSlot return fragment } + +function isForwardedSlot(block: Block): block is DynamicFragment { - return block instanceof DynamicFragment && !!block.forwarded ++ return ( ++ block instanceof DynamicFragment && !!(block as DynamicFragment).forwarded ++ ) +} + +function hasForwardedSlot(block: Block): block is DynamicFragment { + if (isArray(block)) { + return block.some(isForwardedSlot) + } else { + return isForwardedSlot(block) + } +} - - function ensureVaporSlotFallback( - block: VaporFragment, - fallback?: VaporSlot, - ): void { - if (block.insert && !block.fallback && fallback) { - block.fallback = fallback - } - } diff --cc packages/runtime-vapor/src/fragment.ts index ce74e07f74,0000000000..9f51fac382 mode 100644,000000..100644 --- a/packages/runtime-vapor/src/fragment.ts +++ b/packages/runtime-vapor/src/fragment.ts @@@ -1,150 -1,0 +1,204 @@@ +import { EffectScope, setActiveSub } from '@vue/reactivity' +import { createComment, createTextNode } from './dom/node' +import { + type Block, + type BlockFn, + type TransitionOptions, + type VaporTransitionHooks, + insert, + isValidBlock, + remove, +} from './block' +import type { TransitionHooks } from '@vue/runtime-dom' +import { + currentHydrationNode, + isComment, + isHydrating, + locateHydrationNode, + locateVaporFragmentAnchor, + setCurrentHydrationNode, +} from './dom/hydration' +import { + applyTransitionHooks, + applyTransitionLeaveHooks, +} from './components/Transition' +import type { VaporComponentInstance } from './component' + - export class VaporFragment implements TransitionOptions { ++export class VaporFragment ++ implements TransitionOptions ++{ + $key?: any + $transition?: VaporTransitionHooks | undefined - nodes: Block ++ nodes: T + anchor?: Node + insert?: ( + parent: ParentNode, + anchor: Node | null, + transitionHooks?: TransitionHooks, + ) => void + remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void + fallback?: BlockFn + + target?: ParentNode | null + targetAnchor?: Node | null + getNodes?: () => Block + setRef?: (comp: VaporComponentInstance) => void + - constructor(nodes: Block) { ++ constructor(nodes: T) { + this.nodes = nodes + } +} + +export class DynamicFragment extends VaporFragment { + anchor!: Node + scope: EffectScope | undefined + current?: BlockFn + fallback?: BlockFn + /** + * slot only + * indicates forwarded slot + */ + forwarded?: boolean + + constructor(anchorLabel?: string) { + super([]) + if (isHydrating) { + locateHydrationNode(anchorLabel === 'slot') + this.hydrate(anchorLabel!) + } else { + this.anchor = + __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode() + } + } + + update(render?: BlockFn, key: any = render): void { + if (key === this.current) { + return + } + this.current = key + + const prevSub = setActiveSub() + const parent = this.anchor.parentNode + const transition = this.$transition + const renderBranch = () => { + if (render) { + this.scope = new EffectScope() + this.nodes = this.scope.run(render) || [] + if (transition) { + this.$transition = applyTransitionHooks(this.nodes, transition) + } + if (parent) insert(this.nodes, parent, this.anchor) + } else { + this.scope = undefined + this.nodes = [] + } + } + + // teardown previous branch + if (this.scope) { + this.scope.stop() + const mode = transition && transition.mode + if (mode) { + applyTransitionLeaveHooks(this.nodes, transition, renderBranch) + parent && remove(this.nodes, parent) + if (mode === 'out-in') { + setActiveSub(prevSub) + return + } + } else { + parent && remove(this.nodes, parent) + } + } + + renderBranch() + - if (this.fallback && !isValidBlock(this.nodes)) { - parent && remove(this.nodes, parent) - this.nodes = - (this.scope || (this.scope = new EffectScope())).run(this.fallback) || - [] - parent && insert(this.nodes, parent, this.anchor) ++ if (this.fallback) { ++ // set fallback for nested fragments ++ const hasNestedFragment = isFragment(this.nodes) ++ if (hasNestedFragment) { ++ setFragmentFallback(this.nodes as VaporFragment, this.fallback) ++ } ++ ++ const invalidFragment = findInvalidFragment(this) ++ if (invalidFragment) { ++ parent && remove(this.nodes, parent) ++ const scope = this.scope || (this.scope = new EffectScope()) ++ scope.run(() => { ++ // for nested fragments, render invalid fragment's fallback ++ if (hasNestedFragment) { ++ renderFragmentFallback(invalidFragment) ++ } else { ++ this.nodes = this.fallback!() || [] ++ } ++ }) ++ parent && insert(this.nodes, parent, this.anchor) ++ } + } + + if (isHydrating) { + setCurrentHydrationNode(this.anchor.nextSibling) + } + setActiveSub(prevSub) + } + + hydrate(label: string): void { + // for `v-if="false"` the node will be an empty comment, use it as the anchor. + // otherwise, find next sibling vapor fragment anchor + if (label === 'if' && isComment(currentHydrationNode!, '')) { + this.anchor = currentHydrationNode + } else { + let anchor = locateVaporFragmentAnchor(currentHydrationNode!, label)! + if (!anchor && (label === 'slot' || label === 'if')) { + // fallback to fragment end anchor for ssr vdom slot + anchor = locateVaporFragmentAnchor(currentHydrationNode!, ']')! + } + if (anchor) { + this.anchor = anchor + } else if (__DEV__) { + // this should not happen + throw new Error(`${label} fragment anchor node was not found.`) + } + } + } +} + ++export class ForFragment extends VaporFragment { ++ constructor(nodes: Block[]) { ++ super(nodes) ++ } ++} ++ +export function isFragment(val: NonNullable): val is VaporFragment { + return val instanceof VaporFragment +} ++ ++export function setFragmentFallback( ++ fragment: VaporFragment, ++ fallback: BlockFn, ++): void { ++ // stop recursion if fragment has its own fallback ++ if (fragment.fallback) return ++ ++ fragment.fallback = fallback ++ if (isFragment(fragment.nodes)) { ++ setFragmentFallback(fragment.nodes, fallback) ++ } ++} ++ ++function renderFragmentFallback(fragment: VaporFragment): void { ++ if (fragment instanceof ForFragment) { ++ fragment.nodes[0] = [fragment.fallback!() || []] as Block[] ++ } else if (fragment instanceof DynamicFragment) { ++ fragment.update(fragment.fallback) ++ } else { ++ // vdom slots ++ } ++} ++ ++function findInvalidFragment(fragment: VaporFragment): VaporFragment | null { ++ if (isValidBlock(fragment.nodes)) return null ++ ++ return isFragment(fragment.nodes) ++ ? findInvalidFragment(fragment.nodes) || fragment ++ : fragment ++} diff --cc packages/runtime-vapor/src/vdomInterop.ts index e02b3d0cfe,eb579a23be..ff1da72f2b --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@@ -48,18 -45,8 +48,18 @@@ import import { type RawProps, rawPropsProxyHandlers } from './componentProps' import type { RawSlots, VaporSlot } from './componentSlots' import { renderEffect } from './renderEffect' -import { createTextNode } from './dom/node' +import { __next, createTextNode } from './dom/node' import { optimizePropertyLookup } from './dom/prop' +import { setTransitionHooks as setVaporTransitionHooks } from './components/Transition' +import { + currentHydrationNode, + isHydrating, + locateHydrationNode, + locateVaporFragmentAnchor, + setCurrentHydrationNode, + hydrateNode as vaporHydrateNode, +} from './dom/hydration' - import { DynamicFragment, VaporFragment, isFragment } from './fragment' ++import { VaporFragment, isFragment, setFragmentFallback } from './fragment' export const interopKey: unique symbol = Symbol(`interop`) @@@ -226,12 -171,11 +216,12 @@@ function createVDOMComponent component: ConcreteComponent, rawProps?: LooseRawProps | null, rawSlots?: LooseRawSlots | null, + scopeId?: string, ): VaporFragment { -- const frag = new VaporFragment([]) ++ const frag = new VaporFragment([] as Block[]) const vnode = createVNode( component, - rawProps && new Proxy(rawProps, rawPropsProxyHandlers), + rawProps && extend({}, new Proxy(rawProps, rawPropsProxyHandlers)), ) const wrapper = new VaporComponentInstance( { props: component.props },