From e7baaa12055231c9367fa1c7bf917e534bd8a739 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 16 Apr 2021 19:19:29 +0200 Subject: [PATCH] fix(keep-alive): cache what is really needed not the whole VNode data (#12015) Co-authored-by: zrh122 <1229550935@qq.com> --- src/core/components/keep-alive.js | 56 ++++++++++++---- .../component/component-keep-alive.spec.js | 67 +++++++++++++++++++ 2 files changed, 109 insertions(+), 14 deletions(-) diff --git a/src/core/components/keep-alive.js b/src/core/components/keep-alive.js index fb4cf1e8..83a42aab 100644 --- a/src/core/components/keep-alive.js +++ b/src/core/components/keep-alive.js @@ -3,7 +3,13 @@ import { isRegExp, remove } from 'shared/util' import { getFirstComponentChild } from 'core/vdom/helpers/index' -type VNodeCache = { [key: string]: ?VNode }; +type CacheEntry = { + name: ?string; + tag: ?string; + componentInstance: Component; +}; + +type CacheEntryMap = { [key: string]: ?CacheEntry }; function getComponentName (opts: ?VNodeComponentOptions): ?string { return opts && (opts.Ctor.options.name || opts.tag) @@ -24,9 +30,9 @@ function matches (pattern: string | RegExp | Array, name: string): boole function pruneCache (keepAliveInstance: any, filter: Function) { const { cache, keys, _vnode } = keepAliveInstance for (const key in cache) { - const cachedNode: ?VNode = cache[key] - if (cachedNode) { - const name: ?string = getComponentName(cachedNode.componentOptions) + const entry: ?CacheEntry = cache[key] + if (entry) { + const name: ?string = entry.name if (name && !filter(name)) { pruneCacheEntry(cache, key, keys, _vnode) } @@ -35,14 +41,14 @@ function pruneCache (keepAliveInstance: any, filter: Function) { } function pruneCacheEntry ( - cache: VNodeCache, + cache: CacheEntryMap, key: string, keys: Array, current?: VNode ) { - const cached = cache[key] - if (cached && (!current || cached.tag !== current.tag)) { - cached.componentInstance.$destroy() + const entry: ?CacheEntry = cache[key] + if (entry && (!current || entry.tag !== current.tag)) { + entry.componentInstance.$destroy() } cache[key] = null remove(keys, key) @@ -60,6 +66,26 @@ export default { max: [String, Number] }, + methods: { + cacheVNode() { + const { cache, keys, vnodeToCache, keyToCache } = this + if (vnodeToCache) { + const { tag, componentInstance, componentOptions } = vnodeToCache + cache[keyToCache] = { + name: getComponentName(componentOptions), + tag, + componentInstance, + } + keys.push(keyToCache) + // prune oldest entry + if (this.max && keys.length > parseInt(this.max)) { + pruneCacheEntry(cache, keys[0], keys, this._vnode) + } + this.vnodeToCache = null + } + } + }, + created () { this.cache = Object.create(null) this.keys = [] @@ -72,6 +98,7 @@ export default { }, mounted () { + this.cacheVNode() this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) @@ -80,6 +107,10 @@ export default { }) }, + updated () { + this.cacheVNode() + }, + render () { const slot = this.$slots.default const vnode: VNode = getFirstComponentChild(slot) @@ -109,12 +140,9 @@ export default { remove(keys, key) keys.push(key) } else { - cache[key] = vnode - keys.push(key) - // prune oldest entry - if (this.max && keys.length > parseInt(this.max)) { - pruneCacheEntry(cache, keys[0], keys, this._vnode) - } + // delay setting the cache until update + this.vnodeToCache = vnode + this.keyToCache = key } vnode.data.keepAlive = true diff --git a/test/unit/features/component/component-keep-alive.spec.js b/test/unit/features/component/component-keep-alive.spec.js index a2cdf6a4..c8734958 100644 --- a/test/unit/features/component/component-keep-alive.spec.js +++ b/test/unit/features/component/component-keep-alive.spec.js @@ -572,6 +572,73 @@ describe('Component keep-alive', () => { }).then(done) }) + it('max=1', done => { + const spyA = jasmine.createSpy() + const spyB = jasmine.createSpy() + const spyC = jasmine.createSpy() + const spyAD = jasmine.createSpy() + const spyBD = jasmine.createSpy() + const spyCD = jasmine.createSpy() + + function assertCount (calls) { + expect([ + spyA.calls.count(), + spyAD.calls.count(), + spyB.calls.count(), + spyBD.calls.count(), + spyC.calls.count(), + spyCD.calls.count() + ]).toEqual(calls) + } + + const vm = new Vue({ + template: ` + + + + `, + data: { + n: 'aa' + }, + components: { + aa: { + template: '
a
', + created: spyA, + destroyed: spyAD + }, + bb: { + template: '
bbb
', + created: spyB, + destroyed: spyBD + }, + cc: { + template: '
ccc
', + created: spyC, + destroyed: spyCD + } + } + }).$mount() + + assertCount([1, 0, 0, 0, 0, 0]) + vm.n = 'bb' + waitForUpdate(() => { + // should prune A because max cache reached + assertCount([1, 1, 1, 0, 0, 0]) + vm.n = 'cc' + }).then(() => { + // should prune B because max cache reached + assertCount([1, 1, 1, 1, 1, 0]) + vm.n = 'bb' + }).then(() => { + // B is recreated + assertCount([1, 1, 2, 1, 1, 1]) + vm.n = 'aa' + }).then(() => { + // B is destroyed and A recreated + assertCount([2, 1, 2, 2, 1, 1]) + }).then(done) + }) + it('should warn unknown component inside', () => { new Vue({ template: ``