From cf642a7217430f4ed5fe41e0bb8980eccf49e1ed Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sat, 22 Jun 2024 23:42:30 +0900 Subject: [PATCH 1/2] feat(runtime-vapor): KeepAlive (wip) --- .../__tests__/components/KeepAlive.spec.ts | 87 +++++++++++++++++++ .../runtime-vapor/src/components/KeepAlive.ts | 0 2 files changed, 87 insertions(+) create mode 100644 packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts create mode 100644 packages/runtime-vapor/src/components/KeepAlive.ts diff --git a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts new file mode 100644 index 000000000..63577559d --- /dev/null +++ b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts @@ -0,0 +1,87 @@ +describe.todo('KeepAlive', () => { + test.todo('should preserve state', async () => {}) + + test.todo('should call correct lifecycle hooks', async () => {}) + + test.todo( + 'should call correct lifecycle hooks when toggle the KeepAlive first', + async () => {}, + ) + + test.todo('should call lifecycle hooks on nested components', async () => {}) + + test.todo( + 'should call lifecycle hooks on nested components when root component no hooks', + async () => {}, + ) + + test.todo('should call correct hooks for nested keep-alive', async () => {}) + + describe.todo('props', () => { + test.todo('include (string)', async () => {}) + + test.todo('include (regex)', async () => {}) + + test.todo('include (array)', async () => {}) + + test.todo('exclude (string)', async () => {}) + + test.todo('exclude (regex)', async () => {}) + + test.todo('exclude (array)', async () => {}) + + test.todo('include + exclude', async () => {}) + + test.todo('max', async () => {}) + }) + + describe.todo('cache invalidation', () => { + test('on include change', async () => { + test.todo('on exclude change', async () => {}) + + test.todo('on include change + view switch', async () => {}) + + test.todo('on exclude change + view switch', async () => {}) + + test.todo('should not prune current active instance', async () => {}) + + // vuejs/vue #6938 + test.todo( + 'should not cache anonymous component when include is specified', + async () => {}, + ) + + test.todo( + 'should cache anonymous components if include is not specified', + async () => {}, + ) + + // vuejs/vue #7105 + test.todo( + 'should not destroy active instance when pruning cache', + async () => {}, + ) + + test.todo( + 'should update re-activated component if props have changed', + async () => {}, + ) + }) + }) + + it.todo('should call correct vnode hooks', async () => {}) + + // vuejs/core #1511 + test.todo( + 'should work with cloned root due to scopeId / fallthrough attrs', + async () => {}, + ) + + test.todo('should work with async component', async () => {}) + + // vuejs/core #4976 + test.todo('handle error in async onActivated', async () => {}) + + // vuejs/core #3648 + test.todo('should avoid unmount later included components', async () => {}) +}) diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts new file mode 100644 index 000000000..e69de29bb From 4c284690f870419b63b2aca7058f5ebae26c1ca7 Mon Sep 17 00:00:00 2001 From: Ubugeeei Date: Sun, 23 Jun 2024 23:09:51 +0900 Subject: [PATCH 2/2] feat(runtime-vapor): KeepAlive (wip) --- packages/runtime-vapor/src/component.ts | 1 + .../runtime-vapor/src/components/KeepAlive.ts | 230 ++++++++++++++++++ 2 files changed, 231 insertions(+) diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 4c4b02a52..d472ed8d2 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -187,6 +187,7 @@ export interface ComponentInternalInstance { isMounted: boolean isUnmounted: boolean isUpdating: boolean + isDeactivated?: boolean // TODO: registory of provides, lifecycles, ... /** * @internal diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts index e69de29bb..5053d644d 100644 --- a/packages/runtime-vapor/src/components/KeepAlive.ts +++ b/packages/runtime-vapor/src/components/KeepAlive.ts @@ -0,0 +1,230 @@ +import { + type Component, + type ComponentInternalInstance, + type ObjectComponent, + getCurrentInstance, +} from '../component' +import type { Block } from '../apiRender' +import { invokeArrayFns, isArray, isRegExp, isString } from '@vue/shared' +import { queuePostFlushCb } from '../scheduler' +import { watch } from '../apiWatch' +import { onBeforeUnmount, onMounted, onUpdated } from '../apiLifecycle' +import { warn } from '..' + +type MatchPattern = string | RegExp | (string | RegExp)[] + +export interface KeepAliveProps { + include?: MatchPattern + exclude?: MatchPattern + max?: number | string +} + +type CacheKey = PropertyKey | Component +type Cache = Map +type Keys = Set + +// TODO: render coantext alternative +export interface KeepAliveComponentInternalInstance + extends ComponentInternalInstance { + activate: () => void + deactivate: () => void +} + +export const isKeepAlive = (instance: ComponentInternalInstance): boolean => + (instance as any).__isKeepAlive + +const KeepAliveImpl: ObjectComponent = { + name: 'KeepAlive', + + // @ts-expect-error + __isKeepAlive: true, + + props: { + include: [String, RegExp, Array], + exclude: [String, RegExp, Array], + max: [String, Number], + }, + + setup(props: KeepAliveProps, { slots }) { + const instance = getCurrentInstance() as KeepAliveComponentInternalInstance + + // TODO: ssr + + const cache: Cache = new Map() + const keys: Keys = new Set() + let current: ComponentInternalInstance | null = null + + // TODO: is it necessary? + // if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + // ;(instance as any).__v_cache = cache + // } + + // TODO: suspense + // const parentSuspense = instance.suspense + + // TODO: + // const storageContainer = template('
')() + + instance.activate = () => { + // TODO: + // move(vnode, container, anchor, MoveType.ENTER, parentSuspense) + + // TODO: suspense (queuePostRenderEffect) + queuePostFlushCb(() => { + instance.isDeactivated = false + if (instance.a) { + invokeArrayFns(instance.a) + } + }) + + // TODO: devtools + // if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + // // Update components tree + // devtoolsComponentAdded(instance) + // } + } + + instance.deactivate = () => { + // TODO: + // invalidateMount(instance.m) + // invalidateMount(instance.a) + // move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense) + + // TODO: suspense (queuePostRenderEffect) + queuePostFlushCb(() => { + if (instance.da) { + invokeArrayFns(instance.da) + } + instance.isDeactivated = true + }) + + // TODO: devtools + // if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + // // Update components tree + // devtoolsComponentAdded(instance) + // } + } + + function pruneCache(filter?: (name: string) => boolean) { + cache.forEach((cachedInstance, key) => { + const name = cachedInstance.type.name + if (name && (!filter || !filter(name))) { + pruneCacheEntry(key) + } + }) + } + + function pruneCacheEntry(key: CacheKey) { + const cached = cache.get(key) + if (!current || cached !== current) { + // TODO: + // unmount(cached) + } else if (current) { + // current active instance should no longer be kept-alive. + // we can't unmount it now but it might be later, so reset its flag now. + // TODO: + // resetShapeFlag(current) + } + 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 }, + ) + + // cache sub tree after render + let pendingCacheKey: CacheKey | null = null + const cacheSubtree = () => { + // fix #1621, the pendingCacheKey could be 0 + if (pendingCacheKey != null) { + cache.set(pendingCacheKey, instance) + } + } + onMounted(cacheSubtree) + onUpdated(cacheSubtree) + + onBeforeUnmount(() => { + // TODO: + }) + + // TODO: effects + return () => { + pendingCacheKey = null + + if (!slots.default) { + return null + } + + const children = slots.default() + const childInstance = children as ComponentInternalInstance + if (isArray(children) && children.length > 1) { + if (__DEV__) { + warn(`KeepAlive should contain exactly one component child.`) + } + current = null + return children + } else { + // TODO: + } + + const name = childInstance.type.name + const { include, exclude, max } = props + + if ( + (include && (!name || !matches(include, name))) || + (exclude && name && matches(exclude, name)) + ) { + return (current = childInstance) + } + + const key = childInstance.type // TODO: vnode key + const cachedBlock = cache.get(key) + + pendingCacheKey = key + + if (cachedBlock) { + // TODO: setTransitionHooks + + keys.delete(key) + keys.add(key) + } else { + keys.add(key) + if (max && keys.size > parseInt(max as string, 10)) { + pruneCacheEntry(keys.values().next().value) + } + } + + return (current = childInstance) + } + }, +} + +export const KeepAlive = KeepAliveImpl as any as { + __isKeepAlive: true + new (): { + $props: KeepAliveProps + $slots: { + default(): Block + } + } +} + +function matches(pattern: MatchPattern, name: string): boolean { + if (isArray(pattern)) { + return pattern.some((p: string | RegExp) => matches(p, name)) + } else if (isString(pattern)) { + return pattern.split(',').includes(name) + } else if (isRegExp(pattern)) { + return pattern.test(name) + } + /* istanbul ignore next */ + return false +}