BUG复线
版本信息vue:2.7.16
场景代码如下
vue
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<router-view />
</keep-alive>
</transition>
其中两个重点
transition
的mode
值为out-in
keep-alive
的入参include
或exclude
是随时变化的
满足上述条件,当我们切换路由时,更新include的值,我们期望菜单内容是先out,后in。即先隐藏老页面,再显示新页面。
但不符合我们的预期,即mode
不生效 。但vue 2.7.15
没有这个问题
由于vue v2
版本官方不再更新,于是呼我们需要自己去调整代码了
产生原因
起因:修复内存泄露bug
修改代码
原因分析
keep-alvie代码的mounted
中监听了include
和exclude
,调用了pruneCache
方法
ts
function pruneCache(
keepAliveInstance: {
cache: CacheEntryMap
keys: string[]
_vnode: VNode
$vnode: VNode
},
filter: Function
) {
const { cache, keys, _vnode, $vnode } = keepAliveInstance
for (const key in cache) {
const entry = cache[key]
if (entry) {
const name = entry.name
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
// 这里
$vnode.componentOptions!.children = undefined
}
function pruneCacheEntry(
cache: CacheEntryMap,
key: string,
keys: Array<string>,
current?: VNode
) {
const entry = cache[key]
if (entry && (!current || entry.tag !== current.tag)) {
// @ts-expect-error can be undefined
entry.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
其中$vnode.componentOptions!.children = undefined
将自己的vnode(keep-alive
)移除了子节点的关联
在transition中getRealChild
方法会获取真实子节点,其实就是为了避免子节点是抽象节点
kotlin
function getFirstComponentChild(children) {
if (isArray(children)) {
for (var i = 0; i < children.length; i++) {
var c = children[i];
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c;
}
}
}
}
function getRealChild(vnode?: VNode): VNode | undefined {
const compOptions = vnode && vnode.componentOptions
if (compOptions && compOptions.Ctor.options.abstract) {
return getRealChild(getFirstComponentChild(compOptions.children))
} else {
return vnode
}
}
// transition中
const rawChild: VNode = children[0]
// 获取新的真实子节点
const child = getRealChild(rawChild)
// 获取旧的真实子节点
const oldChild = getRealChild(oldRawChild)
// 如果旧节点存在
if (
oldChild &&
oldChild.data &&
!isSameChild(child, oldChild) &&
!isAsyncPlaceholder(oldChild) &&
// #6687 component root is a comment node
!(
oldChild.componentInstance &&
oldChild.componentInstance._vnode!.isComment
)
) {
// handle transition mode
if (mode === 'out-in') {
return placeholder(h, rawChild)
} else if (mode === 'in-out') {
// ...
}
}
return rawChild
问题就出在oldChild不存在了,因为每次include
或exclude
发生变化pruneCache
就会被调用,从而删除componentOptions.children,导致这个if始终进不去,从而mode值不生效。直到include
和exclude
达到稳定。
如何修复
方案1: 项目大先切换vue:2.7.15
,别升级到最高,等待其他大佬解决(推荐)
方案2: 不考虑mode="out-in"
,对transform节点添加绝对定位样式,但可能会对页面内容有影响
css
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s ease;
}
.fade-transform-enter {
transform: translateX(-30px);
}
.fade-transform-leave-to {
position: absolute;
width: 100%;
opacity: 0;
transform: translateX(30px);
}
方案3: 使用稳定的include和exclude,即不将其放状态中
方案4: 项目小可以先按以下方法调整(不推荐)
备注:该方法为个人修复,且只经过个人测试,未经过大量测试,同时代码存在可优化的地方。如果有大佬有好的解决方法,麻烦评论一下,感激不尽。
重写keep-alive,以下是改动,其中改动的思路是
- 移除$vnode.componentOptions!.children = undefined,这样transition的oldChild就能找到内容。(如果不考虑内存泄露,则下述代码均可以不改)
- 在pruneCacheEntry中移除所有对要关闭的vnode的引用,这个引用怎么来的,请看vue中keepalive的内存问题
- watch中添加setTimeout(可能会产生不好的影响),如果不加会在销毁tab时同样回去不到oldChild
ts
function pruneCache(
keepAliveInstance: {
cache: CacheEntryMap
keys: string[]
_vnode: VNode
$vnode: VNode
},
filter: Function
) {
const { cache, keys, _vnode, $vnode } = keepAliveInstance
for (const key in cache) {
const entry = cache[key]
if (entry) {
const name = entry.name
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
// 移除这里
// $vnode.componentOptions!.children = undefined
}
function pruneCacheEntry(
cache: CacheEntryMap,
key: string,
keys: Array<string>,
current?: VNode
) {
const entry = cache[key]
if (entry && (!current || entry.tag !== current.tag)) {
// @ts-expect-error can be undefined
entry.componentInstance.$destroy()
}
// 这里添加
let entryVnode = null
if (entry) {
entryVnode = entry.componentInstance.$vnode
}
for (const c in cache) {
if (!cache[c]) {
continue;
}
const {componentInstance} = cache[c];
const {parent} = componentInstance.$vnode
if (parent && parent.componentOptions.children&&parent.componentOptions.children[0] === entryVnode) {
parent.componentOptions.children = undefined;
}
}
cache[key] = null
remove(keys, key)
}
// keep-alive中
mounted() {
this.cacheVNode()
this.$watch('include', val => {
setTimeout(() => {
pruneCache(this, name => matches(val, name))
})
})
this.$watch('exclude', val => {
setTimeout(() => {
pruneCache(this, name => matches(val, name))
})
})
},
以下是该文件全部内容
ts
import type { VNodeComponentOptions, Component, VNode } from 'vue'
const _toString = Object.prototype.toString;
function isRegExp(v: unknown): v is RegExp{
return _toString.call(v) === '[object RegExp]';
}
const {isArray} = Array;
function getComponentName(options) {
return options.name || options.__name || options._componentTag;
}
/**
* Remove an item from an array.
*/
function remove<T = any>(arr: T[], item: T) {
const len = arr.length;
if (len) {
// fast path for the only / last item
if (item === arr[len - 1]) {
arr.length = len - 1;
return;
}
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}
function isDef(v): v is NonNullable<typeof v>{
return v !== undefined && v !== null;
}
function isAsyncPlaceholder(node: VNode) {
return node.isComment && node.asyncFactory;
}
function getFirstComponentChild(children) {
if (isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i];
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c;
}
}
}
return undefined
}
type CacheEntry = {
name?: string
tag?: string
componentInstance?: Component
}
type CacheEntryMap = Record<string, CacheEntry | null>
function myGetComponentName(opts?: VNodeComponentOptions): string | null {
return opts && (getComponentName(opts.Ctor.options as any) || opts.tag)
}
function matches(
pattern: string | RegExp | Array<string>,
name: string
): boolean {
if (isArray(pattern)) {
return pattern.indexOf(name) > -1
} if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
function pruneCache(
keepAliveInstance: {
cache: CacheEntryMap
keys: string[]
_vnode: VNode
$vnode: VNode
},
filter: (name: string) => boolean
) {
const { cache, keys, _vnode,
// $vnode
} = keepAliveInstance
for (const key in cache) {
const entry = cache[key]
if (entry) {
const {name} = entry
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
// $vnode.componentOptions!.children = undefined
}
function pruneCacheEntry(
cache: CacheEntryMap,
key: string,
keys: Array<string>,
current?: VNode
) {
const entry = cache[key]
if (entry && (!current || entry.tag !== current.tag)) {
// @ts-expect-error can be undefined
entry.componentInstance.$destroy()
}
let entryVnode = null
if (entry) {
entryVnode = entry.componentInstance.$vnode
}
for (const c in cache) {
if (!cache[c]) {
continue;
}
const {componentInstance} = cache[c];
const {parent} = componentInstance.$vnode
if (parent && parent.componentOptions.children&&parent.componentOptions.children[0] === entryVnode) {
parent.componentOptions.children = undefined;
}
}
cache[key] = null
remove(keys, key)
}
const patternTypes = [String, RegExp, Array]
// TODO defineComponent
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
cache[keyToCache] = {
name: myGetComponentName(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 = []
},
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
this.cacheVNode()
this.$watch('include', val => {
setTimeout(() => {
pruneCache(this, name => matches(val, name))
})
})
this.$watch('exclude', val => {
setTimeout(() => {
pruneCache(this, name => matches(val, name))
})
})
},
updated() {
this.cacheVNode()
},
render() {
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name = myGetComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key =
vnode.key == null
? // same constructor may get registered as different local components
// so cid alone is not enough (#3269)
componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
// @ts-expect-error can vnode.data can be undefined
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
在全局入口文件注册Vue.component('KeepAlive', MyKeepAlive)
为什么不注册新的组件Vue.component('MyKeepAlive', MyKeepAlive)
。在transition
源码中有一个placeholder
函数,这里写死了keep-alive
标签,如果用新组件则transition也需要重写。
javascript
function placeholder(h: Function, rawChild: VNode): VNode | undefined {
// @ts-expect-error
if (/\d-keep-alive$/.test(rawChild.tag)) {
return h('keep-alive', {
props: rawChild.componentOptions!.propsData
})
}
}