关于vue2.7.16的transition与keep-alive嵌套bug

BUG复线

版本信息vue:2.7.16

场景代码如下

vue 复制代码
<transition name="fade-transform" mode="out-in">
      <keep-alive :include="cachedViews">
        <router-view />
      </keep-alive>
</transition>

其中两个重点

  1. transitionmode值为out-in
  2. keep-alive的入参includeexclude是随时变化的

满足上述条件,当我们切换路由时,更新include的值,我们期望菜单内容是先out,后in。即先隐藏老页面,再显示新页面。

但不符合我们的预期,即mode不生效 。但vue 2.7.15没有这个问题

由于vue v2版本官方不再更新,于是呼我们需要自己去调整代码了

产生原因

起因:修复内存泄露bug

修改代码

原因分析

keep-alvie代码的mounted中监听了includeexclude,调用了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)移除了子节点的关联

transitiongetRealChild方法会获取真实子节点,其实就是为了避免子节点是抽象节点

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不存在了,因为每次includeexclude发生变化pruneCache就会被调用,从而删除componentOptions.children,导致这个if始终进不去,从而mode值不生效。直到includeexclude达到稳定。

如何修复

方案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,以下是改动,其中改动的思路是

  1. 移除$vnode.componentOptions!.children = undefined,这样transition的oldChild就能找到内容。(如果不考虑内存泄露,则下述代码均可以不改)
  2. 在pruneCacheEntry中移除所有对要关闭的vnode的引用,这个引用怎么来的,请看vue中keepalive的内存问题
  3. 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
    })
  }
}
相关推荐
爱生活的苏苏3 分钟前
vue生成二维码图片+文字说明
前端·vue.js
前端百草阁28 分钟前
从npm库 Vue 组件到独立SDK:打包与 CDN 引入的最佳实践
前端·vue.js·npm
且白1 小时前
vsCode使用本地低版本node启动配置文件
前端·vue.js·vscode·编辑器
疯狂的沙粒1 小时前
在uni-app中如何从Options API迁移到Composition API?
javascript·vue.js·uni-app
Revol_C2 小时前
【调试日志】我只是用wangeditor上传图片而已,页面咋就崩溃了呢~
前端·vue.js·程序员
HelloWord~3 小时前
SpringSecurity+vue通用权限系统2
java·vue.js
bysking3 小时前
【27-vue3】vue3版本的"指令式弹窗"逻辑函数createModal-bysking
前端·vue.js
龚思凯3 小时前
Vue 3 中 watch 监听引用类型的深度解析与全面实践
前端·vue.js
red润4 小时前
封装hook,复刻掘金社区,暗黑白天主题切换功能
前端·javascript·vue.js
Momoly084 小时前
vue3+el-table 利用插槽自定义数据样式
前端·javascript·vue.js