]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
chore: ensure KeepAlive and TransitionGroup treeshake properly
authordaiwei <daiwei521@126.com>
Thu, 25 Dec 2025 09:16:25 +0000 (17:16 +0800)
committeredison <daiwei521@126.com>
Fri, 26 Dec 2025 05:37:59 +0000 (13:37 +0800)
packages/runtime-vapor/src/components/KeepAlive.ts
packages/runtime-vapor/src/components/TransitionGroup.ts

index c2fc6345aecaa6f03515f641414bd766dcbf3f5c..077f2889651af0a03d63bd589b4fb261b92965f0 100644 (file)
@@ -54,229 +54,234 @@ type CacheKey = VaporComponent | VNode['type']
 type Cache = Map<CacheKey, VaporComponentInstance | VaporFragment>
 type Keys = Set<CacheKey>
 
-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<any, EffectScope>()
-    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<any, EffectScope>()
+      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,
index 7055b6123334ef1e8b41e3e8069d8c678e2a2ae9..6884bba0fe87dbcb8c2512712c0a0be2bf9b213d 100644 (file)
@@ -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(`<transition-group> 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(`<transition-group> 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[] = []