fix(keep-alive): cache what is really needed not the whole VNode data (#12015)

Co-authored-by: zrh122 <1229550935@qq.com>
This commit is contained in:
Eduardo San Martin Morote 2021-04-16 19:19:29 +02:00 committed by GitHub
parent 2b93e86aa1
commit e7baaa1205
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 109 additions and 14 deletions

View File

@ -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<string>, 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<string>,
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

View File

@ -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: `
<keep-alive max="1">
<component :is="n"></component>
</keep-alive>
`,
data: {
n: 'aa'
},
components: {
aa: {
template: '<div>a</div>',
created: spyA,
destroyed: spyAD
},
bb: {
template: '<div>bbb</div>',
created: spyB,
destroyed: spyBD
},
cc: {
template: '<div>ccc</div>',
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: `<keep-alive><foo/></keep-alive>`