]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: vapor keepalive
authordaiwei <daiwei521@126.com>
Tue, 8 Apr 2025 06:30:48 +0000 (14:30 +0800)
committerdaiwei <daiwei521@126.com>
Tue, 8 Apr 2025 06:30:48 +0000 (14:30 +0800)
packages/runtime-core/src/components/KeepAlive.ts
packages/runtime-core/src/index.ts
packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts [new file with mode: 0644]
packages/runtime-vapor/src/apiTemplateRef.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/components/KeepAlive.ts [new file with mode: 0644]
packages/runtime-vapor/src/index.ts

index 949de0bdd55f729b2586d3083de62e8f3092f0bf..42f8518bb9de720c9544f68ac5e17cce440235db 100644 (file)
@@ -1,10 +1,10 @@
 import {
-  type ComponentInternalInstance,
   type ComponentOptions,
   type ConcreteComponent,
   type GenericComponentInstance,
   type SetupContext,
   getComponentName,
+  getCurrentGenericInstance,
   getCurrentInstance,
 } from '../component'
 import {
@@ -398,7 +398,7 @@ export const KeepAlive = (__COMPAT__
   }
 }
 
-function matches(pattern: MatchPattern, name: string): boolean {
+export function matches(pattern: MatchPattern, name: string): boolean {
   if (isArray(pattern)) {
     return pattern.some((p: string | RegExp) => matches(p, name))
   } else if (isString(pattern)) {
@@ -413,14 +413,14 @@ function matches(pattern: MatchPattern, name: string): boolean {
 
 export function onActivated(
   hook: Function,
-  target?: ComponentInternalInstance | null,
+  target?: GenericComponentInstance | null,
 ): void {
   registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
 }
 
 export function onDeactivated(
   hook: Function,
-  target?: ComponentInternalInstance | null,
+  target?: GenericComponentInstance | null,
 ): void {
   registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
 }
@@ -428,7 +428,7 @@ export function onDeactivated(
 function registerKeepAliveHook(
   hook: Function & { __wdc?: Function },
   type: LifecycleHooks,
-  target: ComponentInternalInstance | null = getCurrentInstance(),
+  target: GenericComponentInstance | null = getCurrentGenericInstance(),
 ) {
   // cache the deactivate branch check wrapper for injected hooks so the same
   // hook can be properly deduped by the scheduler. "__wdc" stands for "with
@@ -454,8 +454,9 @@ function registerKeepAliveHook(
   // arrays.
   if (target) {
     let current = target.parent
-    while (current && current.parent && current.parent.vnode) {
-      if (isKeepAlive(current.parent.vnode)) {
+    while (current && current.parent) {
+      let parent = current.parent
+      if (isKeepAlive(parent.vapor ? (parent as any) : current.parent.vnode)) {
         injectToKeepAliveRoot(wrappedHook, type, target, current)
       }
       current = current.parent
@@ -466,7 +467,7 @@ function registerKeepAliveHook(
 function injectToKeepAliveRoot(
   hook: Function & { __weh?: Function },
   type: LifecycleHooks,
-  target: ComponentInternalInstance,
+  target: GenericComponentInstance,
   keepAliveRoot: GenericComponentInstance,
 ) {
   // injectHook wraps the original for error handling, so make sure to remove
index c7150e38e808c8cbf4ee1df47d070984d2bfe8bb..0b0e58af1ab4bf1d035ebc8fad6d3ead3eced884 100644 (file)
@@ -505,7 +505,7 @@ export { type VaporInteropInterface } from './apiCreateApp'
 /**
  * @internal
  */
-export { type RendererInternals, MoveType } from './renderer'
+export { type RendererInternals, MoveType, invalidateMount } from './renderer'
 /**
  * @internal
  */
@@ -557,3 +557,15 @@ export { startMeasure, endMeasure } from './profiling'
  * @internal
  */
 export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { getComponentName } from './component'
+/**
+ * @internal
+ */
+export { matches, isKeepAlive } from './components/KeepAlive'
+/**
+ * @internal
+ */
+export { devtoolsComponentAdded } from './devtools'
diff --git a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts
new file mode 100644 (file)
index 0000000..890459a
--- /dev/null
@@ -0,0 +1,122 @@
+import { type VaporComponent, createComponent } from '../../src/component'
+import { makeRender } from '../_utils'
+import { VaporKeepAlive } from '../../src/components/KeepAlive'
+import { defineVaporComponent } from '../../src/apiDefineComponent'
+import { child } from '../../src/dom/node'
+import { setText } from '../../src/dom/prop'
+import { template } from '../../src/dom/template'
+import { renderEffect } from '../../src/renderEffect'
+import { createTemplateRefSetter } from '../../src/apiTemplateRef'
+import { createDynamicComponent } from '../../src/apiCreateDynamicComponent'
+import {
+  nextTick,
+  onActivated,
+  onBeforeMount,
+  onDeactivated,
+  onMounted,
+  onUnmounted,
+  ref,
+} from 'vue'
+
+const define = makeRender()
+
+describe('VaporKeepAlive', () => {
+  let one: VaporComponent
+  let two: VaporComponent
+  let oneTest: VaporComponent
+  let views: Record<string, VaporComponent>
+  let root: HTMLDivElement
+
+  beforeEach(() => {
+    root = document.createElement('div')
+    one = defineVaporComponent({
+      name: 'one',
+      setup(_, { expose }) {
+        onBeforeMount(vi.fn())
+        onMounted(vi.fn())
+        onActivated(vi.fn())
+        onDeactivated(vi.fn())
+        onUnmounted(vi.fn())
+
+        const msg = ref('one')
+        expose({ setMsg: (m: string) => (msg.value = m) })
+
+        const n0 = template(`<div> </div>`)() as any
+        const x0 = child(n0) as any
+        renderEffect(() => setText(x0, msg.value))
+        return n0
+      },
+    })
+    oneTest = defineVaporComponent({
+      name: 'oneTest',
+      setup() {
+        onBeforeMount(vi.fn())
+        onMounted(vi.fn())
+        onActivated(vi.fn())
+        onDeactivated(vi.fn())
+        onUnmounted(vi.fn())
+
+        const msg = ref('oneTest')
+        const n0 = template(`<div> </div>`)() as any
+        const x0 = child(n0) as any
+        renderEffect(() => setText(x0, msg.value))
+        return n0
+      },
+    })
+    two = defineVaporComponent({
+      name: 'two',
+      setup() {
+        onBeforeMount(vi.fn())
+        onMounted(vi.fn())
+        onActivated(vi.fn())
+        onDeactivated(vi.fn())
+        onUnmounted(vi.fn())
+
+        const msg = ref('two')
+        const n0 = template(`<div> </div>`)() as any
+        const x0 = child(n0) as any
+        renderEffect(() => setText(x0, msg.value))
+        return n0
+      },
+    })
+    views = {
+      one,
+      oneTest,
+      two,
+    }
+  })
+
+  test('should preserve state', async () => {
+    const viewRef = ref('one')
+    const instanceRef = ref<any>(null)
+
+    const { mount } = define({
+      setup() {
+        const setTemplateRef = createTemplateRefSetter()
+        const n4 = createComponent(VaporKeepAlive as any, null, {
+          default: () => {
+            const n0 = createDynamicComponent(() => views[viewRef.value]) as any
+            setTemplateRef(n0, instanceRef)
+            return n0
+          },
+        })
+        return n4
+      },
+    }).create()
+
+    mount(root)
+    expect(root.innerHTML).toBe(`<div>one</div><!--dynamic-component-->`)
+
+    instanceRef.value.setMsg('changed')
+    await nextTick()
+    expect(root.innerHTML).toBe(`<div>changed</div><!--dynamic-component-->`)
+
+    viewRef.value = 'two'
+    await nextTick()
+    expect(root.innerHTML).toBe(`<div>two</div><!--dynamic-component-->`)
+
+    viewRef.value = 'one'
+    await nextTick()
+    expect(root.innerHTML).toBe(`<div>changed</div><!--dynamic-component-->`)
+  })
+})
index c5a6c5fb2b67b8b50e855686f3f6dd61c8a60338..06a2c63af130485529b4b602cd699cffaaa4d780 100644 (file)
@@ -20,6 +20,7 @@ import {
   isString,
   remove,
 } from '@vue/shared'
+import { DynamicFragment } from './block'
 
 export type NodeRef = string | Ref | ((ref: Element) => void)
 export type RefEl = Element | VaporComponentInstance
@@ -49,8 +50,11 @@ export function setRef(
   if (!instance || instance.isUnmounted) return
 
   const setupState: any = __DEV__ ? instance.setupState || {} : null
-  const refValue = isVaporComponent(el) ? getExposed(el) || el : el
-
+  const refValue = isVaporComponent(el)
+    ? getExposed(el) || el
+    : el instanceof DynamicFragment
+      ? getExposed(el.nodes as VaporComponentInstance) || el.nodes
+      : el
   const refs =
     instance.refs === EMPTY_OBJ ? (instance.refs = {}) : instance.refs
 
index 548babebf8beef2115e31356d50a989e2e1a0112..118ffe5406399753a4ed1c7a45d58a88e83331be 100644 (file)
@@ -15,6 +15,7 @@ import {
   currentInstance,
   endMeasure,
   expose,
+  isKeepAlive,
   nextUid,
   popWarningContext,
   pushWarningContext,
@@ -60,6 +61,7 @@ import {
 import { hmrReload, hmrRerender } from './hmr'
 import { isHydrating, locateHydrationNode } from './dom/hydration'
 import { insertionAnchor, insertionParent } from './insertionState'
+import type { KeepAliveInstance } from './components/KeepAlive'
 
 export { currentInstance } from '@vue/runtime-dom'
 
@@ -270,7 +272,7 @@ export function createComponent(
   onScopeDispose(() => unmountComponent(instance), true)
 
   if (!isHydrating && _insertionParent) {
-    insert(instance.block, _insertionParent, _insertionAnchor)
+    mountComponent(instance, _insertionParent, _insertionAnchor)
   }
 
   return instance
@@ -493,14 +495,24 @@ export function createComponentWithFallback(
 
 export function mountComponent(
   instance: VaporComponentInstance,
-  parent: ParentNode,
+  parentNode: ParentNode,
   anchor?: Node | null | 0,
 ): void {
+  let parent
+  if (
+    (parent = instance.parent) &&
+    isKeepAlive(parent as any) &&
+    (parent as KeepAliveInstance).isKeptAlive(instance)
+  ) {
+    ;(parent as KeepAliveInstance).activate(instance, parentNode, anchor as any)
+    return
+  }
+
   if (__DEV__) {
     startMeasure(instance, `mount`)
   }
   if (instance.bm) invokeArrayFns(instance.bm)
-  insert(instance.block, parent, anchor)
+  insert(instance.block, parentNode, anchor)
   if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
   instance.isMounted = true
   if (__DEV__) {
@@ -512,6 +524,16 @@ export function unmountComponent(
   instance: VaporComponentInstance,
   parentNode?: ParentNode,
 ): void {
+  let parent
+  if (
+    (parent = instance.parent) &&
+    isKeepAlive(parent as any) &&
+    (parent as KeepAliveInstance).shouldKeepAlive(instance)
+  ) {
+    ;(parent as KeepAliveInstance).deactivate(instance)
+    return
+  }
+
   if (instance.isMounted && !instance.isUnmounted) {
     if (__DEV__ && instance.type.__hmrId) {
       unregisterHMR(instance)
diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts
new file mode 100644 (file)
index 0000000..fe44630
--- /dev/null
@@ -0,0 +1,196 @@
+import {
+  type KeepAliveProps,
+  currentInstance,
+  devtoolsComponentAdded,
+  getComponentName,
+  invalidateMount,
+  matches,
+  onBeforeUnmount,
+  onMounted,
+  onUpdated,
+  queuePostFlushCb,
+  warn,
+  watch,
+} from '@vue/runtime-dom'
+import { type Block, insert, isFragment, isValidBlock, remove } from '../block'
+import {
+  type VaporComponent,
+  type VaporComponentInstance,
+  isVaporComponent,
+} from '../component'
+import { defineVaporComponent } from '../apiDefineComponent'
+import { invokeArrayFns, isArray } from '@vue/shared'
+
+export interface KeepAliveInstance extends VaporComponentInstance {
+  activate: (
+    instance: VaporComponentInstance,
+    parentNode: ParentNode,
+    anchor: Node,
+  ) => void
+  deactivate: (instance: VaporComponentInstance) => void
+  shouldKeepAlive: (instance: VaporComponentInstance) => boolean
+  isKeptAlive: (instance: VaporComponentInstance) => boolean
+}
+
+type CacheKey = PropertyKey | VaporComponent
+type Cache = Map<CacheKey, VaporComponentInstance>
+type Keys = Set<CacheKey>
+
+const VaporKeepAliveImpl = defineVaporComponent({
+  name: 'VaporKeepAlive',
+  // @ts-expect-error
+  __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 = document.createElement('div')
+
+    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
+      ;(keepAliveInstance as any).__v_cache = cache
+    }
+
+    const { include, exclude, max } = props
+
+    function cacheBlock() {
+      // TODO suspense
+      const current = keepAliveInstance.block!
+      if (!isValidBlock(current)) return
+
+      const block = getInnerBlock(current)!
+      if (!block) return
+
+      const key = block.type
+      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, block)
+    }
+
+    onMounted(cacheBlock)
+    onUpdated(cacheBlock)
+    onBeforeUnmount(() => cache.forEach(cached => remove(cached)))
+
+    const children = slots.default()
+    if (isArray(children) && children.length > 1) {
+      if (__DEV__) {
+        warn(`KeepAlive should contain exactly one component child.`)
+      }
+      return children
+    }
+
+    keepAliveInstance.activate = (
+      instance: VaporComponentInstance,
+      parentNode: ParentNode,
+      anchor: Node,
+    ) => {
+      invalidateMount(instance.m)
+      invalidateMount(instance.a)
+
+      const cachedBlock = cache.get(instance.type)!
+      insert((instance.block = cachedBlock.block), parentNode, anchor)
+      queuePostFlushCb(() => {
+        instance.isDeactivated = false
+        if (instance.a) invokeArrayFns(instance.a)
+      })
+
+      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
+        devtoolsComponentAdded(instance)
+      }
+    }
+
+    keepAliveInstance.deactivate = (instance: VaporComponentInstance) => {
+      insert(instance.block, storageContainer)
+      queuePostFlushCb(() => {
+        if (instance.da) invokeArrayFns(instance.da)
+        instance.isDeactivated = true
+      })
+
+      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
+        devtoolsComponentAdded(instance)
+      }
+    }
+
+    keepAliveInstance.shouldKeepAlive = (instance: VaporComponentInstance) => {
+      const name = getComponentName(instance.type)
+      if (
+        (include && (!name || !matches(include, name))) ||
+        (exclude && name && matches(exclude, name))
+      ) {
+        return false
+      }
+      return true
+    }
+
+    keepAliveInstance.isKeptAlive = (instance: VaporComponentInstance) => {
+      return cache.has(instance.type)
+    }
+
+    function pruneCache(filter: (name: string) => boolean) {
+      cache.forEach((instance, key) => {
+        const name = getComponentName(instance.type)
+        if (name && !filter(name)) {
+          pruneCacheEntry(key)
+        }
+      })
+    }
+
+    function pruneCacheEntry(key: CacheKey) {
+      const cached = cache.get(key)
+      if (cached) {
+        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 },
+    )
+
+    return children
+  },
+})
+
+export const VaporKeepAlive = VaporKeepAliveImpl as any as {
+  __isKeepAlive: true
+  new (): {
+    $props: KeepAliveProps
+    $slots: {
+      default(): Block
+    }
+  }
+}
+
+function getInnerBlock(block: Block): VaporComponentInstance | undefined {
+  if (isVaporComponent(block)) {
+    return block
+  }
+  if (isFragment(block)) {
+    return getInnerBlock(block.nodes)
+  }
+}
index 682532fa4d80aa01a23a948bbb538b049715a9ff..6cc06a72e870ff1ab313c28c976b09778bf7a052 100644 (file)
@@ -3,6 +3,7 @@ export { createVaporApp, createVaporSSRApp } from './apiCreateApp'
 export { defineVaporComponent } from './apiDefineComponent'
 export { vaporInteropPlugin } from './vdomInterop'
 export type { VaporDirective } from './directives/custom'
+export { VaporKeepAlive } from './components/KeepAlive'
 
 // compiler-use only
 export { insert, prepend, remove, isFragment, VaporFragment } from './block'