Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(runtime-vapor): KeepAlive (wip) #252

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts
Original file line number Diff line number Diff line change
@@ -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 () => {})
})
1 change: 1 addition & 0 deletions packages/runtime-vapor/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export interface ComponentInternalInstance {
isMounted: boolean
isUnmounted: boolean
isUpdating: boolean
isDeactivated?: boolean
// TODO: registory of provides, lifecycles, ...
/**
* @internal
Expand Down
230 changes: 230 additions & 0 deletions packages/runtime-vapor/src/components/KeepAlive.ts
Original file line number Diff line number Diff line change
@@ -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<CacheKey, ComponentInternalInstance>
type Keys = Set<CacheKey>

// 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('<div></div>')()

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
}
Loading