From 360be2a18e67ba08fc3036056511ced87ced3e58 Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Fri, 12 Mar 2021 20:51:33 +0800 Subject: [PATCH 1/8] feat(KeepAlive): support using key as matching name --- .../__tests__/components/KeepAlive.spec.ts | 43 ++++++++++++++++++- .../runtime-core/src/components/KeepAlive.ts | 13 +++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index 1cc7fe01eff..46c35991749 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -316,13 +316,24 @@ describe('KeepAlive', () => { assertHookCalls(two, [1, 1, 4, 4, 0]) // should remain inactive }) - async function assertNameMatch(props: KeepAliveProps) { + async function assertNameMatch( + props: KeepAliveProps, + useKey: boolean = false + ) { + if (useKey) { + delete one.name + delete two.name + } const outerRef = ref(true) const viewRef = ref('one') const App = { render() { return outerRef.value - ? h(KeepAlive, props, () => h(views[viewRef.value])) + ? h(KeepAlive, props, () => + h(views[viewRef.value], { + key: useKey ? viewRef.value : undefined + }) + ) : null } } @@ -387,6 +398,34 @@ describe('KeepAlive', () => { await assertNameMatch({ include: 'one,two', exclude: 'two' }) }) + test('include (string) w/ key', async () => { + await assertNameMatch({ include: 'one' }, true) + }) + + test('include (regex) w/ key', async () => { + await assertNameMatch({ include: /^one$/ }, true) + }) + + test('include (array) w/ key', async () => { + await assertNameMatch({ include: ['one'] }, true) + }) + + test('exclude (string) w/ key', async () => { + await assertNameMatch({ exclude: 'two' }, true) + }) + + test('exclude (regex) w/ key', async () => { + await assertNameMatch({ exclude: /^two$/ }, true) + }) + + test('exclude (array) w/ key', async () => { + await assertNameMatch({ exclude: ['two'] }, true) + }) + + test('include + exclude w/ key', async () => { + await assertNameMatch({ include: 'one,two', exclude: 'two' }, true) + }) + test('max', async () => { const spyAC = jest.fn() const spyBC = jest.fn() diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index cbba10fd755..88cca4a656f 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -157,7 +157,7 @@ const KeepAliveImpl = { function pruneCache(filter?: (name: string) => boolean) { cache.forEach((vnode, key) => { - const name = getComponentName(vnode.type as ConcreteComponent) + const name = getMatchingName(vnode) if (name && (!filter || !filter(name))) { pruneCacheEntry(key) } @@ -241,7 +241,7 @@ const KeepAliveImpl = { let vnode = getInnerChild(rawVNode) const comp = vnode.type as ConcreteComponent - const name = getComponentName(comp) + const name = getMatchingName(vnode) const { include, exclude, max } = props if ( @@ -399,3 +399,12 @@ function resetShapeFlag(vnode: VNode) { function getInnerChild(vnode: VNode) { return vnode.shapeFlag & ShapeFlags.SUSPENSE ? vnode.ssContent! : vnode } + +function getMatchingName(vnode: VNode) { + const comp = vnode.type as ConcreteComponent + let name = getComponentName(comp) + if (!name && vnode.key) { + name = String(vnode.key) + } + return name +} From 165f59f7bd7d100ecf8ff536256f5b3a057371c0 Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Mon, 15 Mar 2021 20:24:14 +0800 Subject: [PATCH 2/8] feat: allow custom caching strategy --- .../__tests__/components/KeepAlive.spec.ts | 201 ++++++++++++++++-- .../runtime-core/src/components/KeepAlive.ts | 143 +++++++++---- 2 files changed, 288 insertions(+), 56 deletions(-) diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index 46c35991749..03b77d6edc1 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -16,7 +16,11 @@ import { cloneVNode, provide } from '@vue/runtime-test' -import { KeepAliveProps } from '../../src/components/KeepAlive' +import { + KeepAliveProps, + KeepAliveCache, + Cache +} from '../../src/components/KeepAlive' describe('KeepAlive', () => { let one: ComponentOptions @@ -398,32 +402,35 @@ describe('KeepAlive', () => { await assertNameMatch({ include: 'one,two', exclude: 'two' }) }) - test('include (string) w/ key', async () => { - await assertNameMatch({ include: 'one' }, true) + test('include (string) w/ matchBy key', async () => { + await assertNameMatch({ include: 'one', matchBy: 'key' }, true) }) - test('include (regex) w/ key', async () => { - await assertNameMatch({ include: /^one$/ }, true) + test('include (regex) w/ matchBy key', async () => { + await assertNameMatch({ include: /^one$/, matchBy: 'key' }, true) }) - test('include (array) w/ key', async () => { - await assertNameMatch({ include: ['one'] }, true) + test('include (array) w/ matchBy key', async () => { + await assertNameMatch({ include: ['one'], matchBy: 'key' }, true) }) - test('exclude (string) w/ key', async () => { - await assertNameMatch({ exclude: 'two' }, true) + test('exclude (string) w/ matchBy key', async () => { + await assertNameMatch({ exclude: 'two', matchBy: 'key' }, true) }) - test('exclude (regex) w/ key', async () => { - await assertNameMatch({ exclude: /^two$/ }, true) + test('exclude (regex) w/ matchBy key', async () => { + await assertNameMatch({ exclude: /^two$/, matchBy: 'key' }, true) }) - test('exclude (array) w/ key', async () => { - await assertNameMatch({ exclude: ['two'] }, true) + test('exclude (array) w/ matchBy key', async () => { + await assertNameMatch({ exclude: ['two'], matchBy: 'key' }, true) }) - test('include + exclude w/ key', async () => { - await assertNameMatch({ include: 'one,two', exclude: 'two' }, true) + test('include + exclude w/ matchBy key', async () => { + await assertNameMatch( + { include: 'one,two', exclude: 'two', matchBy: 'key' }, + true + ) }) test('max', async () => { @@ -862,4 +869,168 @@ describe('KeepAlive', () => { await nextTick() expect(serializeInner(root)).toBe(`
changed
`) }) + + test('reuse vnode', async () => { + const Comp = { + render() { + return 'one' + }, + activated: jest.fn() + } + const reused = h(Comp) + + const App = { + render() { + return [ + reused, // `reused.el` will exist, it will be cloned in subsequent rendering + h(KeepAlive, null, { + // reuse here + default: () => h(reused) + }) + ] + } + } + + render(h(App), root) + await nextTick() + expect(serializeInner(root)).toBe(`oneone`) + expect(Comp.activated).toHaveBeenCalledTimes(1) + }) + + test('custom caching strategy', async () => { + const _cache = new Map() + const cache: KeepAliveCache = { + get(key) { + _cache.get(key) + }, + set(key, value) { + _cache.set(key, value) + }, + delete(key) { + _cache.delete(key) + }, + forEach(fn) { + _cache.forEach(fn) + } + } + + const viewRef = ref('one') + const toggle = ref(true) + const instanceRef = ref(null) + const App = { + render: () => { + return toggle.value + ? h( + KeepAlive, + { cache }, + { + default: () => h(views[viewRef.value], { ref: instanceRef }) + } + ) + : null + } + } + + render(h(App), root) + expect(cache.pruneCacheEntry).toBeDefined() + await nextTick() + expect(serializeInner(root)).toBe(`
one
`) + expect(_cache.size).toBe(1) + expect([..._cache.keys()]).toEqual([one]) + instanceRef.value.msg = 'changed' + await nextTick() + expect(serializeInner(root)).toBe(`
changed
`) + viewRef.value = 'two' + await nextTick() + expect(serializeInner(root)).toBe(`
two
`) + expect(_cache.size).toBe(2) + expect([..._cache.keys()]).toEqual([one, two]) + toggle.value = false + await nextTick() + expect(_cache.size).toBe(0) + }) + + test('warn custom cache with the `max` prop', () => { + const _cache = new Map() + const cache: KeepAliveCache = { + get(key) { + _cache.get(key) + }, + set(key, value) { + _cache.set(key, value) + }, + delete(key) { + _cache.delete(key) + }, + forEach(fn) { + _cache.forEach(fn) + } + } + render( + h({ + render: () => { + return h( + KeepAlive, + { cache, max: 10 }, + { + default: () => h(one) + } + ) + } + }), + root + ) + + expect( + 'The `max` prop will be ignored if you provide a custom caching strategy' + ).toHaveBeenWarned() + }) + + test('dynamic key changes', async () => { + const cache = new Cache() + const dynamicKey = ref(0) + const Comp = { + mounted: jest.fn(), + activated: jest.fn(), + deactivated: jest.fn(), + unmounted: jest.fn(), + render() { + return 'one' + } + } + function assertCount(calls: number[]) { + expect([ + Comp.mounted.mock.calls.length, + Comp.activated.mock.calls.length, + Comp.deactivated.mock.calls.length, + Comp.unmounted.mock.calls.length + ]).toEqual(calls) + } + + const App = { + render: () => { + return h( + KeepAlive, + { cache, matchBy: 'key' }, + { + default: () => h(Comp, { key: dynamicKey.value }) + } + ) + } + } + + render(h(App), root) + await nextTick() + expect(serializeInner(root)).toBe(`one`) + assertCount([1, 1, 0, 0]) + expect((cache as any)._cache.size).toBe(1) + expect([...(cache as any)._cache.keys()]).toEqual([0]) + + dynamicKey.value = 1 + await nextTick() + expect(serializeInner(root)).toBe(`one`) + assertCount([2, 2, 0, 1]) + expect((cache as any)._cache.size).toBe(1) + expect([...(cache as any)._cache.keys()]).toEqual([1]) + }) }) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 88cca4a656f..dbc60423647 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -21,7 +21,8 @@ import { isArray, ShapeFlags, remove, - invokeArrayFns + invokeArrayFns, + hasOwn } from '@vue/shared' import { watch } from '../apiWatch' import { @@ -41,10 +42,11 @@ export interface KeepAliveProps { include?: MatchPattern exclude?: MatchPattern max?: number | string + matchBy?: 'name' | 'key' + cache?: KeepAliveCache } type CacheKey = string | number | ConcreteComponent -type Cache = Map type Keys = Set export interface KeepAliveContext extends ComponentRenderContext { @@ -62,6 +64,60 @@ export interface KeepAliveContext extends ComponentRenderContext { export const isKeepAlive = (vnode: VNode): boolean => (vnode.type as any).__isKeepAlive +export interface KeepAliveCache { + get(key: CacheKey): VNode | void + set(key: CacheKey, value: VNode): void + delete(key: CacheKey): void + forEach( + fn: (value: VNode, key: CacheKey, map: Map) => void, + thisArg?: any + ): void + pruneCacheEntry?: (cached: VNode) => void +} + +export class Cache implements KeepAliveCache { + private readonly _cache = new Map() + private readonly _keys: Keys = new Set() + private readonly _max?: number + public pruneCacheEntry!: (cached: VNode) => void + + constructor(readonly max?: string | number) { + this._max = parseInt(max as string, 10) + } + + get(key: CacheKey) { + const { _cache, _keys, _max } = this + const cached = _cache.get(key) + if (cached) { + // make this key the freshest + _keys.delete(key) + _keys.add(key) + } else { + _keys.add(key) + // prune oldest entry + if (_max && _keys.size > _max) { + this.delete(_keys.values().next().value) + } + } + return cached + } + set(key: CacheKey, value: VNode) { + this._cache.set(key, value) + } + delete(key: CacheKey) { + const { _cache, _keys } = this + this.pruneCacheEntry(_cache.get(key)!) + _cache.delete(key) + _keys.delete(key) + } + forEach( + fn: (value: VNode, key: CacheKey, map: Map) => void, + thisArg?: any + ) { + this._cache.forEach(fn.bind(thisArg)) + } +} + const KeepAliveImpl = { name: `KeepAlive`, @@ -73,7 +129,12 @@ const KeepAliveImpl = { props: { include: [String, RegExp, Array], exclude: [String, RegExp, Array], - max: [String, Number] + max: [String, Number], + matchBy: { + type: String, + default: 'name' + }, + cache: Object }, setup(props: KeepAliveProps, { slots }: SetupContext) { @@ -91,9 +152,26 @@ const KeepAliveImpl = { return slots.default } - const cache: Cache = new Map() - const keys: Keys = new Set() + if (__DEV__ && props.cache && hasOwn(props, 'max')) { + warn( + 'The `max` prop will be ignored if you provide a custom caching strategy' + ) + } + + const cache = props.cache || new Cache(props.max) + cache.pruneCacheEntry = pruneCacheEntry + let current: VNode | null = null + function pruneCacheEntry(cached: VNode) { + if (!current || cached.type !== current.type) { + 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. + resetShapeFlag(current) + } + } + const parentSuspense = instance.suspense @@ -157,29 +235,16 @@ const KeepAliveImpl = { function pruneCache(filter?: (name: string) => boolean) { cache.forEach((vnode, key) => { - const name = getMatchingName(vnode) + const name = getMatchingName(vnode, props.matchBy!) if (name && (!filter || !filter(name))) { - pruneCacheEntry(key) + cache.delete(key) } }) } - function pruneCacheEntry(key: CacheKey) { - const cached = cache.get(key) as VNode - if (!current || cached.type !== current.type) { - 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. - resetShapeFlag(current) - } - cache.delete(key) - keys.delete(key) - } - // prune cache on include/exclude prop change watch( - () => [props.include, props.exclude], + () => [props.include, props.exclude, props.matchBy], ([include, exclude]) => { include && pruneCache(name => matches(include, name)) exclude && pruneCache(name => !matches(exclude, name)) @@ -200,7 +265,8 @@ const KeepAliveImpl = { onUpdated(cacheSubtree) onBeforeUnmount(() => { - cache.forEach(cached => { + cache.forEach((cached, key) => { + cache.delete(key) const { subTree, suspense } = instance const vnode = getInnerChild(subTree) if (cached.type === vnode.type) { @@ -211,7 +277,6 @@ const KeepAliveImpl = { da && queuePostRenderEffect(da, suspense) return } - unmount(cached) }) }) @@ -241,8 +306,15 @@ const KeepAliveImpl = { let vnode = getInnerChild(rawVNode) const comp = vnode.type as ConcreteComponent - const name = getMatchingName(vnode) - const { include, exclude, max } = props + const name = getMatchingName(vnode, props.matchBy!) + const { include, exclude } = props + + // in the case of the key changes, delete stale instances + cache.forEach((cached, key) => { + if (cached.type === vnode.type && cached.key !== vnode.key) { + cache.delete(key) + } + }) if ( (include && (!name || !matches(include, name))) || @@ -279,21 +351,12 @@ const KeepAliveImpl = { } // avoid vnode being mounted as fresh vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE - // 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) - } } // avoid vnode being unmounted vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE current = vnode - return rawVNode + return rawVNode.shapeFlag & ShapeFlags.SUSPENSE ? rawVNode : vnode } } } @@ -400,11 +463,9 @@ function getInnerChild(vnode: VNode) { return vnode.shapeFlag & ShapeFlags.SUSPENSE ? vnode.ssContent! : vnode } -function getMatchingName(vnode: VNode) { - const comp = vnode.type as ConcreteComponent - let name = getComponentName(comp) - if (!name && vnode.key) { - name = String(vnode.key) +function getMatchingName(vnode: VNode, matchBy: 'name' | 'key') { + if (matchBy === 'name') { + return getComponentName(vnode.type as ConcreteComponent) } - return name + return String(vnode.key) } From e427b48ef5cd29866057a94d5f973cfed9393d1d Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Mon, 15 Mar 2021 22:16:53 +0800 Subject: [PATCH 3/8] test: assert hook calls --- .../__tests__/components/KeepAlive.spec.ts | 9 +++++++++ packages/runtime-core/src/components/KeepAlive.ts | 13 ++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index 03b77d6edc1..fbdf3df820c 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -937,17 +937,26 @@ describe('KeepAlive', () => { expect(serializeInner(root)).toBe(`
one
`) expect(_cache.size).toBe(1) expect([..._cache.keys()]).toEqual([one]) + assertHookCalls(one, [1, 1, 1, 0, 0]) + assertHookCalls(two, [0, 0, 0, 0, 0]) + instanceRef.value.msg = 'changed' await nextTick() expect(serializeInner(root)).toBe(`
changed
`) + viewRef.value = 'two' await nextTick() expect(serializeInner(root)).toBe(`
two
`) expect(_cache.size).toBe(2) expect([..._cache.keys()]).toEqual([one, two]) + assertHookCalls(one, [1, 1, 1, 1, 0]) + assertHookCalls(two, [1, 1, 1, 0, 0]) + toggle.value = false await nextTick() expect(_cache.size).toBe(0) + assertHookCalls(one, [1, 1, 1, 1, 1]) + assertHookCalls(two, [1, 1, 1, 1, 1]) }) test('warn custom cache with the `max` prop', () => { diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index dbc60423647..ac7fa2a1eb0 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -96,7 +96,9 @@ export class Cache implements KeepAliveCache { _keys.add(key) // prune oldest entry if (_max && _keys.size > _max) { - this.delete(_keys.values().next().value) + const staleKey = _keys.values().next().value + this.pruneCacheEntry(_cache.get(staleKey)!) + this.delete(staleKey) } } return cached @@ -105,10 +107,8 @@ export class Cache implements KeepAliveCache { this._cache.set(key, value) } delete(key: CacheKey) { - const { _cache, _keys } = this - this.pruneCacheEntry(_cache.get(key)!) - _cache.delete(key) - _keys.delete(key) + this._cache.delete(key) + this._keys.delete(key) } forEach( fn: (value: VNode, key: CacheKey, map: Map) => void, @@ -238,6 +238,7 @@ const KeepAliveImpl = { const name = getMatchingName(vnode, props.matchBy!) if (name && (!filter || !filter(name))) { cache.delete(key) + pruneCacheEntry(vnode) } }) } @@ -267,6 +268,7 @@ const KeepAliveImpl = { onBeforeUnmount(() => { cache.forEach((cached, key) => { cache.delete(key) + pruneCacheEntry(cached) const { subTree, suspense } = instance const vnode = getInnerChild(subTree) if (cached.type === vnode.type) { @@ -313,6 +315,7 @@ const KeepAliveImpl = { cache.forEach((cached, key) => { if (cached.type === vnode.type && cached.key !== vnode.key) { cache.delete(key) + pruneCacheEntry(cached) } }) From 2326cd96bb218e3985afd2fbf74d12a6e9c3c90c Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Mon, 15 Mar 2021 23:35:05 +0800 Subject: [PATCH 4/8] test: delete the cache manually --- .../__tests__/components/KeepAlive.spec.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index fbdf3df820c..a19cf98f081 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -952,10 +952,26 @@ describe('KeepAlive', () => { assertHookCalls(one, [1, 1, 1, 1, 0]) assertHookCalls(two, [1, 1, 1, 0, 0]) + // delete the cache manually + const itr = _cache.keys() + const key1 = itr.next().value + cache.pruneCacheEntry!(_cache.get(key1)) + _cache.delete(key1) + await nextTick() + assertHookCalls(one, [1, 1, 1, 1, 1]) + + viewRef.value = 'one' + await nextTick() + expect(serializeInner(root)).toBe(`
one
`) + expect(_cache.size).toBe(2) + expect([..._cache.keys()]).toEqual([two, one]) + assertHookCalls(one, [2, 2, 2, 1, 1]) + assertHookCalls(two, [1, 1, 1, 1, 0]) + toggle.value = false await nextTick() expect(_cache.size).toBe(0) - assertHookCalls(one, [1, 1, 1, 1, 1]) + assertHookCalls(one, [2, 2, 2, 2, 2]) assertHookCalls(two, [1, 1, 1, 1, 1]) }) From da79f78ff4cc842724f71c6577479ac2c4cc0924 Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Tue, 16 Mar 2021 21:47:03 +0800 Subject: [PATCH 5/8] fix: adjust the caching strategy that uses key to match --- .../__tests__/components/KeepAlive.spec.ts | 29 ++++++++++++------- .../runtime-core/src/components/KeepAlive.ts | 19 ++++++------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index a19cf98f081..9b3323016e0 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -1014,6 +1014,7 @@ describe('KeepAlive', () => { test('dynamic key changes', async () => { const cache = new Cache() const dynamicKey = ref(0) + const toggle = ref(true) const Comp = { mounted: jest.fn(), activated: jest.fn(), @@ -1034,13 +1035,15 @@ describe('KeepAlive', () => { const App = { render: () => { - return h( - KeepAlive, - { cache, matchBy: 'key' }, - { - default: () => h(Comp, { key: dynamicKey.value }) - } - ) + return toggle.value + ? h( + KeepAlive, + { cache, matchBy: 'key' }, + { + default: () => h(Comp, { key: dynamicKey.value }) + } + ) + : null } } @@ -1054,8 +1057,14 @@ describe('KeepAlive', () => { dynamicKey.value = 1 await nextTick() expect(serializeInner(root)).toBe(`one`) - assertCount([2, 2, 0, 1]) - expect((cache as any)._cache.size).toBe(1) - expect([...(cache as any)._cache.keys()]).toEqual([1]) + assertCount([2, 2, 1, 0]) + expect((cache as any)._cache.size).toBe(2) + expect([...(cache as any)._cache.keys()]).toEqual([0, 1]) + + toggle.value = false + await nextTick() + expect(serializeInner(root)).toBe(``) + assertCount([2, 2, 2, 2]) + expect((cache as any)._cache.size).toBe(0) }) }) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index ac7fa2a1eb0..5573f63504d 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -163,7 +163,11 @@ const KeepAliveImpl = { let current: VNode | null = null function pruneCacheEntry(cached: VNode) { - if (!current || cached.type !== current.type) { + if ( + !current || + cached.type !== current.type || + (props.matchBy === 'key' && cached.key !== current.key) + ) { unmount(cached) } else if (current) { // current active instance should no longer be kept-alive. @@ -271,7 +275,10 @@ const KeepAliveImpl = { pruneCacheEntry(cached) const { subTree, suspense } = instance const vnode = getInnerChild(subTree) - if (cached.type === vnode.type) { + if ( + cached.type === vnode.type && + (props.matchBy !== 'key' || cached.key === vnode.key) + ) { // current instance will be unmounted as part of keep-alive's unmount resetShapeFlag(vnode) // but invoke its deactivated hook here @@ -311,14 +318,6 @@ const KeepAliveImpl = { const name = getMatchingName(vnode, props.matchBy!) const { include, exclude } = props - // in the case of the key changes, delete stale instances - cache.forEach((cached, key) => { - if (cached.type === vnode.type && cached.key !== vnode.key) { - cache.delete(key) - pruneCacheEntry(cached) - } - }) - if ( (include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name)) From 0cc469b0c1f9e068b15e7b38176eba3d0cc4577b Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Wed, 17 Mar 2021 14:49:53 +0800 Subject: [PATCH 6/8] feat: implement beforeActivate and beforeDeactivate hooks --- .../__tests__/components/KeepAlive.spec.ts | 230 ++++++++++-------- packages/runtime-core/src/apiLifecycle.ts | 7 +- packages/runtime-core/src/component.ts | 12 + packages/runtime-core/src/componentOptions.ts | 12 + .../runtime-core/src/components/KeepAlive.ts | 48 +++- packages/runtime-core/src/index.ts | 2 + packages/runtime-core/src/renderer.ts | 21 +- 7 files changed, 221 insertions(+), 111 deletions(-) diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index 9b3323016e0..3c5835dfd41 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -38,7 +38,9 @@ describe('KeepAlive', () => { }, created: jest.fn(), mounted: jest.fn(), + beforeActivate: jest.fn(), activated: jest.fn(), + beforeDeactivate: jest.fn(), deactivated: jest.fn(), unmounted: jest.fn() } @@ -50,7 +52,9 @@ describe('KeepAlive', () => { }, created: jest.fn(), mounted: jest.fn(), + beforeActivate: jest.fn(), activated: jest.fn(), + beforeDeactivate: jest.fn(), deactivated: jest.fn(), unmounted: jest.fn() } @@ -64,7 +68,9 @@ describe('KeepAlive', () => { expect([ component.created.mock.calls.length, component.mounted.mock.calls.length, + component.beforeActivate.mock.calls.length, component.activated.mock.calls.length, + component.beforeDeactivate.mock.calls.length, component.deactivated.mock.calls.length, component.unmounted.mock.calls.length ]).toEqual(callCounts) @@ -104,34 +110,34 @@ describe('KeepAlive', () => { render(h(App), root) expect(serializeInner(root)).toBe(`
one
`) - assertHookCalls(one, [1, 1, 1, 0, 0]) - assertHookCalls(two, [0, 0, 0, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 0, 0, 0]) + assertHookCalls(two, [0, 0, 0, 0, 0, 0, 0]) // toggle kept-alive component viewRef.value = 'two' await nextTick() expect(serializeInner(root)).toBe(`
two
`) - assertHookCalls(one, [1, 1, 1, 1, 0]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 0]) + assertHookCalls(two, [1, 1, 1, 1, 0, 0, 0]) viewRef.value = 'one' await nextTick() expect(serializeInner(root)).toBe(`
one
`) - assertHookCalls(one, [1, 1, 2, 1, 0]) - assertHookCalls(two, [1, 1, 1, 1, 0]) + assertHookCalls(one, [1, 1, 2, 2, 1, 1, 0]) + assertHookCalls(two, [1, 1, 1, 1, 1, 1, 0]) viewRef.value = 'two' await nextTick() expect(serializeInner(root)).toBe(`
two
`) - assertHookCalls(one, [1, 1, 2, 2, 0]) - assertHookCalls(two, [1, 1, 2, 1, 0]) + assertHookCalls(one, [1, 1, 2, 2, 2, 2, 0]) + assertHookCalls(two, [1, 1, 2, 2, 1, 1, 0]) // teardown keep-alive, should unmount all components including cached toggle.value = false await nextTick() expect(serializeInner(root)).toBe(``) - assertHookCalls(one, [1, 1, 2, 2, 1]) - assertHookCalls(two, [1, 1, 2, 2, 1]) + assertHookCalls(one, [1, 1, 2, 2, 2, 2, 1]) + assertHookCalls(two, [1, 1, 2, 2, 2, 2, 1]) }) test('should call correct lifecycle hooks when toggle the KeepAlive first', async () => { @@ -145,35 +151,35 @@ describe('KeepAlive', () => { render(h(App), root) expect(serializeInner(root)).toBe(`
one
`) - assertHookCalls(one, [1, 1, 1, 0, 0]) - assertHookCalls(two, [0, 0, 0, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 0, 0, 0]) + assertHookCalls(two, [0, 0, 0, 0, 0, 0, 0]) // should unmount 'one' component when toggle the KeepAlive first toggle.value = false await nextTick() expect(serializeInner(root)).toBe(``) - assertHookCalls(one, [1, 1, 1, 1, 1]) - assertHookCalls(two, [0, 0, 0, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 1]) + assertHookCalls(two, [0, 0, 0, 0, 0, 0, 0]) toggle.value = true await nextTick() expect(serializeInner(root)).toBe(`
one
`) - assertHookCalls(one, [2, 2, 2, 1, 1]) - assertHookCalls(two, [0, 0, 0, 0, 0]) + assertHookCalls(one, [2, 2, 2, 2, 1, 1, 1]) + assertHookCalls(two, [0, 0, 0, 0, 0, 0, 0]) // 1. the first time toggle kept-alive component viewRef.value = 'two' await nextTick() expect(serializeInner(root)).toBe(`
two
`) - assertHookCalls(one, [2, 2, 2, 2, 1]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [2, 2, 2, 2, 2, 2, 1]) + assertHookCalls(two, [1, 1, 1, 1, 0, 0, 0]) // 2. should unmount all components including cached toggle.value = false await nextTick() expect(serializeInner(root)).toBe(``) - assertHookCalls(one, [2, 2, 2, 2, 2]) - assertHookCalls(two, [1, 1, 1, 1, 1]) + assertHookCalls(one, [2, 2, 2, 2, 2, 2, 2]) + assertHookCalls(two, [1, 1, 1, 1, 1, 1, 1]) }) test('should call lifecycle hooks on nested components', async () => { @@ -188,26 +194,26 @@ describe('KeepAlive', () => { render(h(App), root) expect(serializeInner(root)).toBe(`
two
`) - assertHookCalls(one, [1, 1, 1, 0, 0]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 0, 0, 0]) + assertHookCalls(two, [1, 1, 1, 1, 0, 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]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 0]) + assertHookCalls(two, [1, 1, 1, 1, 1, 1, 0]) toggle.value = true await nextTick() expect(serializeInner(root)).toBe(`
two
`) - assertHookCalls(one, [1, 1, 2, 1, 0]) - assertHookCalls(two, [1, 1, 2, 1, 0]) + assertHookCalls(one, [1, 1, 2, 2, 1, 1, 0]) + assertHookCalls(two, [1, 1, 2, 2, 1, 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]) + assertHookCalls(one, [1, 1, 2, 2, 2, 2, 0]) + assertHookCalls(two, [1, 1, 2, 2, 2, 2, 0]) }) // #1742 @@ -253,71 +259,71 @@ describe('KeepAlive', () => { render(h(App), root) expect(serializeInner(root)).toBe(`
two
`) - assertHookCalls(one, [1, 1, 1, 0, 0]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 0, 0, 0]) + assertHookCalls(two, [1, 1, 1, 1, 0, 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]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 0]) + assertHookCalls(two, [1, 1, 1, 1, 1, 1, 0]) toggle1.value = true await nextTick() expect(serializeInner(root)).toBe(`
two
`) - assertHookCalls(one, [1, 1, 2, 1, 0]) - assertHookCalls(two, [1, 1, 2, 1, 0]) + assertHookCalls(one, [1, 1, 2, 2, 1, 1, 0]) + assertHookCalls(two, [1, 1, 2, 2, 1, 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]) + assertHookCalls(one, [1, 1, 2, 2, 1, 1, 0]) + assertHookCalls(two, [1, 1, 2, 2, 2, 2, 0]) toggle2.value = true await nextTick() expect(serializeInner(root)).toBe(`
two
`) - assertHookCalls(one, [1, 1, 2, 1, 0]) - assertHookCalls(two, [1, 1, 3, 2, 0]) + assertHookCalls(one, [1, 1, 2, 2, 1, 1, 0]) + assertHookCalls(two, [1, 1, 3, 3, 2, 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]) + assertHookCalls(one, [1, 1, 2, 2, 2, 2, 0]) + assertHookCalls(two, [1, 1, 3, 3, 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 + assertHookCalls(one, [1, 1, 2, 2, 2, 2, 0]) + assertHookCalls(two, [1, 1, 3, 3, 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 + assertHookCalls(one, [1, 1, 2, 2, 2, 2, 0]) + assertHookCalls(two, [1, 1, 3, 3, 3, 3, 0]) // should not be affected toggle1.value = true await nextTick() expect(serializeInner(root)).toBe(`
two
`) - assertHookCalls(one, [1, 1, 3, 2, 0]) - assertHookCalls(two, [1, 1, 4, 3, 0]) + assertHookCalls(one, [1, 1, 3, 3, 2, 2, 0]) + assertHookCalls(two, [1, 1, 4, 4, 3, 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]) + assertHookCalls(one, [1, 1, 3, 3, 3, 3, 0]) + assertHookCalls(two, [1, 1, 4, 4, 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 + assertHookCalls(one, [1, 1, 4, 4, 3, 3, 0]) + assertHookCalls(two, [1, 1, 4, 4, 4, 4, 0]) // should remain inactive }) async function assertNameMatch( @@ -344,33 +350,33 @@ describe('KeepAlive', () => { render(h(App), root) expect(serializeInner(root)).toBe(`
one
`) - assertHookCalls(one, [1, 1, 1, 0, 0]) - assertHookCalls(two, [0, 0, 0, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 0, 0, 0]) + assertHookCalls(two, [0, 0, 0, 0, 0, 0, 0]) viewRef.value = 'two' await nextTick() expect(serializeInner(root)).toBe(`
two
`) - assertHookCalls(one, [1, 1, 1, 1, 0]) - assertHookCalls(two, [1, 1, 0, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 0]) + assertHookCalls(two, [1, 1, 0, 0, 0, 0, 0]) viewRef.value = 'one' await nextTick() expect(serializeInner(root)).toBe(`
one
`) - assertHookCalls(one, [1, 1, 2, 1, 0]) - assertHookCalls(two, [1, 1, 0, 0, 1]) + assertHookCalls(one, [1, 1, 2, 2, 1, 1, 0]) + assertHookCalls(two, [1, 1, 0, 0, 0, 0, 1]) viewRef.value = 'two' await nextTick() expect(serializeInner(root)).toBe(`
two
`) - assertHookCalls(one, [1, 1, 2, 2, 0]) - assertHookCalls(two, [2, 2, 0, 0, 1]) + assertHookCalls(one, [1, 1, 2, 2, 2, 2, 0]) + assertHookCalls(two, [2, 2, 0, 0, 0, 0, 1]) // teardown outerRef.value = false await nextTick() expect(serializeInner(root)).toBe(``) - assertHookCalls(one, [1, 1, 2, 2, 1]) - assertHookCalls(two, [2, 2, 0, 0, 2]) + assertHookCalls(one, [1, 1, 2, 2, 2, 2, 1]) + assertHookCalls(two, [2, 2, 0, 0, 0, 0, 2]) } describe('props', () => { @@ -437,9 +443,15 @@ describe('KeepAlive', () => { const spyAC = jest.fn() const spyBC = jest.fn() const spyCC = jest.fn() + const spyABA = jest.fn() + const spyBBA = jest.fn() + const spyCBA = jest.fn() const spyAA = jest.fn() const spyBA = jest.fn() const spyCA = jest.fn() + const spyABDA = jest.fn() + const spyBBDA = jest.fn() + const spyCBDA = jest.fn() const spyADA = jest.fn() const spyBDA = jest.fn() const spyCDA = jest.fn() @@ -450,15 +462,21 @@ describe('KeepAlive', () => { function assertCount(calls: number[]) { expect([ spyAC.mock.calls.length, + spyABA.mock.calls.length, spyAA.mock.calls.length, + spyABDA.mock.calls.length, spyADA.mock.calls.length, spyAUM.mock.calls.length, spyBC.mock.calls.length, + spyBBA.mock.calls.length, spyBA.mock.calls.length, + spyBBDA.mock.calls.length, spyBDA.mock.calls.length, spyBUM.mock.calls.length, spyCC.mock.calls.length, + spyCBA.mock.calls.length, spyCA.mock.calls.length, + spyCBDA.mock.calls.length, spyCDA.mock.calls.length, spyCUM.mock.calls.length ]).toEqual(calls) @@ -469,21 +487,27 @@ describe('KeepAlive', () => { a: { render: () => `one`, created: spyAC, + beforeActivate: spyABA, activated: spyAA, + beforeDeactivate: spyABDA, deactivated: spyADA, unmounted: spyAUM }, b: { render: () => `two`, created: spyBC, + beforeActivate: spyBBA, activated: spyBA, + beforeDeactivate: spyBBDA, deactivated: spyBDA, unmounted: spyBUM }, c: { render: () => `three`, created: spyCC, + beforeActivate: spyCBA, activated: spyCA, + beforeDeactivate: spyCBDA, deactivated: spyCDA, unmounted: spyCUM } @@ -497,26 +521,26 @@ describe('KeepAlive', () => { } } render(h(App), root) - assertCount([1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + assertCount([1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) viewRef.value = 'b' await nextTick() - assertCount([1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0]) + assertCount([1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]) viewRef.value = 'c' await nextTick() // should prune A because max cache reached - assertCount([1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0]) + assertCount([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0]) viewRef.value = 'b' await nextTick() // B should be reused, and made latest - assertCount([1, 1, 1, 1, 1, 2, 1, 0, 1, 1, 1, 0]) + assertCount([1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 0, 1, 1, 1, 1, 1, 0]) viewRef.value = 'a' await nextTick() // C should be pruned because B was used last so C is the oldest cached - assertCount([2, 2, 1, 1, 1, 2, 2, 0, 1, 1, 1, 1]) + assertCount([2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1]) }) }) @@ -562,18 +586,18 @@ describe('KeepAlive', () => { viewRef.value = 'two' await nextTick() - assertHookCalls(one, [1, 1, 1, 1, 0]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 0]) + assertHookCalls(two, [1, 1, 1, 1, 0, 0, 0]) includeRef.value = 'two' await nextTick() - assertHookCalls(one, [1, 1, 1, 1, 1]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 1]) + assertHookCalls(two, [1, 1, 1, 1, 0, 0, 0]) viewRef.value = 'one' await nextTick() - assertHookCalls(one, [2, 2, 1, 1, 1]) - assertHookCalls(two, [1, 1, 1, 1, 0]) + assertHookCalls(one, [2, 2, 1, 1, 1, 1, 1]) + assertHookCalls(two, [1, 1, 1, 1, 1, 1, 0]) }) test('on exclude change', async () => { @@ -581,18 +605,18 @@ describe('KeepAlive', () => { viewRef.value = 'two' await nextTick() - assertHookCalls(one, [1, 1, 1, 1, 0]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 0]) + assertHookCalls(two, [1, 1, 1, 1, 0, 0, 0]) excludeRef.value = 'one' await nextTick() - assertHookCalls(one, [1, 1, 1, 1, 1]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 1]) + assertHookCalls(two, [1, 1, 1, 1, 0, 0, 0]) viewRef.value = 'one' await nextTick() - assertHookCalls(one, [2, 2, 1, 1, 1]) - assertHookCalls(two, [1, 1, 1, 1, 0]) + assertHookCalls(one, [2, 2, 1, 1, 1, 1, 1]) + assertHookCalls(two, [1, 1, 1, 1, 1, 1, 0]) }) test('on include change + view switch', async () => { @@ -600,15 +624,15 @@ describe('KeepAlive', () => { viewRef.value = 'two' await nextTick() - assertHookCalls(one, [1, 1, 1, 1, 0]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 0]) + assertHookCalls(two, [1, 1, 1, 1, 0, 0, 0]) includeRef.value = 'one' viewRef.value = 'one' await nextTick() - assertHookCalls(one, [1, 1, 2, 1, 0]) + assertHookCalls(one, [1, 1, 2, 2, 1, 1, 0]) // two should be pruned - assertHookCalls(two, [1, 1, 1, 1, 1]) + assertHookCalls(two, [1, 1, 1, 1, 1, 1, 1]) }) test('on exclude change + view switch', async () => { @@ -616,15 +640,15 @@ describe('KeepAlive', () => { viewRef.value = 'two' await nextTick() - assertHookCalls(one, [1, 1, 1, 1, 0]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 0]) + assertHookCalls(two, [1, 1, 1, 1, 0, 0, 0]) excludeRef.value = 'two' viewRef.value = 'one' await nextTick() - assertHookCalls(one, [1, 1, 2, 1, 0]) + assertHookCalls(one, [1, 1, 2, 2, 1, 1, 0]) // two should be pruned - assertHookCalls(two, [1, 1, 1, 1, 1]) + assertHookCalls(two, [1, 1, 1, 1, 1, 1, 1]) }) test('should not prune current active instance', async () => { @@ -632,13 +656,13 @@ describe('KeepAlive', () => { includeRef.value = 'two' await nextTick() - assertHookCalls(one, [1, 1, 1, 0, 0]) - assertHookCalls(two, [0, 0, 0, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 0, 0, 0]) + assertHookCalls(two, [0, 0, 0, 0, 0, 0, 0]) viewRef.value = 'two' await nextTick() - assertHookCalls(one, [1, 1, 1, 0, 1]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 0, 0, 1]) + assertHookCalls(two, [1, 1, 1, 1, 0, 0, 0]) }) async function assertAnonymous(include: boolean) { @@ -937,8 +961,8 @@ describe('KeepAlive', () => { expect(serializeInner(root)).toBe(`
one
`) expect(_cache.size).toBe(1) expect([..._cache.keys()]).toEqual([one]) - assertHookCalls(one, [1, 1, 1, 0, 0]) - assertHookCalls(two, [0, 0, 0, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 0, 0, 0]) + assertHookCalls(two, [0, 0, 0, 0, 0, 0, 0]) instanceRef.value.msg = 'changed' await nextTick() @@ -949,8 +973,8 @@ describe('KeepAlive', () => { expect(serializeInner(root)).toBe(`
two
`) expect(_cache.size).toBe(2) expect([..._cache.keys()]).toEqual([one, two]) - assertHookCalls(one, [1, 1, 1, 1, 0]) - assertHookCalls(two, [1, 1, 1, 0, 0]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 0]) + assertHookCalls(two, [1, 1, 1, 1, 0, 0, 0]) // delete the cache manually const itr = _cache.keys() @@ -958,21 +982,21 @@ describe('KeepAlive', () => { cache.pruneCacheEntry!(_cache.get(key1)) _cache.delete(key1) await nextTick() - assertHookCalls(one, [1, 1, 1, 1, 1]) + assertHookCalls(one, [1, 1, 1, 1, 1, 1, 1]) viewRef.value = 'one' await nextTick() expect(serializeInner(root)).toBe(`
one
`) expect(_cache.size).toBe(2) expect([..._cache.keys()]).toEqual([two, one]) - assertHookCalls(one, [2, 2, 2, 1, 1]) - assertHookCalls(two, [1, 1, 1, 1, 0]) + assertHookCalls(one, [2, 2, 2, 2, 1, 1, 1]) + assertHookCalls(two, [1, 1, 1, 1, 1, 1, 0]) toggle.value = false await nextTick() expect(_cache.size).toBe(0) - assertHookCalls(one, [2, 2, 2, 2, 2]) - assertHookCalls(two, [1, 1, 1, 1, 1]) + assertHookCalls(one, [2, 2, 2, 2, 2, 2, 2]) + assertHookCalls(two, [1, 1, 1, 1, 1, 1, 1]) }) test('warn custom cache with the `max` prop', () => { @@ -1017,7 +1041,9 @@ describe('KeepAlive', () => { const toggle = ref(true) const Comp = { mounted: jest.fn(), + beforeActivate: jest.fn(), activated: jest.fn(), + beforeDeactivate: jest.fn(), deactivated: jest.fn(), unmounted: jest.fn(), render() { @@ -1027,7 +1053,9 @@ describe('KeepAlive', () => { function assertCount(calls: number[]) { expect([ Comp.mounted.mock.calls.length, + Comp.beforeActivate.mock.calls.length, Comp.activated.mock.calls.length, + Comp.beforeDeactivate.mock.calls.length, Comp.deactivated.mock.calls.length, Comp.unmounted.mock.calls.length ]).toEqual(calls) @@ -1050,21 +1078,21 @@ describe('KeepAlive', () => { render(h(App), root) await nextTick() expect(serializeInner(root)).toBe(`one`) - assertCount([1, 1, 0, 0]) + assertCount([1, 1, 1, 0, 0, 0]) expect((cache as any)._cache.size).toBe(1) expect([...(cache as any)._cache.keys()]).toEqual([0]) dynamicKey.value = 1 await nextTick() expect(serializeInner(root)).toBe(`one`) - assertCount([2, 2, 1, 0]) + assertCount([2, 2, 2, 1, 1, 0]) expect((cache as any)._cache.size).toBe(2) expect([...(cache as any)._cache.keys()]).toEqual([0, 1]) toggle.value = false await nextTick() expect(serializeInner(root)).toBe(``) - assertCount([2, 2, 2, 2]) + assertCount([2, 2, 2, 2, 2, 2]) expect((cache as any)._cache.size).toBe(0) }) }) diff --git a/packages/runtime-core/src/apiLifecycle.ts b/packages/runtime-core/src/apiLifecycle.ts index 4d7b53d36a7..92966c36ced 100644 --- a/packages/runtime-core/src/apiLifecycle.ts +++ b/packages/runtime-core/src/apiLifecycle.ts @@ -11,7 +11,12 @@ import { warn } from './warning' import { toHandlerKey } from '@vue/shared' import { DebuggerEvent, pauseTracking, resetTracking } from '@vue/reactivity' -export { onActivated, onDeactivated } from './components/KeepAlive' +export { + onActivated, + onDeactivated, + onBeforeActivate, + onBeforeDeactivate +} from './components/KeepAlive' export function injectHook( type: LifecycleHooks, diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 6e2c1d53d78..9b622354874 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -157,7 +157,9 @@ export const enum LifecycleHooks { UPDATED = 'u', BEFORE_UNMOUNT = 'bum', UNMOUNTED = 'um', + BEFORE_DEACTIVATE = 'bda', DEACTIVATED = 'da', + BEFORE_ACTIVATE = 'ba', ACTIVATED = 'a', RENDER_TRIGGERED = 'rtg', RENDER_TRACKED = 'rtc', @@ -386,10 +388,18 @@ export interface ComponentInternalInstance { * @internal */ [LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.BEFORE_ACTIVATE]: LifecycleHook /** * @internal */ [LifecycleHooks.ACTIVATED]: LifecycleHook + /** + * @internal + */ + [LifecycleHooks.BEFORE_DEACTIVATE]: LifecycleHook /** * @internal */ @@ -477,7 +487,9 @@ export function createComponentInstance( u: null, um: null, bum: null, + bda: null, da: null, + ba: null, a: null, rtg: null, rtc: null, diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index e00206baa00..38594175af0 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -31,7 +31,9 @@ import { onRenderTracked, onBeforeUnmount, onUnmounted, + onBeforeActivate, onActivated, + onBeforeDeactivate, onDeactivated, onRenderTriggered, DebuggerHook, @@ -410,7 +412,9 @@ interface LegacyOptions< mounted?(): void beforeUpdate?(): void updated?(): void + beforeActivate?(): void activated?(): void + beforeDeactivate?(): void deactivated?(): void /** @deprecated use `beforeUnmount` instead */ beforeDestroy?(): void @@ -504,7 +508,9 @@ export function applyOptions( mounted, beforeUpdate, updated, + beforeActivate, activated, + beforeDeactivate, deactivated, beforeDestroy, beforeUnmount, @@ -775,9 +781,15 @@ export function applyOptions( if (updated) { onUpdated(updated.bind(publicThis)) } + if (beforeActivate) { + onBeforeActivate(beforeActivate.bind(publicThis)) + } if (activated) { onActivated(activated.bind(publicThis)) } + if (beforeDeactivate) { + onBeforeDeactivate(beforeDeactivate.bind(publicThis)) + } if (deactivated) { onDeactivated(deactivated.bind(publicThis)) } diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 5573f63504d..163c339ddbb 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -191,6 +191,12 @@ const KeepAliveImpl = { sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => { const instance = vnode.component! + if (instance.ba) { + const currentState = instance.isDeactivated + instance.isDeactivated = false + invokeArrayFns(instance.ba) + instance.isDeactivated = currentState + } move(vnode, container, anchor, MoveType.ENTER, parentSuspense) // in case props have changed patch( @@ -218,8 +224,14 @@ const KeepAliveImpl = { sharedContext.deactivate = (vnode: VNode) => { const instance = vnode.component! + if (instance.bda) { + invokeKeepAliveHooks(instance.bda) + } move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense) queuePostRenderEffect(() => { + if (instance.bda) { + resetHookState(instance.bda) + } if (instance.da) { invokeArrayFns(instance.da) } @@ -279,6 +291,10 @@ const KeepAliveImpl = { cached.type === vnode.type && (props.matchBy !== 'key' || cached.key === vnode.key) ) { + // invoke its beforeDeactivate hook here + if (vnode.component!.bda) { + invokeArrayFns(vnode.component!.bda) + } // current instance will be unmounted as part of keep-alive's unmount resetShapeFlag(vnode) // but invoke its deactivated hook here @@ -384,6 +400,13 @@ function matches(pattern: MatchPattern, name: string): boolean { return false } +export function onBeforeActivate( + hook: Function, + target?: ComponentInternalInstance | null +) { + registerKeepAliveHook(hook, LifecycleHooks.BEFORE_ACTIVATE, target) +} + export function onActivated( hook: Function, target?: ComponentInternalInstance | null @@ -391,6 +414,13 @@ export function onActivated( registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target) } +export function onBeforeDeactivate( + hook: Function, + target?: ComponentInternalInstance | null +) { + registerKeepAliveHook(hook, LifecycleHooks.BEFORE_DEACTIVATE, target) +} + export function onDeactivated( hook: Function, target?: ComponentInternalInstance | null @@ -398,6 +428,7 @@ export function onDeactivated( registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target) } +export type WrappedHook = Function & { __called?: boolean } function registerKeepAliveHook( hook: Function & { __wdc?: Function }, type: LifecycleHooks, @@ -406,7 +437,7 @@ function registerKeepAliveHook( // 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 = + const wrappedHook: WrappedHook = hook.__wdc || (hook.__wdc = () => { // only fire the hook if the target instance is NOT in a deactivated branch. @@ -419,6 +450,7 @@ function registerKeepAliveHook( } hook() }) + wrappedHook.__called = false 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. @@ -471,3 +503,17 @@ function getMatchingName(vnode: VNode, matchBy: 'name' | 'key') { } return String(vnode.key) } + +export function invokeKeepAliveHooks(hooks: WrappedHook[]) { + for (let i = 0; i < hooks.length; i++) { + const hook = hooks[i] + if (!hook.__called) { + hook() + hook.__called = true + } + } +} + +export function resetHookState(hooks: WrappedHook[]) { + hooks.forEach((hook: WrappedHook) => (hook.__called = false)) +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 6026d4c248d..e4050d635f5 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -33,7 +33,9 @@ export { onUpdated, onBeforeUnmount, onUnmounted, + onBeforeActivate, onActivated, + onBeforeDeactivate, onDeactivated, onRenderTracked, onRenderTriggered, diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 15f86faa438..e7f47ca8750 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -66,7 +66,12 @@ import { SuspenseImpl } from './components/Suspense' import { TeleportImpl, TeleportVNode } from './components/Teleport' -import { isKeepAlive, KeepAliveContext } from './components/KeepAlive' +import { + isKeepAlive, + KeepAliveContext, + invokeKeepAliveHooks, + resetHookState +} from './components/KeepAlive' import { registerHMR, unregisterHMR, isHmrUpdating } from './hmr' import { ErrorCodes, @@ -1465,14 +1470,14 @@ function baseCreateRenderer( }, parentSuspense) } // activated hook for keep-alive roots. - // #1742 activated hook must be accessed after first render + // #1742 beforeActivate/activated hook must be accessed after first render // since the hook may be injected by a child keep-alive - const { a } = instance - if ( - a && - initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE - ) { - queuePostRenderEffect(a, parentSuspense) + const { ba, a } = instance + if (initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { + ba && invokeKeepAliveHooks(ba) + a && queuePostRenderEffect(a, parentSuspense) + // reset hook state + ba && queuePostRenderEffect(() => resetHookState(ba), parentSuspense) } instance.isMounted = true From 72780d875c2f5465dac53fa34059b3a2dccd746d Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Wed, 17 Mar 2021 16:15:28 +0800 Subject: [PATCH 7/8] chore: add comment --- packages/runtime-core/src/components/KeepAlive.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 163c339ddbb..03f9c5a030a 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -428,6 +428,8 @@ export function onDeactivated( registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target) } +// the beforeActivate/beforeDeactivate hook is called synchronously +// and cannot be deduped by scheduler, so we need the `__called` flag export type WrappedHook = Function & { __called?: boolean } function registerKeepAliveHook( hook: Function & { __wdc?: Function }, From 28d4e9420d0671d7e431ada3efeed74be9e76e58 Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Wed, 17 Mar 2021 19:37:47 +0800 Subject: [PATCH 8/8] refactor: use isSuspense --- packages/runtime-core/src/components/KeepAlive.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 03f9c5a030a..bc76d535669 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -35,6 +35,7 @@ import { } from '../renderer' import { setTransitionHooks } from './BaseTransition' import { ComponentRenderContext } from '../componentPublicInstance' +import { isSuspense } from './Suspense' type MatchPattern = string | RegExp | string[] | RegExp[] @@ -323,7 +324,7 @@ const KeepAliveImpl = { } else if ( !isVNode(rawVNode) || (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) && - !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE)) + !isSuspense(rawVNode.type)) ) { current = null return rawVNode @@ -348,7 +349,7 @@ const KeepAliveImpl = { // clone vnode if it's reused because we are going to mutate it if (vnode.el) { vnode = cloneVNode(vnode) - if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) { + if (isSuspense(rawVNode.type)) { rawVNode.ssContent = vnode } } @@ -374,7 +375,7 @@ const KeepAliveImpl = { vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE current = vnode - return rawVNode.shapeFlag & ShapeFlags.SUSPENSE ? rawVNode : vnode + return isSuspense(rawVNode.type) ? rawVNode : vnode } } } @@ -496,7 +497,7 @@ function resetShapeFlag(vnode: VNode) { } function getInnerChild(vnode: VNode) { - return vnode.shapeFlag & ShapeFlags.SUSPENSE ? vnode.ssContent! : vnode + return isSuspense(vnode.type) ? vnode.ssContent! : vnode } function getMatchingName(vnode: VNode, matchBy: 'name' | 'key') {