--- /dev/null
+import { ComponentOptions } from '../src/component'
+import {
+ h,
+ TestElement,
+ nodeOps,
+ render,
+ ref,
+ KeepAlive,
+ serializeInner,
+ nextTick
+} from '@vue/runtime-test'
+
+describe('keep-alive', () => {
+ let one: ComponentOptions
+ let two: ComponentOptions
+ let root: TestElement
+
+ beforeEach(() => {
+ root = nodeOps.createElement('div')
+ one = {
+ data: () => ({ msg: 'one' }),
+ render() {
+ return h('div', this.msg)
+ },
+ created: jest.fn(),
+ mounted: jest.fn(),
+ activated: jest.fn(),
+ deactivated: jest.fn(),
+ unmounted: jest.fn()
+ }
+ two = {
+ data: () => ({ msg: 'two' }),
+ render() {
+ return h('div', this.msg)
+ },
+ created: jest.fn(),
+ mounted: jest.fn(),
+ activated: jest.fn(),
+ deactivated: jest.fn(),
+ unmounted: jest.fn()
+ }
+ })
+
+ function assertHookCalls(component: any, callCounts: number[]) {
+ expect([
+ component.created.mock.calls.length,
+ component.mounted.mock.calls.length,
+ component.activated.mock.calls.length,
+ component.deactivated.mock.calls.length,
+ component.unmounted.mock.calls.length
+ ]).toEqual(callCounts)
+ }
+
+ test('should preserve state', async () => {
+ const toggle = ref(true)
+ const instanceRef = ref<any>(null)
+ const App = {
+ render() {
+ return h(KeepAlive, null, {
+ default: () => h(toggle.value ? one : two, { ref: instanceRef })
+ })
+ }
+ }
+ render(h(App), root)
+ expect(serializeInner(root)).toBe(`<div>one</div>`)
+ instanceRef.value.msg = 'changed'
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<div>changed</div>`)
+ toggle.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<div>two</div>`)
+ toggle.value = true
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<div>changed</div>`)
+ })
+
+ test('should call correct lifecycle hooks', async () => {
+ const toggle1 = ref(true)
+ const toggle2 = ref(true)
+ const App = {
+ render() {
+ return toggle1.value
+ ? h(KeepAlive, () => h(toggle2.value ? one : two))
+ : null
+ }
+ }
+ render(h(App), root)
+
+ expect(serializeInner(root)).toBe(`<div>one</div>`)
+ assertHookCalls(one, [1, 1, 1, 0, 0])
+ assertHookCalls(two, [0, 0, 0, 0, 0])
+
+ // toggle kept-alive component
+ toggle2.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<div>two</div>`)
+ assertHookCalls(one, [1, 1, 1, 1, 0])
+ assertHookCalls(two, [1, 1, 1, 0, 0])
+
+ toggle2.value = true
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<div>one</div>`)
+ assertHookCalls(one, [1, 1, 2, 1, 0])
+ assertHookCalls(two, [1, 1, 1, 1, 0])
+
+ toggle2.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<div>two</div>`)
+ assertHookCalls(one, [1, 1, 2, 2, 0])
+ assertHookCalls(two, [1, 1, 2, 1, 0])
+
+ // teardown keep-alive, should unmount all components including cached
+ toggle1.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<!---->`)
+ assertHookCalls(one, [1, 1, 2, 2, 1])
+ assertHookCalls(two, [1, 1, 2, 2, 1])
+ })
+
+ test('should call lifecycle hooks on nested components', async () => {
+ one.render = () => h(two)
+
+ const toggle = ref(true)
+ const App = {
+ render() {
+ return h(KeepAlive, () => (toggle.value ? h(one) : null))
+ }
+ }
+ render(h(App), root)
+
+ expect(serializeInner(root)).toBe(`<div>two</div>`)
+ assertHookCalls(one, [1, 1, 1, 0, 0])
+ assertHookCalls(two, [1, 1, 1, 0, 0])
+
+ toggle.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<!---->`)
+ assertHookCalls(one, [1, 1, 1, 1, 0])
+ assertHookCalls(two, [1, 1, 1, 1, 0])
+
+ toggle.value = true
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<div>two</div>`)
+ assertHookCalls(one, [1, 1, 2, 1, 0])
+ assertHookCalls(two, [1, 1, 2, 1, 0])
+
+ toggle.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<!---->`)
+ assertHookCalls(one, [1, 1, 2, 2, 0])
+ assertHookCalls(two, [1, 1, 2, 2, 0])
+ })
+
+ test('should call correct hooks for nested keep-alive', async () => {
+ const toggle2 = ref(true)
+ one.render = () => h(KeepAlive, () => (toggle2.value ? h(two) : null))
+
+ const toggle1 = ref(true)
+ const App = {
+ render() {
+ return h(KeepAlive, () => (toggle1.value ? h(one) : null))
+ }
+ }
+ render(h(App), root)
+
+ expect(serializeInner(root)).toBe(`<div>two</div>`)
+ assertHookCalls(one, [1, 1, 1, 0, 0])
+ assertHookCalls(two, [1, 1, 1, 0, 0])
+
+ toggle1.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<!---->`)
+ assertHookCalls(one, [1, 1, 1, 1, 0])
+ assertHookCalls(two, [1, 1, 1, 1, 0])
+
+ toggle1.value = true
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<div>two</div>`)
+ assertHookCalls(one, [1, 1, 2, 1, 0])
+ assertHookCalls(two, [1, 1, 2, 1, 0])
+
+ // toggle nested instance
+ toggle2.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<!---->`)
+ assertHookCalls(one, [1, 1, 2, 1, 0])
+ assertHookCalls(two, [1, 1, 2, 2, 0])
+
+ toggle2.value = true
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<div>two</div>`)
+ assertHookCalls(one, [1, 1, 2, 1, 0])
+ assertHookCalls(two, [1, 1, 3, 2, 0])
+
+ toggle1.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<!---->`)
+ assertHookCalls(one, [1, 1, 2, 2, 0])
+ assertHookCalls(two, [1, 1, 3, 3, 0])
+
+ // toggle nested instance when parent is deactivated
+ toggle2.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<!---->`)
+ assertHookCalls(one, [1, 1, 2, 2, 0])
+ assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected
+
+ toggle2.value = true
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<!---->`)
+ assertHookCalls(one, [1, 1, 2, 2, 0])
+ assertHookCalls(two, [1, 1, 3, 3, 0]) // should not be affected
+
+ toggle1.value = true
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<div>two</div>`)
+ assertHookCalls(one, [1, 1, 3, 2, 0])
+ assertHookCalls(two, [1, 1, 4, 3, 0])
+
+ toggle1.value = false
+ toggle2.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<!---->`)
+ assertHookCalls(one, [1, 1, 3, 3, 0])
+ assertHookCalls(two, [1, 1, 4, 4, 0])
+
+ toggle1.value = true
+ await nextTick()
+ expect(serializeInner(root)).toBe(`<!---->`)
+ assertHookCalls(one, [1, 1, 4, 3, 0])
+ assertHookCalls(two, [1, 1, 4, 4, 0]) // should remain inactive
+ })
+})
import { warn } from './warning'
import { capitalize } from '@vue/shared'
import { pauseTracking, resumeTracking, DebuggerEvent } from '@vue/reactivity'
-import { registerKeepAliveHook } from './keepAlive'
+
+export { onActivated, onDeactivated } from './keepAlive'
export function injectHook(
type: LifecycleHooks,
- hook: Function,
+ hook: Function & { __weh?: Function },
target: ComponentInternalInstance | null = currentInstance,
prepend: boolean = false
) {
if (target) {
const hooks = target[type] || (target[type] = [])
- const wrappedHook = (...args: unknown[]) => {
- if (target.isUnmounted) {
- return
- }
- // disable tracking inside all lifecycle hooks
- // since they can potentially be called inside effects.
- pauseTracking()
- // Set currentInstance during hook invocation.
- // This assumes the hook does not synchronously trigger other hooks, which
- // can only be false when the user does something really funky.
- setCurrentInstance(target)
- const res = callWithAsyncErrorHandling(hook, target, type, args)
- setCurrentInstance(null)
- resumeTracking()
- return res
- }
+ // cache the error handling wrapper for injected hooks so the same hook
+ // can be properly deduped by the scheduler. "__weh" stands for "with error
+ // handling".
+ const wrappedHook =
+ hook.__weh ||
+ (hook.__weh = (...args: unknown[]) => {
+ if (target.isUnmounted) {
+ return
+ }
+ // disable tracking inside all lifecycle hooks
+ // since they can potentially be called inside effects.
+ pauseTracking()
+ // Set currentInstance during hook invocation.
+ // This assumes the hook does not synchronously trigger other hooks, which
+ // can only be false when the user does something really funky.
+ setCurrentInstance(target)
+ const res = callWithAsyncErrorHandling(hook, target, type, args)
+ setCurrentInstance(null)
+ resumeTracking()
+ return res
+ })
if (prepend) {
hooks.unshift(wrappedHook)
} else {
export const onErrorCaptured = createHook<ErrorCapturedHook>(
LifecycleHooks.ERROR_CAPTURED
)
-
-export function onActivated(
- hook: Function,
- target?: ComponentInternalInstance | null
-) {
- registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
-}
-
-export function onDeactivated(
- hook: Function,
- target?: ComponentInternalInstance | null
-) {
- registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
-}
} from './component'
import { VNode, cloneVNode, isVNode } from './vnode'
import { warn } from './warning'
-import { onBeforeUnmount, injectHook } from './apiLifecycle'
+import { onBeforeUnmount, injectHook, onUnmounted } from './apiLifecycle'
import { isString, isArray } from '@vue/shared'
import { watch } from './apiWatch'
import { ShapeFlags } from './shapeFlags'
return false
}
-export function registerKeepAliveHook(
+export function onActivated(
hook: Function,
+ target?: ComponentInternalInstance | null
+) {
+ registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
+}
+
+export function onDeactivated(
+ hook: Function,
+ target?: ComponentInternalInstance | null
+) {
+ registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
+}
+
+function registerKeepAliveHook(
+ hook: Function & { __wdc?: Function },
type: LifecycleHooks,
target: ComponentInternalInstance | null = currentInstance
) {
- // When registering an activated/deactivated hook, instead of registering it
- // on the target instance, we walk up the parent chain and register it on
- // every ancestor instance that is a keep-alive root. This avoids the need
- // to walk the entire component tree when invoking these hooks, and more
- // importantly, avoids the need to track child components in arrays.
+ // cache the deactivate branch check wrapper for injected hooks so the same
+ // hook can be properly deduped by the scheduler. "__wdc" stands for "with
+ // deactivation check".
+ const wrappedHook =
+ hook.__wdc ||
+ (hook.__wdc = () => {
+ // only fire the hook if the target instance is NOT in a deactivated branch.
+ let current: ComponentInternalInstance | null = target
+ while (current) {
+ if (current.isDeactivated) {
+ return
+ }
+ current = current.parent
+ }
+ hook()
+ })
+ injectHook(type, wrappedHook, target)
+ // In addition to registering it on the target instance, we walk up the parent
+ // chain and register it on all ancestor instances that are keep-alive roots.
+ // This avoids the need to walk the entire component tree when invoking these
+ // hooks, and more importantly, avoids the need to track child components in
+ // arrays.
if (target) {
- let current = target
- while (current.parent) {
+ let current = target.parent
+ while (current && current.parent) {
if (current.parent.type === KeepAlive) {
- register(hook, type, target, current)
+ injectToKeepAliveRoot(wrappedHook, type, target, current)
}
current = current.parent
}
}
}
-function register(
+function injectToKeepAliveRoot(
hook: Function,
type: LifecycleHooks,
target: ComponentInternalInstance,
keepAliveRoot: ComponentInternalInstance
) {
- const wrappedHook = () => {
- // only fire the hook if the target instance is NOT in a deactivated branch.
- let current: ComponentInternalInstance | null = target
- while (current) {
- if (current.isDeactivated) {
- return
- }
- current = current.parent
- }
- hook()
- }
- injectHook(type, wrappedHook, keepAliveRoot, true)
- onBeforeUnmount(() => {
+ injectHook(type, hook, keepAliveRoot, true /* prepend */)
+ onUnmounted(() => {
const hooks = keepAliveRoot[type]!
- hooks.splice(hooks.indexOf(wrappedHook), 1)
+ hooks.splice(hooks.indexOf(hook), 1)
}, target)
}