From: daiwei Date: Thu, 25 Dec 2025 09:16:25 +0000 (+0800) Subject: chore: ensure KeepAlive and TransitionGroup treeshake properly X-Git-Tag: v3.6.0-beta.2~18 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6915ce888ef62db192797e9b4e2939758a474900;p=thirdparty%2Fvuejs%2Fcore.git chore: ensure KeepAlive and TransitionGroup treeshake properly --- diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts index c2fc6345ae..077f288965 100644 --- a/packages/runtime-vapor/src/components/KeepAlive.ts +++ b/packages/runtime-vapor/src/components/KeepAlive.ts @@ -54,229 +54,234 @@ type CacheKey = VaporComponent | VNode['type'] type Cache = Map type Keys = Set -export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ - name: 'VaporKeepAlive', - __isKeepAlive: true, - props: { - include: [String, RegExp, Array], - exclude: [String, RegExp, Array], - max: [String, Number], - }, - setup(props: KeepAliveProps, { slots }) { - if (!slots.default) { - return undefined - } - - const keepAliveInstance = currentInstance! as KeepAliveInstance - const cache: Cache = new Map() - const keys: Keys = new Set() - const storageContainer = createElement('div') - const keptAliveScopes = new Map() - let current: VaporComponentInstance | VaporFragment | undefined +export const VaporKeepAliveImpl: ObjectVaporComponent = + /*@__PURE__*/ defineVaporComponent({ + name: 'VaporKeepAlive', + __isKeepAlive: true, + props: { + include: [String, RegExp, Array], + exclude: [String, RegExp, Array], + max: [String, Number], + }, + setup(props: KeepAliveProps, { slots }) { + if (!slots.default) { + return undefined + } - if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { - ;(keepAliveInstance as any).__v_cache = cache - } + const keepAliveInstance = currentInstance! as KeepAliveInstance + const cache: Cache = new Map() + const keys: Keys = new Set() + const storageContainer = createElement('div') + const keptAliveScopes = new Map() + let current: VaporComponentInstance | VaporFragment | undefined - keepAliveInstance.getStorageContainer = () => storageContainer + if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + ;(keepAliveInstance as any).__v_cache = cache + } - keepAliveInstance.getCachedComponent = comp => cache.get(comp) + keepAliveInstance.getStorageContainer = () => storageContainer - keepAliveInstance.activate = (instance, parentNode, anchor) => { - current = instance - activate(instance, parentNode, anchor) - } + keepAliveInstance.getCachedComponent = comp => cache.get(comp) - keepAliveInstance.deactivate = instance => { - current = undefined - deactivate(instance, storageContainer) - } + keepAliveInstance.activate = (instance, parentNode, anchor) => { + current = instance + activate(instance, parentNode, anchor) + } - const innerCacheBlock = ( - key: CacheKey, - instance: VaporComponentInstance | VaporFragment, - ) => { - const { max } = props + keepAliveInstance.deactivate = instance => { + current = undefined + deactivate(instance, storageContainer) + } - if (cache.has(key)) { - // make this key the freshest - keys.delete(key) - keys.add(key) - } else { - keys.add(key) - // prune oldest entry - if (max && keys.size > parseInt(max as string, 10)) { - pruneCacheEntry(keys.values().next().value!) + const innerCacheBlock = ( + key: CacheKey, + instance: VaporComponentInstance | VaporFragment, + ) => { + const { max } = props + + if (cache.has(key)) { + // make this key the freshest + keys.delete(key) + keys.add(key) + } else { + keys.add(key) + // prune oldest entry + if (max && keys.size > parseInt(max as string, 10)) { + pruneCacheEntry(keys.values().next().value!) + } } - } - cache.set(key, instance) - current = instance - } + cache.set(key, instance) + current = instance + } - const cacheBlock = () => { - // TODO suspense - const block = keepAliveInstance.block! - const [innerBlock, interop] = getInnerBlock(block) - if (!innerBlock || !shouldCache(innerBlock, props, interop)) return - innerCacheBlock( - interop ? innerBlock.vnode!.type : innerBlock.type, - innerBlock, - ) - } + const cacheBlock = () => { + // TODO suspense + const block = keepAliveInstance.block! + const [innerBlock, interop] = getInnerBlock(block) + if (!innerBlock || !shouldCache(innerBlock, props, interop)) return + innerCacheBlock( + interop ? innerBlock.vnode!.type : innerBlock.type, + innerBlock, + ) + } - const processFragment = (frag: DynamicFragment) => { - const [innerBlock, interop] = getInnerBlock(frag.nodes) - if (!innerBlock || !shouldCache(innerBlock!, props, interop)) return false + const processFragment = (frag: DynamicFragment) => { + const [innerBlock, interop] = getInnerBlock(frag.nodes) + if (!innerBlock || !shouldCache(innerBlock!, props, interop)) + return false - if (interop) { - if (cache.has(innerBlock.vnode!.type)) { - innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE - } - innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE - } else { - if (cache.has(innerBlock!.type)) { - innerBlock!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE + if (interop) { + if (cache.has(innerBlock.vnode!.type)) { + innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE + } + innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + } else { + if (cache.has(innerBlock!.type)) { + innerBlock!.shapeFlag! |= ShapeFlags.COMPONENT_KEPT_ALIVE + } + innerBlock!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE } - innerBlock!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + return true } - return true - } - - const cacheFragment = (fragment: DynamicFragment) => { - const [innerBlock, interop] = getInnerBlock(fragment.nodes) - if (!innerBlock || !shouldCache(innerBlock, props, interop)) return - - let key: CacheKey - if (interop) { - innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE - key = innerBlock.vnode!.type - } else { - innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE - key = innerBlock.type - } - innerCacheBlock(key, innerBlock) - } - const pruneCache = (filter: (name: string) => boolean) => { - cache.forEach((cached, key) => { - const instance = getInstanceFromCache(cached) - if (!instance) return - const name = getComponentName(instance.type) - if (name && !filter(name)) { - pruneCacheEntry(key) + const cacheFragment = (fragment: DynamicFragment) => { + const [innerBlock, interop] = getInnerBlock(fragment.nodes) + if (!innerBlock || !shouldCache(innerBlock, props, interop)) return + + let key: CacheKey + if (interop) { + innerBlock.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + key = innerBlock.vnode!.type + } else { + innerBlock.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + key = innerBlock.type } - }) - } - - const pruneCacheEntry = (key: CacheKey) => { - const cached = cache.get(key)! - - resetCachedShapeFlag(cached) + innerCacheBlock(key, innerBlock) + } - // don't unmount if the instance is the current one - if (cached !== current) { - remove(cached) + const pruneCache = (filter: (name: string) => boolean) => { + cache.forEach((cached, key) => { + const instance = getInstanceFromCache(cached) + if (!instance) return + const name = getComponentName(instance.type) + if (name && !filter(name)) { + pruneCacheEntry(key) + } + }) } - cache.delete(key) - keys.delete(key) - } - // prune cache on include/exclude prop change - watch( - () => [props.include, props.exclude], - ([include, exclude]) => { - include && pruneCache(name => matches(include, name)) - exclude && pruneCache(name => !matches(exclude, name)) - }, - // prune post-render after `current` has been updated - { flush: 'post', deep: true }, - ) - - onMounted(cacheBlock) - onUpdated(cacheBlock) - onBeforeUnmount(() => { - cache.forEach((cached, key) => { - const instance = getInstanceFromCache(cached) - if (!instance) return + const pruneCacheEntry = (key: CacheKey) => { + const cached = cache.get(key)! resetCachedShapeFlag(cached) + + // don't unmount if the instance is the current one + if (cached !== current) { + remove(cached) + } cache.delete(key) + keys.delete(key) + } + + // prune cache on include/exclude prop change + watch( + () => [props.include, props.exclude], + ([include, exclude]) => { + include && pruneCache(name => matches(include, name)) + exclude && pruneCache(name => !matches(exclude, name)) + }, + // prune post-render after `current` has been updated + { flush: 'post', deep: true }, + ) - // current instance will be unmounted as part of keep-alive's unmount - if (current) { - const currentKey = isVaporComponent(current) - ? current.type - : current.vnode!.type - if (currentKey === key) { - // call deactivated hook - const da = instance.da - da && queuePostFlushCb(da) - return + onMounted(cacheBlock) + onUpdated(cacheBlock) + onBeforeUnmount(() => { + cache.forEach((cached, key) => { + const instance = getInstanceFromCache(cached) + if (!instance) return + + resetCachedShapeFlag(cached) + cache.delete(key) + + // current instance will be unmounted as part of keep-alive's unmount + if (current) { + const currentKey = isVaporComponent(current) + ? current.type + : current.vnode!.type + if (currentKey === key) { + // call deactivated hook + const da = instance.da + da && queuePostFlushCb(da) + return + } } - } - remove(cached, storageContainer) + remove(cached, storageContainer) + }) + keptAliveScopes.forEach(scope => scope.stop()) + keptAliveScopes.clear() }) - keptAliveScopes.forEach(scope => scope.stop()) - keptAliveScopes.clear() - }) - - let children = slots.default() - if (isArray(children)) { - children = children.filter(child => !(child instanceof Comment)) - if (children.length > 1) { - if (__DEV__) { - warn(`KeepAlive should contain exactly one component child.`) + + let children = slots.default() + if (isArray(children)) { + children = children.filter(child => !(child instanceof Comment)) + if (children.length > 1) { + if (__DEV__) { + warn(`KeepAlive should contain exactly one component child.`) + } + return children } - return children } - } - // inject hooks to DynamicFragment to cache components during updates - const injectKeepAliveHooks = (frag: DynamicFragment) => { - ;(frag.onBeforeTeardown || (frag.onBeforeTeardown = [])).push( - (oldKey, nodes, scope) => { - // if the fragment's nodes include a component that should be cached - // return true to avoid tearing down the fragment's scope - if (processFragment(frag)) { - keptAliveScopes.set(oldKey, scope) - return true + // inject hooks to DynamicFragment to cache components during updates + const injectKeepAliveHooks = (frag: DynamicFragment) => { + ;(frag.onBeforeTeardown || (frag.onBeforeTeardown = [])).push( + (oldKey, nodes, scope) => { + // if the fragment's nodes include a component that should be cached + // return true to avoid tearing down the fragment's scope + if (processFragment(frag)) { + keptAliveScopes.set(oldKey, scope) + return true + } + return false + }, + ) + ;(frag.onBeforeMount || (frag.onBeforeMount = [])).push(() => + cacheFragment(frag), + ) + frag.getScope = key => { + const scope = keptAliveScopes.get(key) + if (scope) { + keptAliveScopes.delete(key) + return scope } - return false - }, - ) - ;(frag.onBeforeMount || (frag.onBeforeMount = [])).push(() => - cacheFragment(frag), - ) - frag.getScope = key => { - const scope = keptAliveScopes.get(key) - if (scope) { - keptAliveScopes.delete(key) - return scope } } - } - // process shapeFlag - if (isVaporComponent(children)) { - children.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE - if (isAsyncWrapper(children)) { - injectKeepAliveHooks(children.block as DynamicFragment) - } - } else if (isInteropFragment(children)) { - children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE - } else if (isDynamicFragment(children)) { - processFragment(children) - injectKeepAliveHooks(children) - if (isVaporComponent(children.nodes) && isAsyncWrapper(children.nodes)) { - injectKeepAliveHooks(children.nodes.block as DynamicFragment) + // process shapeFlag + if (isVaporComponent(children)) { + children.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + if (isAsyncWrapper(children)) { + injectKeepAliveHooks(children.block as DynamicFragment) + } + } else if (isInteropFragment(children)) { + children.vnode!.shapeFlag! |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE + } else if (isDynamicFragment(children)) { + processFragment(children) + injectKeepAliveHooks(children) + if ( + isVaporComponent(children.nodes) && + isAsyncWrapper(children.nodes) + ) { + injectKeepAliveHooks(children.nodes.block as DynamicFragment) + } } - } - return children - }, -}) + return children + }, + }) const shouldCache = ( block: GenericComponentInstance | VaporFragment, diff --git a/packages/runtime-vapor/src/components/TransitionGroup.ts b/packages/runtime-vapor/src/components/TransitionGroup.ts index 7055b61233..6884bba0fe 100644 --- a/packages/runtime-vapor/src/components/TransitionGroup.ts +++ b/packages/runtime-vapor/src/components/TransitionGroup.ts @@ -45,127 +45,128 @@ const decorate = (t: typeof VaporTransitionGroup) => { return t } -export const VaporTransitionGroup: ObjectVaporComponent = decorate({ - name: 'VaporTransitionGroup', - - props: /*@__PURE__*/ extend({}, TransitionPropsValidators, { - tag: String, - moveClass: String, - }), - - setup(props: TransitionGroupProps, { slots }) { - const instance = currentInstance as VaporComponentInstance - const state = useTransitionState() - - // use proxy to keep props reference stable - let cssTransitionProps = resolveTransitionProps(props) - const propsProxy = new Proxy({} as typeof cssTransitionProps, { - get(_, key) { - return cssTransitionProps[key as keyof typeof cssTransitionProps] - }, - }) - - renderEffect(() => { - cssTransitionProps = resolveTransitionProps(props) - }) - - let prevChildren: TransitionBlock[] - let children: TransitionBlock[] - const slottedBlock = slots.default && slots.default() - - onBeforeUpdate(() => { - prevChildren = [] - children = getTransitionBlocks(slottedBlock) - if (children) { - for (let i = 0; i < children.length; i++) { - const child = children[i] - if (isValidTransitionBlock(child)) { - prevChildren.push(child) - // disabled transition during enter, so the children will be - // inserted into the correct position immediately. this prevents - // `recordPosition` from getting incorrect positions in `onUpdated` - child.$transition!.disabled = true - positionMap.set( - child, - getTransitionElement(child).getBoundingClientRect(), - ) +export const VaporTransitionGroup: ObjectVaporComponent = + /*@__PURE__*/ decorate({ + name: 'VaporTransitionGroup', + + props: /*@__PURE__*/ extend({}, TransitionPropsValidators, { + tag: String, + moveClass: String, + }), + + setup(props: TransitionGroupProps, { slots }) { + const instance = currentInstance as VaporComponentInstance + const state = useTransitionState() + + // use proxy to keep props reference stable + let cssTransitionProps = resolveTransitionProps(props) + const propsProxy = new Proxy({} as typeof cssTransitionProps, { + get(_, key) { + return cssTransitionProps[key as keyof typeof cssTransitionProps] + }, + }) + + renderEffect(() => { + cssTransitionProps = resolveTransitionProps(props) + }) + + let prevChildren: TransitionBlock[] + let children: TransitionBlock[] + const slottedBlock = slots.default && slots.default() + + onBeforeUpdate(() => { + prevChildren = [] + children = getTransitionBlocks(slottedBlock) + if (children) { + for (let i = 0; i < children.length; i++) { + const child = children[i] + if (isValidTransitionBlock(child)) { + prevChildren.push(child) + // disabled transition during enter, so the children will be + // inserted into the correct position immediately. this prevents + // `recordPosition` from getting incorrect positions in `onUpdated` + child.$transition!.disabled = true + positionMap.set( + child, + getTransitionElement(child).getBoundingClientRect(), + ) + } } } - } - }) + }) - onUpdated(() => { - if (!prevChildren.length) { - return - } - const moveClass = props.moveClass || `${props.name || 'v'}-move` - const firstChild = getFirstConnectedChild(prevChildren) - if ( - !firstChild || - !hasCSSTransform( - firstChild as ElementWithTransition, - firstChild.parentNode as Node, - moveClass, + onUpdated(() => { + if (!prevChildren.length) { + return + } + const moveClass = props.moveClass || `${props.name || 'v'}-move` + const firstChild = getFirstConnectedChild(prevChildren) + if ( + !firstChild || + !hasCSSTransform( + firstChild as ElementWithTransition, + firstChild.parentNode as Node, + moveClass, + ) + ) { + prevChildren = [] + return + } + + prevChildren.forEach(callPendingCbs) + prevChildren.forEach(child => { + child.$transition!.disabled = false + recordPosition(child) + }) + const movedChildren = prevChildren.filter(applyTranslation) + + // force reflow to put everything in position + forceReflow() + + movedChildren.forEach(c => + handleMovedChildren( + getTransitionElement(c) as ElementWithTransition, + moveClass, + ), ) - ) { prevChildren = [] - return - } - - prevChildren.forEach(callPendingCbs) - prevChildren.forEach(child => { - child.$transition!.disabled = false - recordPosition(child) }) - const movedChildren = prevChildren.filter(applyTranslation) - - // force reflow to put everything in position - forceReflow() - - movedChildren.forEach(c => - handleMovedChildren( - getTransitionElement(c) as ElementWithTransition, - moveClass, - ), - ) - prevChildren = [] - }) - - // store props and state on fragment for reusing during insert new items - setTransitionHooksOnFragment(slottedBlock, { - props: propsProxy, - state, - instance, - } as VaporTransitionHooks) - - children = getTransitionBlocks(slottedBlock) - for (let i = 0; i < children.length; i++) { - const child = children[i] - if (isValidTransitionBlock(child)) { - if (child.$key != null) { - const hooks = resolveTransitionHooks( - child, - propsProxy, - state, - instance!, - ) - setTransitionHooks(child, hooks) - } else if (__DEV__) { - warn(` children must be keyed`) + + // store props and state on fragment for reusing during insert new items + setTransitionHooksOnFragment(slottedBlock, { + props: propsProxy, + state, + instance, + } as VaporTransitionHooks) + + children = getTransitionBlocks(slottedBlock) + for (let i = 0; i < children.length; i++) { + const child = children[i] + if (isValidTransitionBlock(child)) { + if (child.$key != null) { + const hooks = resolveTransitionHooks( + child, + propsProxy, + state, + instance!, + ) + setTransitionHooks(child, hooks) + } else if (__DEV__) { + warn(` children must be keyed`) + } } } - } - const tag = props.tag - if (tag) { - const container = createElement(tag) - insert(slottedBlock, container) - return container - } else { - return slottedBlock - } - }, -}) + const tag = props.tag + if (tag) { + const container = createElement(tag) + insert(slottedBlock, container) + return container + } else { + return slottedBlock + } + }, + }) function getTransitionBlocks(block: Block) { let children: TransitionBlock[] = []